A game component class representing a generic physics/interaction entity called Thing. It stores 2D position/velocity, collision radius and collider, handles trigger enter/exit to track overlapping Things, does bounds checking and per-frame collision handling (calling a virtual Colliding method), and applies spawn scale on start.
using System;
using System.Runtime.ExceptionServices;
using Sandbox;
using Sandbox.Diagnostics;
using Sandbox.Utility;
public class Thing : Component, Component.ITriggerListener
{
public Vector2 Position2D
{
get { return (Vector2)WorldPosition; }
set { WorldPosition = new Vector3( value.x, value.y, WorldPosition.z ); }
}
public bool IsInTheAir { get; set; }
[Property] public Collider Collider { get; set; }
public TimeSince TimeSinceSpawn { get; protected set; }
public Vector2 Velocity { get; set; }
public float Deceleration { get; set; }
[Sync] public float Radius { get; set; }
public float PushStrength { get; set; }
public float Weight { get; set; } = 1f;
public bool ShouldCheckBounds { get; set; }
public virtual bool BoundsCheckIncludesRadius => false;
public virtual float BoundsExpand => 0f;
public bool IgnoreCollision { get; set; }
public bool IsRemoved { get; set; }
public float TimeScale { get; set; } // todo: move to Unit/Enemy?
protected List<Thing> Touching = new List<Thing>();
public List<string> CollideWithTags = new List<string>();
public virtual bool UseSpawnScale => true;
public virtual Vector3 SpawnScale => Vector3.One;
protected override void OnStart()
{
base.OnStart();
ApplySpawnScale();
if ( Collider is CapsuleCollider capsuleCollider )
Radius = capsuleCollider.Radius * WorldScale.x;
else if ( Collider is SphereCollider sphereCollider )
Radius = sphereCollider.Radius * WorldScale.x;
TimeSinceSpawn = 0f;
if ( IsProxy )
return;
TimeScale = 1f;
}
protected virtual void ApplySpawnScale()
{
if ( UseSpawnScale )
WorldScale = SpawnScale;
}
protected override void OnUpdate()
{
//Gizmo.Draw.Color = Color.White;
//Gizmo.Draw.Text( $"\n\n{TimeSinceSpawn}", new global::Transform( WorldPosition ) );
//Gizmo.Draw.LineCircle( WorldPosition, Vector3.Up, Radius );
if ( IsProxy )
return;
if ( !IgnoreCollision )
HandleCollision();
if( ShouldCheckBounds )
CheckBounds();
}
void HandleCollision()
{
var dt = Time.Delta;
for(int i = Touching.Count - 1; i >= 0; i--)
{
var other = Touching[i];
if ( !other.IsValid() )
{
Touching.RemoveAt( i );
continue;
}
if ( other.IgnoreCollision )
continue;
// todo: unnecessary to check radius since we're using colliders?
var dist_sqr = (Position2D - other.Position2D).LengthSquared;
var total_radius_sqr = MathF.Pow( Radius + other.Radius, 2f );
if ( dist_sqr < total_radius_sqr )
{
float percent = Utils.Map( dist_sqr, total_radius_sqr, 0f, 0f, 1f );
Colliding( other, percent, dt * TimeScale );
}
}
}
protected virtual void CheckBounds()
{
if ( BoundsCheckIncludesRadius || ((Vector2)WorldPosition).LengthSquared > Manager.Instance.BOUNDS_CHECK_SIZE_SQR )
{
var offset = BoundsCheckIncludesRadius ? Radius : 0f;
var expand = BoundsExpand;
if ( WorldPosition.x < Manager.Instance.BOUNDS_MIN.x - expand + offset )
OnOutOfBounds( Direction.Left );
else if ( WorldPosition.x > Manager.Instance.BOUNDS_MAX.x + expand - offset )
OnOutOfBounds( Direction.Right );
if ( WorldPosition.y < Manager.Instance.BOUNDS_MIN.y - expand + offset )
OnOutOfBounds( Direction.Down );
else if ( WorldPosition.y > Manager.Instance.BOUNDS_MAX.y + expand - offset )
OnOutOfBounds( Direction.Up );
}
}
protected virtual void OnOutOfBounds( Direction direction )
{
var offset = BoundsCheckIncludesRadius ? Radius : 0f;
var expand = BoundsExpand;
if ( direction == Direction.Left )
{
WorldPosition = new Vector3( Manager.Instance.BOUNDS_MIN.x - expand + offset, WorldPosition.y, WorldPosition.z );
Velocity = new Vector2( Math.Abs( Velocity.x ), Velocity.y );
}
else if( direction == Direction.Right )
{
WorldPosition = new Vector3( Manager.Instance.BOUNDS_MAX.x + expand - offset, WorldPosition.y, WorldPosition.z );
Velocity = new Vector2( -Math.Abs( Velocity.x ), Velocity.y );
}
else if( direction == Direction.Down )
{
WorldPosition = new Vector3( WorldPosition.x, Manager.Instance.BOUNDS_MIN.y - expand + offset, WorldPosition.z );
Velocity = new Vector2( Velocity.x, Math.Abs( Velocity.y ) );
}
else if( direction == Direction.Up )
{
WorldPosition = new Vector3( WorldPosition.x, Manager.Instance.BOUNDS_MAX.y + expand - offset, WorldPosition.z );
Velocity = new Vector2( Velocity.x, -Math.Abs( Velocity.y ) );
}
}
public virtual void Colliding( Thing other, float percent, float dt )
{
}
void ITriggerListener.OnTriggerEnter( Collider collider )
{
if ( IsProxy || !collider.IsTrigger || CollideWithTags.Count == 0 ) return;
var thing = collider.GetComponent<Thing>();
if ( !thing.IsValid() )
return;
if ( !thing.Tags.HasAny(CollideWithTags) )
return;
Touching.Add( thing );
}
void ITriggerListener.OnTriggerExit( Collider collider )
{
if ( IsProxy || !collider.IsTrigger || CollideWithTags.Count == 0 ) return;
var thing = collider.GetComponent<Thing>();
if ( !thing.IsValid() )
return;
if ( !thing.Tags.HasAny( CollideWithTags ) )
return;
Touching.Remove( thing );
}
}