agentleFS
jame581jame581/GodotPrompter/skills/player-controller/SKILL.md

player-controller

Skill870 starsChanged 21 days agoMITView on GitHub

Use when implementing player movement — CharacterBody2D/3D patterns, input handling, physics, common movement recipes

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    All examples target Godot 4.3+ with no deprecated APIs.

    line 8
  2. Very specific:

    Rule of thumb: Use CharacterBody when you need tight, responsive control.

    line 25
  3. Distinctive:

    Every physics frame follows this order:

    line 29
  4. Very specific:

    Always put this loop in _physics_process(delta), never _process(delta).

    line 39

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Player Controllers in Godot 4.3+

All examples target Godot 4.3+ with no deprecated APIs. GDScript is shown first, then C#.

Related skills: physics-system for RigidBody, Area, raycasting, and collision shapes, 2d-essentials for TileMaps, parallax, and 2D lighting, 3d-essentials for CharacterBody3D and 3D movement setup, state-machine for movement state management, camera-system for camera follow and shake, component-system for hitbox/hurtbox integration, animation-system for animation driven by movement state, input-handling for InputMap actions and controller support, ai-navigation for enemy movement and pathfinding.


1. Core Concepts

CharacterBody vs RigidBody

Body TypeUse ForPhysics ControlNotes
CharacterBody2D/3DPlayer, enemies, NPCsManual (full)You control velocity; move_and_slide() handles collisions
RigidBody2D/3DProjectiles, props, debrisEngine-drivenPhysics engine applies forces; harder to control precisely
RigidBody2D/3DProjectiles with bouncingEngine-drivenSet linear_velocity once; let physics resolve bounces
CharacterBody2D/3DPlatformers, top-down, FPSManual (full)Reliable and predictable; best for responsive game feel

Rule of thumb: Use CharacterBody when you need tight, responsive control. Use RigidBody when you want realistic physics simulation.

The Movement Loop

Every physics frame follows this order:

1. Read input          → get axis/action values
2. Apply forces        → gravity, friction, acceleration
3. Modify velocity     → move_toward, lerp, clamp
4. move_and_slide()    → engine resolves collisions, updates position
5. Post-movement state → check is_on_floor(), is_on_wall(), landing events

Always put this loop in _physics_process(delta), never _process(delta).


2. 2D Top-Down Controller

GDScript

extends CharacterBody2D

@export var speed: float = 200.0
@export var acceleration: float = 1500.0
@export var friction: float = 1200.0

func _physics_process(delta: float) -> void:
    # 1. Read input (analog vector, length capped at 1)
    var input_dir: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")

    # 2 & 3. Apply acceleration or friction to velocity
    if input_dir != Vector2.ZERO:
        velocity = velocity.move_toward(input_dir * speed, acceleration * delta)
    else:
        velocity = velocity.move_toward(Vector2.ZERO, friction * delta)

    # 4. Move and resolve collisions
    move_and_slide()

C#

using Godot;

public partial class TopDownPlayer : CharacterBody2D
{
    [Export] public float Speed { get; set; } = 200.0f;
    [Export] public float Acceleration { get; set; } = 1500.0f;
    [Export] public float Friction { get; set; } = 1200.0f;

    public override void _PhysicsProcess(double delta)
    {
        // 1. Read input (analog vector, length capped at 1)
        Vector2 inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");

        // 2 & 3. Apply acceleration or friction
        if (inputDir != Vector2.Zero)
            Velocity = Velocity.MoveToward(inputDir * Speed, Acceleration * (float)delta);
        else
            Velocity = Velocity.MoveToward(Vector2.Zero, Friction * (float)delta);

        // 4. Move and resolve collisions
        MoveAndSlide();
    }
}

3. 2D Platformer Controller

GDScript

extends CharacterBody2D

@export var speed: float = 200.0
@export var jump_velocity: float = -400.0
@export var acceleration: float = 1200.0
@export var deceleration: float = 900.0

# Coyote time and jump buffer
@export var coyote_time: float = 0.12
@export var jump_buffer_time: float = 0.12

var _gravity: float = ProjectSettings.get_setting("physics/2d/default_gravity")
var _coyote_timer: float = 0.0
var _jump_buffer_timer: float = 0.0
var _was_on_floor: bool = false

