using Sandbox.Movement;
namespace Sandbox;
[Icon( "directions_walk" )]
[EditorHandle( Icon = "directions_walk" )]
[Title( "Player Controller" )]
[Category( "Physics" )]
[Alias( "PhysicsCharacter", "Sandbox.PhysicsCharacter", "Sandbox.BodyController" )]
[HelpUrl( "https://sbox.game/dev/doc/scene/components/reference/player-controller/" )]
public sealed partial class PlayerController : Component, IScenePhysicsEvents, Component.ExecuteInEditor
{
///
/// This is used to keep a distance away from surfaces. For example, when grounding, we'll
/// be a skin distance away from the ground.
///
const float _skin = 0.095f;
[Property, Hide, RequireComponent] public Rigidbody Body { get; set; }
public CapsuleCollider BodyCollider { get; private set; }
public BoxCollider FeetCollider { get; private set; }
[Property, Hide]
public GameObject ColliderObject { get; private set; }
bool _showRigidBodyComponent;
[Property, Group( "Body" ), Range( 1, 64 )] public float BodyRadius { get; set; } = 16.0f;
[Property, Group( "Body" ), Range( 1, 128 )] public float BodyHeight { get; set; } = 72.0f;
[Property, Group( "Body" ), Range( 1, 1000 )] public float BodyMass { get; set; } = 500;
[Property, Group( "Body" )] public TagSet BodyCollisionTags { get; set; }
///
/// We will apply extra friction when we're on the ground and our desired velocity is
/// lower than our current velocity, so we will slow down.
///
[Property, Group( "Physics" ), Range( 0, 1 )] public float BrakePower { get; set; } = 1;
///
/// How much friction to add when we're in the air. This will slow you down unless you have a wish
/// velocity.
///
[Property, Group( "Physics" ), Range( 0, 1 )] public float AirFriction { get; set; } = 0.1f;
[Property, Group( "Components" ), Title( "Show Rigidbody" )]
public bool ShowRigidbodyComponent
{
get => _showRigidBodyComponent;
set
{
_showRigidBodyComponent = value;
if ( Body.IsValid() )
{
Body.Flags = Body.Flags.WithFlag( ComponentFlags.Hidden, !value );
}
}
}
bool _showColliderComponent;
[Property, Group( "Components" ), Title( "Show Colliders" )]
public bool ShowColliderComponents
{
get => _showColliderComponent;
set
{
_showColliderComponent = value;
if ( ColliderObject.IsValid() )
{
ColliderObject.Flags = ColliderObject.Flags.WithFlag( GameObjectFlags.Hidden, !value );
}
if ( BodyCollider.IsValid() )
{
BodyCollider.Flags = BodyCollider.Flags.WithFlag( ComponentFlags.Hidden, !value );
}
if ( FeetCollider.IsValid() )
{
FeetCollider.Flags = FeetCollider.Flags.WithFlag( ComponentFlags.Hidden, !value );
}
}
}
[Sync]
public Vector3 WishVelocity { get; set; }
public bool IsOnGround => GroundObject.IsValid();
///
/// Not touching the ground, and not swimming or climbing
///
public bool IsAirborne => !IsOnGround && !IsSwimming && !IsClimbing;
///
/// Our actual physical velocity minus our ground velocity
///
public Vector3 Velocity { get; private set; }
///
/// The velocity that the ground underneath us is moving
///
public Vector3 GroundVelocity { get; set; }
///
/// Set to true when entering a climbing .
///
public bool IsClimbing { get; set; }
///
/// Set to true when entering a swimming .
///
public bool IsSwimming { get; set; }
protected override void OnAwake()
{
base.OnAwake();
// Some scenes/prefabs may have saved the old colliders before
// we moved them to their own GameObject. If that's the case then
// we should destroy them right now to avoid any trouble.
{
var bc = GetComponent();
var cc = GetComponent();
if ( bc.IsValid() && cc.IsValid() && !bc.IsTrigger && !cc.IsTrigger )
{
bc.Destroy();
cc.Destroy();
}
}
Mode = GetOrAddComponent();
EnsureComponentsCreated();
UpdateBody();
Body.Velocity = 0;
}
protected override void OnEnabled()
{
base.OnEnabled();
if ( !Scene.IsEditor )
{
EyeAngles = WorldRotation.Angles() with { pitch = 0, roll = 0 };
WorldRotation = Rotation.Identity;
if ( Renderer is not null ) Renderer.WorldRotation = new Angles( 0, EyeAngles.yaw, 0 );
}
}
protected override void OnDestroy()
{
base.OnDestroy();
ColliderObject?.Destroy();
ColliderObject = default;
Body?.Destroy();
Body = default;
}
protected override void OnDisabled()
{
base.OnDisabled();
DisableAnimationEvents();
StopPressing();
}
protected override void OnValidate()
{
EnsureComponentsCreated();
UpdateBody();
}
void IScenePhysicsEvents.PrePhysicsStep()
{
UpdateBody();
if ( IsProxy )
return;
Mode.AddVelocity();
Mode.PrePhysicsStep();
}
void IScenePhysicsEvents.PostPhysicsStep()
{
Velocity = Body.Velocity - GroundVelocity;
UpdateGroundVelocity();
RestoreStep();
Mode?.PostPhysicsStep();
CategorizeGround();
ChooseBestMoveMode();
}
Transform _groundTransform;
void UpdateGroundVelocity()
{
if ( GroundObject is null )
{
GroundVelocity = 0;
return;
}
if ( GroundComponent is Collider collider )
{
GroundVelocity = collider.GetVelocityAtPoint( WorldPosition );
}
if ( GroundComponent is Rigidbody rigidbody )
{
var mass1 = BodyMass;
var mass2 = rigidbody.Mass;
var massFactor = mass2 / (mass1 + mass2);
GroundVelocity = rigidbody.GetVelocityAtPoint( WorldPosition ) * massFactor;
}
}
///
/// Adds velocity in a special way. First we subtract any opposite velocity (ie, falling) then
/// we add the velocity, but we clamp it to that direction. This means that if you jump when you're running
/// up a platform, you don't get extra jump power.
///
public void Jump( Vector3 velocity )
{
PreventGrounding( 0.2f );
var currentVel = Body.Velocity;
// moving in the opposite direction
// because this is a jump, we want to counteract that
var dot = currentVel.Dot( velocity );
if ( dot < 0 )
{
currentVel = currentVel.SubtractDirection( velocity.Normal, 1 );
}
currentVel = currentVel.AddClamped( velocity, velocity.Length );
Body.Velocity = currentVel;
}
void UpdateEyeTransform()
{
EyeTransform = Mode?.CalculateEyeTransform() ?? WorldTransform;
}
}