Use when implementing player movement — CharacterBody2D/3D patterns, input handling, physics, common movement recipes
Scanned 6/3/2026
Install to Claude Code
npx -y skills add jame581/GodotPrompter --skill player-controller --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Player Controller?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jame581-player-controller)More formats (shields.io, HTML) on the badges page.
---
name: player-controller
description: Use when implementing player movement — CharacterBody2D/3D patterns, input handling, physics, common movement recipes
---
# 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 Type | Use For | Physics Control | Notes |
|-------------------|--------------------------------|-----------------|----------------------------------------------------------|
| `CharacterBody2D/3D` | Player, enemies, NPCs | Manual (full) | You control velocity; `move_and_slide()` handles collisions |
| `RigidBody2D/3D` | Projectiles, props, debris | Engine-driven | Physics engine applies forces; harder to control precisely |
| `RigidBody2D/3D` | Projectiles with bouncing | Engine-driven | Set `linear_velocity` once; let physics resolve bounces |
| `CharacterBody2D` | Platformers, top-down, FPS | Manual (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
```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 (normalized 4-directional vector)
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#
```csharp
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 (normalized 4-directional vector)
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
```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#
```csharp
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
```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#
```csharp
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) 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](references/common-movement-recipes.md) for full GDScript and C# implementations of both recipes.
---
## 6. Common Pitfalls
| Symptom | Cause | Fix |
|-------------------------------|----------------------------------------------|------------------------------------------------------------------|
| Player sticks to walls | Default wall blocking behavior | Set `floor_block_on_wall = false` on the CharacterBody |
| Jittery or frame-rate-dependent movement | Movement in `_process` | Move all physics/velocity code to `_physics_process(delta)` |
| Inconsistent jump height | Fixed velocity ignores frame timing | Use variable jump (cut `velocity.y` on button release) |
| Player slides down slopes | No snap or angle limits | Set `floor_snap_length` > 0 and tune `floor_max_angle` |
| Mouse look inverted | Wrong sign on rotation delta | Negate `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
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!