func _physics_process(delta: float) -> void:
    # Coyote time: allow jump briefly after walking off a ledge
    if is_on_floor():
        _coyote_timer = coyote_time
        _was_on_floor = true
    else:
        _coyote_timer -= delta

    # Jump buffer: register jump input before landing
    if Input.is_action_just_pressed("ui_accept"):
        _jump_buffer_timer = jump_buffer_time
    else:
        _jump_buffer_timer -= delta

    # Apply gravity when airborne
    if not is_on_floor():
        velocity.y += _gravity * delta

    # Jump: consume coyote time and buffer together
    var can_jump: bool = _coyote_timer > 0.0
    if _jump_buffer_timer > 0.0 and can_jump:
        velocity.y = jump_velocity
        _coyote_timer = 0.0
        _jump_buffer_timer = 0.0

    # Variable jump height: cut velocity when button released early
    if Input.is_action_just_released("ui_accept") and velocity.y < 0.0:
        velocity.y *= 0.5

    # Horizontal movement with deceleration
    var input_x: float = Input.get_axis("ui_left", "ui_right")
    if input_x != 0.0:
        velocity.x = move_toward(velocity.x, input_x * speed, acceleration * delta)
    else:
        velocity.x = move_toward(velocity.x, 0.0, deceleration * delta)

    move_and_slide()

C#

using Godot;

public partial class PlatformerPlayer : CharacterBody2D
{
    [Export] public float Speed { get; set; } = 200.0f;
    [Export] public float JumpVelocity { get; set; } = -400.0f;
    [Export] public float Acceleration { get; set; } = 1200.0f;
    [Export] public float Deceleration { get; set; } = 900.0f;
    [Export] public float CoyoteTime { get; set; } = 0.12f;
    [Export] public float JumpBufferTime { get; set; } = 0.12f;

    private float _gravity = ProjectSettings.GetSetting("physics/2d/default_gravity").AsSingle();
    private float _coyoteTimer;
    private float _jumpBufferTimer;

    public override void _PhysicsProcess(double delta)
    {
        float dt = (float)delta;

        // Coyote time
        if (IsOnFloor())
            _coyoteTimer = CoyoteTime;
        else
            _coyoteTimer -= dt;

        // Jump buffer
        if (Input.IsActionJustPressed("ui_accept"))
            _jumpBufferTimer = JumpBufferTime;
        else
            _jumpBufferTimer -= dt;

        // Gravity
        if (!IsOnFloor())
        {
            Vector2 vel = Velocity;
            vel.Y += _gravity * dt;
            Velocity = vel;
        }

        // Jump
        if (_jumpBufferTimer > 0f && _coyoteTimer > 0f)
        {
            Vector2 vel = Velocity;
            vel.Y = JumpVelocity;
            Velocity = vel;
            _coyoteTimer = 0f;
            _jumpBufferTimer = 0f;
        }

        // Variable jump height
        if (Input.IsActionJustReleased("ui_accept") && Velocity.Y < 0f)
        {
            Vector2 vel = Velocity;
            vel.Y *= 0.5f;
            Velocity = vel;
        }

        // Horizontal movement
        float inputX = Input.GetAxis("ui_left", "ui_right");
        Vector2 velocity = Velocity;
        if (inputX != 0f)
            velocity.X = Mathf.MoveToward(velocity.X, inputX * Speed, Acceleration * dt);
        else
            velocity.X = Mathf.MoveToward(velocity.X, 0f, Deceleration * dt);

        Velocity = velocity;
        MoveAndSlide();
    }
}

4. 3D First-Person Controller

GDScript

extends CharacterBody3D

@export var move_speed: float = 5.0
@export var jump_velocity: float = 5.0
@export var mouse_sensitivity: float = 0.002

@onready var head: Node3D = $Head  # Child Node3D that holds Camera3D

var _gravity: float = ProjectSettings.get_setting("physics/3d/default_gravity")

func _ready() -> void:
    Input.mouse_mode = Input.MOUSE_MODE_CAPTURED

func _unhandled_input(event: InputEvent) -> void:
    if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED:
        # Horizontal look: rotate the body (yaw)
        rotate_y(-event.relative.x * mouse_sensitivity)
        # Vertical look: rotate the head (pitch), clamped to ±90°
        head.rotate_x(-event.relative.y * mouse_sensitivity)
        head.rotation.x = clamp(head.rotation.x, -PI / 2.0, PI / 2.0)

    if event.is_action_pressed("ui_cancel"):
        Input.mouse_mode = Input.MOUSE_MODE_VISIBLE

