things/Thing.cs

A game Component representing a generic Thing with 2D position, velocity, collision handling and bounds checking. It tracks nearby Things via trigger callbacks, computes collisions using radii, applies spawn scale, and responds to leaving world bounds by clamping position and reversing velocity on that axis.

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