A scene component that implements a push volume trigger. It converts colliders to triggers, computes a force from configured direction and magnitude, and applies impulses to Rigidbodies or velocity to CharacterController objects either continuously on FixedUpdate or once on trigger enter.
using Sandbox;
using System.Collections.Generic;
namespace LegacyEntityPack;
/// <summary>
/// A volume that pushes things touching it. Modern scene-system port of the legacy
/// <c>push_volume</c> entity.
///
/// Put this on a GameObject that has a Collider (e.g. a BoxCollider) - the collider is
/// turned into a trigger automatically. Pushes <see cref="Rigidbody"/> props and
/// <see cref="CharacterController"/> players; the listener callback and trigger collider
/// must live on the same GameObject.
/// </summary>
[Title( "Push Volume" )]
[Category( "Triggers" )]
[Icon( "deblur" )]
public class PushVolume : Component, Component.ITriggerListener
{
/// <summary>How strong the push is, in units per second.</summary>
[Property] public float Force { get; set; } = 500f;
/// <summary>Direction of the force.</summary>
[Property] public Angles ForceDirection { get; set; }
/// <summary>If set, applies one second of force once when something enters, instead of pushing continuously.</summary>
[Property] public bool OnlyPushOnEnter { get; set; } = false;
/// <summary>Objects already pushed this tick, so a multi-collider object isn't pushed twice.</summary>
protected readonly HashSet<GameObject> PushedThisTick = new();
/// <summary>The force vector for the given timeslice. Use this in <see cref="Push"/> overrides.</summary>
protected Vector3 GetForce( float time ) => Rotation.From( ForceDirection ).Forward * Force * time;
protected override void OnAwake()
{
// The legacy push_volume was a Solid trigger, so make our colliders triggers.
foreach ( var collider in Components.GetAll<Collider>( FindMode.EverythingInSelfAndDescendants ) )
collider.IsTrigger = true;
}
protected override void OnFixedUpdate()
{
if ( OnlyPushOnEnter )
return;
var trigger = Components.Get<Collider>( FindMode.EverythingInSelfAndDescendants );
if ( trigger is null )
return;
PushedThisTick.Clear();
foreach ( var other in trigger.Touching )
{
if ( other.IsValid() )
Push( other, Time.Delta );
}
}
public void OnTriggerEnter( Collider other )
{
if ( !OnlyPushOnEnter || !other.IsValid() )
return;
PushedThisTick.Clear();
Push( other, 1f );
}
/// <summary>
/// Push a single touching collider. Override to support custom controllers, calling
/// <c>base.Push</c> to fall back to the default Rigidbody / CharacterController handling.
/// </summary>
protected virtual void Push( Collider other, float time )
{
var force = GetForce( time );
var rb = other.Rigidbody;
if ( rb.IsValid() )
{
if ( !PushedThisTick.Add( rb.GameObject ) )
return;
if ( Networking.IsActive && rb.Network.IsProxy )
return;
// Multiply by mass so everything accelerates equally, matching the original.
rb.ApplyImpulse( force * rb.Mass );
return;
}
var controller = other.Components.Get<CharacterController>( FindMode.EverythingInSelfAndAncestors );
if ( controller.IsValid() )
{
if ( !PushedThisTick.Add( controller.GameObject ) )
return;
if ( Networking.IsActive && controller.Network.IsProxy )
return;
if ( force.z > 1f )
controller.GroundObject = null;
controller.Velocity += force;
}
}
}