func _physics_process(delta: float) -> void:
    # Gravity
    if not is_on_floor():
        velocity.y -= _gravity * delta

    # Jump
    if Input.is_action_just_pressed("ui_accept") and is_on_floor():
        velocity.y = jump_velocity

    # Movement relative to the direction the player is facing
    var input_dir: Vector2 = Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
    var direction: Vector3 = (transform.basis * Vector3(input_dir.x, 0, input_dir.y)).normalized()

    if direction != Vector3.ZERO:
        velocity.x = direction.x * move_speed
        velocity.z = direction.z * move_speed
    else:
        velocity.x = move_toward(velocity.x, 0.0, move_speed)
        velocity.z = move_toward(velocity.z, 0.0, move_speed)

    move_and_slide()

C#

using Godot;

public partial class FPSController : CharacterBody3D
{
    [Export] public float MoveSpeed { get; set; } = 5.0f;
    [Export] public float JumpVelocity { get; set; } = 5.0f;
    [Export] public float MouseSensitivity { get; set; } = 0.002f;

    private float _gravity = ProjectSettings.GetSetting("physics/3d/default_gravity").AsSingle();
    private Node3D _head;

    public override void _Ready()
    {
        _head = GetNode<Node3D>("Head");
        Input.MouseMode = Input.MouseModeEnum.Captured;
    }

    public override void _UnhandledInput(InputEvent @event)
    {
        if (@event is InputEventMouseMotion motion
            && Input.MouseMode == Input.MouseModeEnum.Captured)
        {
            // Horizontal look (yaw on body)
            RotateY(-motion.Relative.X * MouseSensitivity);
            // Vertical look (pitch on head), clamped to ±90°
            _head.RotateX(-motion.Relative.Y * MouseSensitivity);
            Vector3 rot = _head.Rotation;
            rot.X = Mathf.Clamp(rot.X, -Mathf.Pi / 2f, Mathf.Pi / 2f);
            _head.Rotation = rot;
        }

        if (@event.IsActionPressed("ui_cancel"))
            Input.MouseMode = Input.MouseModeEnum.Visible;
    }

    public override void _PhysicsProcess(double delta)
    {
        float dt = (float)delta;
        Vector3 vel = Velocity;

        // Gravity
        if (!IsOnFloor())
            vel.Y -= _gravity * dt;

        // Jump
        if (Input.IsActionJustPressed("ui_accept") && IsOnFloor())
            vel.Y = JumpVelocity;

        // Movement relative to facing direction
        Vector2 inputDir = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
        Vector3 direction = (Transform.Basis * new Vector3(inputDir.X, 0, inputDir.Y)).Normalized();

        if (direction != Vector3.Zero)
        {
            vel.X = direction.X * MoveSpeed;
            vel.Z = direction.Z * MoveSpeed;
        }
        else
        {
            vel.X = Mathf.MoveToward(vel.X, 0f, MoveSpeed);
            vel.Z = Mathf.MoveToward(vel.Z, 0f, MoveSpeed);
        }

        Velocity = vel;
        MoveAndSlide();
    }
}

5. Common Movement Recipes

Beyond the basic locomotion patterns above, two recipes come up so often that they deserve their own block: Dash (timer-based velocity override for a short burst, with a cooldown and one dash per airtime) and Wall Jump (vertical wall slide + bounce off GetWallNormal() when jump is pressed). Both apply to a CharacterBody2D and slot into the standard _physics_process loop alongside gravity and horizontal movement.

See references/common-movement-recipes.md for full GDScript and C# implementations of both recipes.


6. Common Pitfalls

SymptomCauseFix
Player climbs steep wallsfloor_block_on_wall turned offKeep it true (default): blocks floor movement up walls, lets the body slide down them
Jittery or frame-rate-dependent movementMovement in _processMove all physics/velocity code to _physics_process(delta)
Jump height can't be variedJump sets one fixed velocityUse variable jump (cut velocity.y on button release)
Player slides down slopesfloor_stop_on_slope off, or slope steeper than floor_max_angleKeep floor_stop_on_slope = true (default), tune floor_max_angle; floor_snap_length keeps the body on slopes going down
Mouse look invertedWrong sign on rotation deltaNegate event.relative.x for yaw and/or event.relative.y for pitch

7. Implementation Checklist

  • All movement logic is inside _physics_process(delta), not _process
  • Input action names match exactly what is defined in Project > Project Settings > Input Map
  • Gravity value is read from ProjectSettings (physics/2d/default_gravity or physics/3d/default_gravity), not hard-coded
  • move_and_slide() is called after all velocity modifications each frame
  • Platformers implement coyote time and jump buffering for responsive feel
  • FPS controllers capture mouse in _ready() and release on escape
  • Variable jump height is handled via early-release velocity reduction

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.