Movement & Character Controller

Altruist provides a built-in Kinematic Character Controller (KinematicCharacterController3D) for server-authoritative player movement. It handles camera-relative motion, gravity, ground detection, capsule sweep collision, depenetration, slope limits, pluggable movement profiles, and an extensible ability system.

The controller works with and without physics. Altruist auto-selects the right backend:

ConfigBackendHow Collisions Are Resolved
physics.enabled: trueBEPU physics engineEngine-level capsule casts
physics.enabled: falseHeightmap + collider mathDistance checks against entity ColliderDescriptors

Your controller code is the same in both cases. The backend is swapped via ISpatialQueryProvider, which the framework injects automatically based on your config.

How It Works

The controller follows an input-frame model:

  1. Network or gameplay input arrives from the client or server systems.
  2. The host translates that into an ICharacterMovementInput3D such as CharacterRealtimeWasdInput3D or CharacterClickToMoveInput3D.
  3. The input frame is stored on the controller with SetMovementInput(...).
  4. On the next engine tick, Step() evaluates the active MovementProfile and computes movement via capsule sweeps.
  5. The resulting position and rotation are applied to the entity.
Loading diagram...

Note:

The controller is server-authoritative. The client sends movement intent, but the server computes the actual position. This prevents speed hacks and teleport exploits.

IKinematicCharacterController3D Interface

public interface IKinematicCharacterController3D
{
    void SetBody(IPhysxBody3D body);
    void SetMovementInput(ICharacterMovementInput3D input);
    void AddLocalMotionDelta(Vector3 localDelta);
    void Step(float dt, IGameWorldManager3D world);

    Vector3 Position { get; }
    Quaternion Rotation { get; }
    float Yaw { get; }
    bool IsGrounded { get; }
    Vector3 Velocity { get; }
    CharacterMovementFrame3D LastMovementFrame { get; }
    bool HasActiveMovementInput { get; }
}

Setting Up a Character Controller

public async Task SetupCharacter(CharacterPrefab prefab, IGameWorldManager3D world)
{
    // 1. Create body profile (capsule shape for humanoid)
    var profile = new HumanoidCapsuleBodyProfile(
        radius: 0.28f,
        halfLength: 0.62f,
        mass: 0f,
        isKinematic: true
    );

    // 2. Set physics descriptors on the prefab
    prefab.BodyDescriptor = profile.CreateBody(prefab.Transform);
    prefab.ColliderDescriptors = profile.CreateColliders(prefab.Transform);

    // 3. Spawn into world (creates the runtime physics body)
    var body = await world.SpawnDynamicObject(prefab);

    // 4. Create and configure the controller
    var controller = new KinematicCharacterController3D
    {
        Radius = 0.28f,
        Height = 1.8f,
        PivotToCenterY = 0.9f,
        MaxSlopeAngleDeg = 60f,
        MovementProfile = TpsMovementProfile3D.Default,
        MovementStats = new CharacterMovementStats3D(
            WalkSpeed: 5f,
            SprintSpeed: 8f,
            MoveSpeedMultiplier: 1f,
            RotationSpeedDegPerSec: 720f),
    };

    // 5. Add abilities
    controller.AddAbility(new SimpleJumpAbility3D { JumpSpeed = 7.5f });

    // 6. Attach the runtime physics body
    controller.SetBody(body);
}

Note:

The Radius and Height on the controller must match the body profile dimensions. These values are used for capsule sweeps and ground probing. Mismatched values cause incorrect collision and grounding.

Movement Profiles & Input Models

MovementProfile is now a first-class configuration point. The profile decides how a raw input frame becomes a movement frame: speed selection, facing rules, jump and sprint interpretation, gravity defaults, and click-to-move behavior.

Built-in profiles:

TypeBest ForBehavior
MmoMovementProfile3Dtop-down MMO or gravity-free movementdisables gravity and ground snap, supports realtime and click-to-move input
TpsMovementProfile3Dthird-person action gamesenables gravity and ground snap, faces move direction by default
FpsMovementProfile3Dfirst-person and strafe shootersenables gravity and ground snap, keeps facing independent from move direction

The controller also exposes typed input records:

public readonly record struct CharacterRealtimeWasdInput3D(
    float MoveX,
    float MoveZ,
    Vector3 LookDirection,
    float CameraYaw,
    float ExternalFacingYaw,
    bool UseExternalFacing,
    bool Sprint,
    bool Jump) : ICharacterMovementInput3D;

