using static Sandbox.Component;
namespace Sandbox;
///
/// Used to abstract the listening of collision events
///
class CollisionEventSystem : IDisposable
{
private readonly PhysicsBody _body;
private readonly GameObject _gameObject;
public Action OnCollisionStart;
public Action OnCollisionStop;
public Action OnCollisionUpdate;
public IReadOnlyCollection Touching => _touchingColliders is null ? Array.Empty() : _touchingColliders;
private record struct TouchingPair( Collider Self, Collider Other, GameObject GameObject );
private HashSet _touchingPairs;
private HashSet _touchingColliders;
private HashSet _touchingObjects;
private HashSet TouchingPairs => _touchingPairs ??= new();
private HashSet TouchingColliders => _touchingColliders ??= new();
private HashSet TouchingObjects => _touchingObjects ??= new();
public CollisionEventSystem( PhysicsBody body, GameObject go = null )
{
_body = body;
_gameObject = go ?? body.GameObject;
_body.OnIntersectionStart += OnIntersectionStart;
_body.OnIntersectionEnd += OnIntersectionEnd;
_body.OnIntersectionUpdate += OnIntersectionUpdate;
_body.OnTriggerBegin += OnTriggerBegin;
_body.OnTriggerEnd += OnTriggerEnd;
}
private void OnTriggerBegin( PhysicsIntersection c )
{
var self = new CollisionSource( c.Self );
var other = new CollisionSource( c.Other );
var gameObject = other.Body.GameObject;
OnObjectTriggerStart( self.Collider, gameObject );
OnColliderTriggerStart( self.Collider, other.Collider, gameObject );
}
private void OnTriggerEnd( PhysicsIntersectionEnd c )
{
var self = new CollisionSource( c.Self );
var other = new CollisionSource( c.Other );
var gameObject = other.Body.GameObject;
OnColliderTriggerStop( self.Collider, other.Collider, gameObject );
if ( IsTouching( other.Component ) )
return;
OnObjectTriggerStop( self.Collider, gameObject );
}
private void OnIntersectionStart( PhysicsIntersection c )
{
var self = new CollisionSource( c.Self );
var other = new CollisionSource( c.Other );
var o = new Collision( self, other, c.Contact );
OnCollisionStart?.Invoke( o );
_gameObject.Components.ExecuteEnabledInSelfAndDescendants( x => x.OnCollisionStart( o ) );
}
private bool IsTouching( Component other )
{
return false;
}
private void OnIntersectionEnd( PhysicsIntersectionEnd c )
{
var self = new CollisionSource( c.Self );
var other = new CollisionSource( c.Other );
var o = new CollisionStop( self, other );
OnCollisionStop?.Invoke( o );
_gameObject.Components.ExecuteEnabledInSelfAndDescendants( x => x.OnCollisionStop( o ) );
}
private void OnIntersectionUpdate( PhysicsIntersection c )
{
var self = new CollisionSource( c.Self );
var other = new CollisionSource( c.Other );
var o = new Collision( self, other, c.Contact );
OnCollisionUpdate?.Invoke( o );
_gameObject.Components.ExecuteEnabledInSelfAndDescendants( x => x.OnCollisionUpdate( o ) );
}
private void OnColliderTriggerStart( Collider self, Collider other, GameObject go )
{
if ( self is null )
return;
if ( other is null )
return;
if ( !TouchingPairs.Any( x => x.Other == other ) )
{
TouchingColliders.Add( other );
other.OnComponentDisabled += RemoveDeactivated;
}
if ( !TouchingPairs.Add( new TouchingPair( self, other, go ) ) )
return;
if ( !self.IsValid() )
return;
if ( !other.IsValid() )
return;
if ( !self.IsTrigger )
return;
self.OnTriggerEnter?.InvokeWithWarning( other );
_gameObject.Components.ExecuteEnabledInSelfAndDescendants( x => x.OnTriggerEnter( self, other ) );
}
private void OnColliderTriggerStop( Collider self, Collider other, GameObject go )
{
if ( self is null )
return;
if ( other is null )
return;
foreach ( var shape in self.Shapes )
{
if ( other.Shapes.Any( x => shape.IsTouching( x, true ) ) )
return;
}
if ( !TouchingPairs.Remove( new TouchingPair( self, other, go ) ) )
return;
if ( !TouchingPairs.Any( x => x.Other == other ) )
{
TouchingColliders.Remove( other );
other.OnComponentDisabled -= RemoveDeactivated;
}
if ( !self.IsValid() )
return;
if ( !other.IsValid() )
return;
if ( !self.IsTrigger )
return;
self.OnTriggerExit?.InvokeWithWarning( other );
_gameObject.Components.ExecuteEnabledInSelfAndDescendants( x => x.OnTriggerExit( self, other ) );
}
private void OnObjectTriggerStart( Collider self, GameObject other )
{
if ( self is null )
return;
if ( other is null )
return;
if ( !TouchingObjects.Add( other ) )
return;
if ( !self.IsValid() )
return;
if ( !other.IsValid() )
return;
if ( !self.IsTrigger )
return;
self.OnObjectTriggerEnter?.InvokeWithWarning( other );
_gameObject.Components.ExecuteEnabledInSelfAndDescendants( x => x.OnTriggerEnter( self, other ) );
}
private void OnObjectTriggerStop( Collider self, GameObject other )
{
if ( self is null )
return;
if ( other is null )
return;
if ( TouchingPairs.Any( x => x.GameObject == other ) )
return;
if ( !TouchingObjects.Remove( other ) )
return;
if ( !self.IsValid() )
return;
if ( !other.IsValid() )
return;
if ( !self.IsTrigger )
return;
self.OnObjectTriggerExit?.InvokeWithWarning( other );
_gameObject.Components.ExecuteEnabledInSelfAndDescendants( x => x.OnTriggerExit( self, other ) );
}
private void RemoveDeactivated()
{
Action actions = default;
foreach ( var pair in TouchingPairs )
{
if ( pair.Other.Active )
continue;
actions += () => OnColliderTriggerStop( pair.Self, pair.Other, pair.GameObject );
actions += () => OnObjectTriggerStop( pair.Self, pair.GameObject );
}
actions?.InvokeWithWarning();
}
public void Dispose()
{
_touchingPairs?.Clear();
_touchingColliders?.Clear();
_touchingObjects?.Clear();
if ( !_body.IsValid() )
return;
_body.OnIntersectionStart -= OnIntersectionStart;
_body.OnIntersectionEnd -= OnIntersectionEnd;
_body.OnIntersectionUpdate -= OnIntersectionUpdate;
_body.OnTriggerBegin -= OnTriggerBegin;
_body.OnTriggerEnd -= OnTriggerEnd;
}
}