MoveController.cs
using Sandbox;
namespace Facepunch.BombRoyale;
[Title( "Move Controller" )]
[Category( "Bomb Royale" )]
[Icon( "directions_walk" )]
public class MoveController : Component
{
[Range( 0f, 200f )] [Property] public float Radius { get; set; } = 16f;
[Range( 0f, 200f )] [Property] public float Height { get; set; } = 64f;
[Range( 0f, 50f )] [Property] public float StepHeight { get; set; } = 18f;
[Range( 0f, 90f )] [Property] public float GroundAngle { get; set; } = 45f;
[Range( 0f, 64f )] [Property] public float Acceleration { get; set; } = 10f;
/// <summary>
/// When jumping into walls, should we bounce off or just stop dead?
/// </summary>
[Range( 0, 1 )] [Property] public float Bounciness { get; set; } = 0.3f;
[Property] public TagSet IgnoreLayers { get; set; } = new();
[Property] public bool EnableFixUnstuck { get; set; } = true;
public BBox BoundingBox => new( new( -Radius, -Radius, 0 ), new Vector3( Radius, Radius, Height ) );
[Sync] public Vector3 Velocity { get; set; }
[Sync] public bool IsOnGround { get; set; }
public GameObject GroundObject { get; set; }
public Collider GroundCollider { get; set; }
private int StuckTries;
protected override void DrawGizmos()
{
Gizmo.Draw.LineBBox( BoundingBox );
}
/// <summary>
/// Add acceleration to the current velocity. No need to scale by time delta - it will be done inside.
/// </summary>
public void Accelerate( Vector3 vector )
{
Velocity = Velocity.WithAcceleration( vector, Acceleration * Time.Delta );
}
/// <summary>
/// Apply an amount of friction to the current velocity. No need to scale by time delta - it will be done inside.
/// </summary>
public void ApplyFriction( float frictionAmount, float stopSpeed = 140f )
{
var speed = Velocity.Length;
if ( speed < 0.01f ) return;
// Bleed off some speed, but if we have less than the bleed threshold, bleed the threshold amount.
var control = (speed < stopSpeed) ? stopSpeed : speed;
// Add the amount to the drop amount.
var drop = control * Time.Delta * frictionAmount;
float newSpeed = speed - drop;
if ( newSpeed < 0 ) newSpeed = 0;
if ( newSpeed == speed ) return;
newSpeed /= speed;
Velocity *= newSpeed;
}
SceneTrace BuildTrace( Vector3 from, Vector3 to ) => BuildTrace( Scene.Trace.Ray( from, to ) );
SceneTrace BuildTrace( SceneTrace source ) => source.Size( BoundingBox )
.WithoutTags( IgnoreLayers )
.IgnoreGameObjectHierarchy( GameObject );
/// <summary>
/// Trace the controller's current position to the specified delta.
/// </summary>
public SceneTraceResult TraceDirection( Vector3 direction )
{
return BuildTrace( GameObject.WorldPosition, GameObject.WorldPosition + direction ).Run();
}
private void Move( bool step )
{
if ( step && IsOnGround )
{
Velocity = Velocity.WithZ( 0f );
}
if ( Velocity.Length < 0.001f )
{
Velocity = Vector3.Zero;
return;
}
var pos = GameObject.WorldPosition;
var mover = new CharacterControllerHelper( BuildTrace( pos, pos ), pos, Velocity )
{
Bounce = Bounciness,
MaxStandableAngle = GroundAngle
};
if ( step && IsOnGround )
{
mover.TryMoveWithStep( Time.Delta, StepHeight );
}
else
{
mover.TryMove( Time.Delta );
}
WorldPosition = mover.Position;
Velocity = mover.Velocity;
}
private void CategorizePosition()
{
var Position = WorldPosition;
var point = Position + Vector3.Down * 2f;
var vBumpOrigin = Position;
var wasOnGround = IsOnGround;
// We're flying upwards too fast, never land on ground.
if ( !IsOnGround && Velocity.z > 40f )
{
ClearGround();
return;
}
//
// Trace down one step height if we're already on the ground "step down". If not, search for floor right below us
// because if we do StepHeight we'll snap that many units to the ground.
//
point.z -= wasOnGround ? StepHeight : 0.1f;
var pm = BuildTrace( vBumpOrigin, point ).Run();
if ( !pm.Hit || Vector3.GetAngle( Vector3.Up, pm.Normal ) > GroundAngle )
{
ClearGround();
return;
}
IsOnGround = true;
GroundObject = pm.GameObject;
GroundCollider = pm.Shape?.Collider as Collider;
if ( wasOnGround && pm is { StartedSolid: false, Fraction: > 0f and < 1f } )
{
WorldPosition = pm.EndPosition + pm.Normal * 0.01f;
}
}
/// <summary>
/// Disconnect from ground and punch our velocity. This is useful if you want the player to jump or something.
/// </summary>
public void Punch( in Vector3 amount )
{
ClearGround();
Velocity += amount;
}
private void ClearGround()
{
IsOnGround = false;
GroundObject = default;
GroundCollider = default;
}
/// <summary>
/// Move a character with this velocity.
/// </summary>
public void Move()
{
if ( EnableFixUnstuck && TryUnstuck() )
return;
Move( IsOnGround );
CategorizePosition();
}
/// <summary>
/// Move from our current position to this target position, but using tracing an sliding.
/// This is good for different control modes like ladders and stuff.
/// </summary>
public void MoveTo( Vector3 targetPosition, bool useStep )
{
if ( EnableFixUnstuck && TryUnstuck() )
return;
var pos = WorldPosition;
var delta = targetPosition - pos;
var mover = new CharacterControllerHelper( BuildTrace( pos, pos ), pos, delta );
mover.MaxStandableAngle = GroundAngle;
if ( useStep )
mover.TryMoveWithStep( 1f, StepHeight );
else
mover.TryMove( 1f );
WorldPosition = mover.Position;
}
private bool TryUnstuck()
{
var result = BuildTrace( WorldPosition, WorldPosition ).Run();
if ( !result.StartedSolid )
{
StuckTries = 0;
return false;
}
var attemptsPerTick = 20;
for ( int i = 0; i < attemptsPerTick; i++ )
{
var pos = WorldPosition + Vector3.Random.Normal * (((float)StuckTries) / 2f);
if ( i == 0 )
pos = WorldPosition + Vector3.Up * 2f;
result = BuildTrace( pos, pos ).Run();
if ( result.StartedSolid )
continue;
WorldPosition = pos;
return false;
}
StuckTries++;
return true;
}
}