public readonly record struct CharacterClickToMoveInput3D(
    Vector3 TargetWorldPosition,
    float ArrivalDistance,
    bool Sprint = false) : ICharacterMovementInput3D;

public readonly record struct CharacterMovementStats3D(
    float WalkSpeed,
    float SprintSpeed,
    float MoveSpeedMultiplier,
    float RotationSpeedDegPerSec);

Example profile setup:

controller.MovementProfile = TpsMovementProfile3D.Default;
controller.MovementStats = new CharacterMovementStats3D(
    WalkSpeed: 5f,
    SprintSpeed: 8f,
    MoveSpeedMultiplier: 1f,
    RotationSpeedDegPerSec: 720f);

Example click-to-move command:

controller.SetMovementInput(new CharacterClickToMoveInput3D(
    TargetWorldPosition: clickPoint,
    ArrivalDistance: 0.15f,
    Sprint: false));

Configuration Reference

Profile & Stats

PropertyDefaultDescription
MovementProfileTpsMovementProfile3D.DefaultHigh-level movement mode. Choose MMO, TPS, FPS, or your own IMovementProfile3D
MovementStats.WalkSpeed5Base walking speed before multipliers
MovementStats.SprintSpeed5Base sprint speed before multipliers
MovementStats.MoveSpeedMultiplier1Per-character scalar applied to walk and sprint speed
MovementStats.RotationSpeedDegPerSec0Turn smoothing from the profile output (0 = instant)

Speed & Acceleration

PropertyDefaultDescription
Acceleration1000How fast the character reaches target speed (m/s^2). High default = instant
Deceleration1000How fast the character stops (m/s^2). High default = instant

Note:

The default Acceleration and Deceleration of 1000 make movement feel instant. Lower these for more realistic inertia, for example Acceleration = 20 and Deceleration = 15 for a heavier character.

Gravity & Vertical

PropertyDefaultDescription
Gravity25Downward acceleration (m/s^2)
MaxFallSpeed50Maximum fall velocity clamp

Air Control

PropertyDefaultDescription
AirAccelerationMultiplier1Multiplier on acceleration while airborne. Set to 0.3 for limited air control
AirMaxSpeedMultiplier1Multiplier on max speed while airborne

Ground Detection

PropertyDefaultDescription
GroundProbeDistance0.12How far below the capsule to probe for ground
SkinWidth0.03Small gap maintained between capsule and surfaces
MaxSlopeAngleDeg60Maximum walkable slope angle in degrees
GroundSnapSpeed2Small downward velocity applied when grounded
GroundMaskPhysxLayer.WorldWhich physics layers count as ground

Note:

MaxSlopeAngleDeg controls which surfaces are walkable. A slope steeper than this angle is treated as a wall. Capsule sweep probing remains the main grounding path, and the controller also performs an extra downward terrain or raycast fallback when no usable body hit is returned. This helps keep grounding stable on heightfields and sparse ground geometry.

Collision

PropertyDefaultDescription
UseCapsuleSweepstrueUse capsule sweep for collision. Disable for ray-only checks
MaxSlideIterations3Maximum collision slide iterations per tick
CollisionMaskWorld | DynamicWhich physics layers to collide with during movement

Depenetration

PropertyDefaultDescription
UseDepenetrationtruePush the character out of overlapping geometry
DepenetrationIterations3Maximum push-out attempts per tick
DepenetrationPushDistance0.03Distance to push out per iteration

Note:

Depenetration handles edge cases like spawning inside geometry or being pushed into a wall by a moving platform. Keep it enabled unless you have a specific reason to disable it.

Character Shape

PropertyDefaultDescription
Radius0.28Capsule radius (must match body profile)
Height1.8Capsule total height (must match body profile)
PivotToCenterY0Vertical offset from your gameplay pivot to the physics capsule center

Note:

Use PivotToCenterY when your gameplay transform is stored at foot or pivot level instead of capsule-center level. When this offset is configured, Position is reported back in pivot-space, while SetBody(...) and controller internals convert to and from the physics body's center position automatically.

Feeding Input from Network

When receiving movement packets from the client, translate them into a typed movement input frame:

