things/Thing.cs

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.

NetworkingFile Access
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 );
	}
}