[Gate("move")]
public void OnMove(MovePacket packet, string clientId)
{
    var prefab = GetPlayerPrefab(clientId);

    prefab.Controller.SetMovementInput(new CharacterRealtimeWasdInput3D(
        MoveX: packet.InputX,
        MoveZ: packet.InputZ,
        LookDirection: new Vector3(packet.LookX, packet.LookY, packet.LookZ),
        CameraYaw: packet.CameraYaw,
        ExternalFacingYaw: packet.ExternalFacingYaw,
        UseExternalFacing: packet.UseExternalFacing,
        Sprint: packet.Sprint,
        Jump: packet.Jump));
}
Input TypeParametersDescription
CharacterRealtimeWasdInput3Dmove axes, look direction, camera yaw, sprint/jump flagsStandard realtime input for action movement
CharacterClickToMoveInput3Dtarget world position, arrival distance, sprint flagClick-to-move or AI-directed locomotion

Note:

The controller consumes one active input frame per tick. If multiple movement packets arrive between ticks, the last applied input wins. This is intentional. The server runs at a fixed tick rate, not at network packet rate.

Integrating with a Prefab

The controller does not run on its own. You call Controller.Step() inside your prefab's Step() override:

[Prefab("character")]
[WorldObject("character")]
public class CharacterPrefab : WorldObjectPrefab3D
{
    [PrefabComponentRoot]
    public CharacterVault Character { get; set; } = default!;

    public IKinematicCharacterController3D Controller { get; set; } = default!;

    public override void Step(float dt, IGameWorldManager3D world)
    {
        if (Controller is null) return;

        Controller.Step(dt, world);

        Transform = Transform
            .WithPosition(Position3D.From(Controller.Position))
            .WithRotation(Rotation3D.FromQuaternion(Controller.Rotation));

        Character.X = Transform.Position.X;
        Character.Y = Transform.Position.Y;
        Character.Z = Transform.Position.Z;
    }
}

Note:

The controller modifies the runtime body directly, then exposes the final gameplay-facing transform through Position and Rotation. If PivotToCenterY is configured, Controller.Position already returns the corrected pivot-space position for your world object and vault.

The Step Cycle

Each tick, the controller performs these steps in order:

  1. Depenetrate - push out of overlapping geometry from the previous frame.
  2. Ground probe - probe for walkable ground and slope angle.
  3. Apply authored motion - consume any extra motion added through AddLocalMotionDelta(...).
  4. Evaluate movement profile - convert the active input frame into a movement frame.
  5. Rotation - resolve yaw from facing rules, camera, or external facing.
  6. Run abilities - each ICharacterAbility3D can modify the CharacterMotorContext.
  7. Gravity and ground snap - apply gravity if airborne, snap when grounded.
  8. Sweep and slide - resolve movement against world collision.
  9. Post-move depenetration - clean up numerical edge cases.
  10. Store state - update IsGrounded, Velocity, LastMovementFrame, and outputs.

Abilities

Abilities are modular hooks that run inside the movement tick. They receive a mutable CharacterMotorContext and can modify velocity, grounding state, and interpreted input.

CharacterMotorContext

public struct CharacterMotorContext
{
    public IPhysxBody3D Body;
    public IGameWorldManager3D World;

    public Vector3 DesiredMoveWorld;
    public float DesiredSpeed;
    public Vector3 Velocity;

    public bool IsGrounded;
    public Vector3 GroundNormal;

    public bool JumpPressed;
    public bool SprintHeld;
}

Note:

Abilities modify the context by reference. Multiple abilities compose naturally. For example, a jump ability can set vertical velocity while a dash ability overrides horizontal velocity.

Built-in: SimpleJumpAbility3D

controller.AddAbility(new SimpleJumpAbility3D { JumpSpeed = 7.5f });

Detects a rising edge on JumpPressed while grounded, then sets the upward velocity.

Custom Ability: Dash

public class DashAbility : ICharacterAbility3D
{
    public float DashSpeed { get; set; } = 20f;
    public float DashDuration { get; set; } = 0.2f;

    private float _dashTimer;
    private bool _wasDashPressed;

    public void Step(float dt, ref CharacterMotorContext ctx)
    {
        bool dashPressed = ctx.SprintHeld;
        bool pressedThisFrame = dashPressed && !_wasDashPressed;
        _wasDashPressed = dashPressed;

        if (_dashTimer > 0f)
        {
            _dashTimer -= dt;
            if (ctx.DesiredMoveWorld.LengthSquared() > 0.01f)
            {
                var dashDir = Vector3.Normalize(ctx.DesiredMoveWorld);
                ctx.Velocity = new Vector3(
                    dashDir.X * DashSpeed,
                    ctx.Velocity.Y,
                    dashDir.Z * DashSpeed);
            }
            return;
        }

        if (pressedThisFrame && ctx.IsGrounded)
            _dashTimer = DashDuration;
    }
}

controller.AddAbility(new DashAbility { DashSpeed = 25f, DashDuration = 0.15f });

Custom Ability: Double Jump

public class DoubleJumpAbility : ICharacterAbility3D
{
    public float JumpSpeed { get; set; } = 6f;
    public int MaxAirJumps { get; set; } = 1;

    private int _airJumpsUsed;
    private bool _wasPressed;
    private bool _wasGrounded;

    public void Step(float dt, ref CharacterMotorContext ctx)
    {
        if (ctx.IsGrounded && !_wasGrounded)
            _airJumpsUsed = 0;

        _wasGrounded = ctx.IsGrounded;

        bool pressedThisFrame = ctx.JumpPressed && !_wasPressed;
        _wasPressed = ctx.JumpPressed;

        if (!pressedThisFrame) return;

        if (!ctx.IsGrounded && _airJumpsUsed < MaxAirJumps)
        {
            ctx.Velocity = new Vector3(ctx.Velocity.X, JumpSpeed, ctx.Velocity.Z);
            ctx.IsGrounded = false;
            _airJumpsUsed++;
        }
    }
}

controller.AddAbility(new SimpleJumpAbility3D { JumpSpeed = 7.5f });
controller.AddAbility(new DoubleJumpAbility { JumpSpeed = 6f, MaxAirJumps = 1 });

Note:

Abilities run in the order they are added. Put the ground jump ability first so it handles the grounded case, then let the double jump handle the airborne case.

State Outputs

After Step(), the controller exposes the resulting state for replication:

Vector3 pos = controller.Position;
Quaternion rot = controller.Rotation;
float yaw = controller.Yaw;
bool grounded = controller.IsGrounded;
Vector3 vel = controller.Velocity;
CharacterMovementFrame3D frame = controller.LastMovementFrame;

Use these to build position sync packets:

var syncPacket = new PositionSyncPacket
{
    EntityId = prefab.EntityId,
    X = controller.Position.X,
    Y = controller.Position.Y,
    Z = controller.Position.Z,
    Yaw = controller.Yaw,
    Speed = controller.Velocity.Length(),
    Grounded = controller.IsGrounded,
};

Body Profiles

A body profile encapsulates the physics shape for an entity. The controller requires a kinematic capsule body:

var profile = new HumanoidCapsuleBodyProfile(
    radius: 0.28f,
    halfLength: 0.62f,
    mass: 0f,
    isKinematic: true
);

prefab.BodyDescriptor = profile.CreateBody(prefab.Transform);
prefab.ColliderDescriptors = profile.CreateColliders(prefab.Transform);

Note:

Set isKinematic: true for character controllers. Kinematic bodies are moved directly by the controller rather than by physics forces, which gives precise control over movement.

Custom Body Profile

For non-humanoid characters such as vehicles, mounts, or large creatures, create a custom profile:

public class VehicleBodyProfile : IBodyProfile3D
{
    public float Width { get; }
    public float Height { get; }
    public float Length { get; }
    public float Mass { get; }

    public VehicleBodyProfile(float width, float height, float length, float mass)
    {
        Width = width;
        Height = height;
        Length = length;
        Mass = mass;
    }

    public PhysxBody3DDesc CreateBody(Transform3D transform)
    {
        var sized = transform.WithSize(Size3D.Of(Width / 2, Height / 2, Length / 2));
        return PhysxBody3D.Create(PhysxBodyType.Dynamic, Mass, sized);
    }

    public IEnumerable<PhysxCollider3DDesc> CreateColliders(Transform3D transform)
    {
        var sized = transform.WithSize(Size3D.Of(Width / 2, Height / 2, Length / 2));
        yield return PhysxCollider3D.Create(PhysxColliderShape3D.Box3D, sized, isTrigger: false);
    }
}