关于 player-controller
面向 Godot 4.3+ 的玩家移动控制器开发技能,提供 GDScript 和 C# 示例。内容涵盖俯视角与平台跳跃移动、第一人称控制,以及冲刺和蹬墙跳实现方案。
主要功能
- 012D 俯视角控制器
- 022D 平台跳跃控制器
- 033D 第一人称控制器
- 04物理帧移动循环
- 05冲刺与蹬墙跳方案
适用场景
- 01构建响应灵敏的俯视角移动
- 02实现平台跳跃与移动
- 03创建 3D 第一人称控制器
- 04添加冲刺或蹬墙跳行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill player-controller安装后在 Claude Code 中输入/player-controller确认已加载。
附带文件references/common-movement-recipes.md
详细介绍
Godot 4.3+ 中的玩家控制器
所有示例均适用于 Godot 4.3+,不使用已弃用的 API。先展示 GDScript,再展示 C#。
相关技能: physics-system 用于 RigidBody、Area、射线检测和碰撞形状,2d-essentials 用于 TileMaps、视差和 2D 光照,3d-essentials 用于 CharacterBody3D 和 3D 移动设置,state-machine 用于移动状态管理,camera-system 用于相机跟随和震动,component-system 用于命中框/受击框集成,animation-system 用于由移动状态驱动动画,input-handling 用于 InputMap 操作和控制器支持,ai-navigation 用于敌人移动和寻路。
1. 核心概念
CharacterBody 与 RigidBody
| 刚体类型 | 用途 | 物理控制 | 说明 |
|---|---|---|---|
CharacterBody2D/3D | 玩家、敌人、NPC | 手动(完全控制) | 由你控制速度;move_and_slide() 负责处理碰撞 |
RigidBody2D/3D | 投射物、道具、碎片 | 引擎驱动 | 物理引擎施加力;难以精确控制 |
RigidBody2D/3D | 会弹跳的投射物 | 引擎驱动 | 设置一次 linear_velocity;让物理系统处理弹跳 |
CharacterBody2D/3D | 平台游戏、俯视角游戏、FPS | 手动(完全控制) | 可靠且可预测;最适合实现灵敏的游戏手感 |
经验法则: 需要紧密、灵敏的控制时,使用 CharacterBody。需要逼真的物理模拟时,使用 RigidBody。
移动循环
每个物理帧都遵循以下顺序:
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
始终将此循环放在 _physics_process(delta) 中,绝不要放在 _process(delta) 中。
2. 2D 俯视角控制器
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 平台游戏控制器
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 第一人称控制器
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. 常见移动方案
除了上述基础移动模式,还有两种常见方案值得单独介绍:冲刺(基于计时器,在短时间内覆盖速度,实现爆发式移动,并带有冷却时间,且每次腾空只能冲刺一次)和蹬墙跳(垂直沿墙滑落,并在按下跳跃键时通过 GetWallNormal() 反弹)。两者都适用于 CharacterBody2D,并可与重力和水平移动一起放入标准的 _physics_process 循环中。
请参阅 references/common-movement-recipes.md,了解这两种方案的完整 GDScript 和 C# 实现。
6. 常见问题
| 现象 | 原因 | 解决方法 |
|---|---|---|
| 玩家能沿陡峭墙壁向上移动 | floor_block_on_wall 已关闭 | 保持为 true(默认值):阻止角色沿墙面向上移动,同时允许角色沿墙面下滑 |
| 移动抖动或依赖帧率 | 在 _process 中处理移动 | 将所有物理/速度相关代码移至 _physics_process(delta) |
| 无法改变跳跃高度 | 跳跃时设置了固定速度 | 使用可变跳跃(松开按键时削减 velocity.y) |
| 玩家沿斜坡下滑 | floor_stop_on_slope 已关闭,或斜坡比 floor_max_angle 更陡 | 保持 floor_stop_on_slope = true(默认值),调整 floor_max_angle;floor_snap_length 可让角色在下坡时贴合斜坡 |
| 鼠标视角方向反转 | 旋转增量的符号错误 | 对偏航使用 event.relative.x 的相反数,和/或对俯仰使用 event.relative.y 的相反数 |
7. 实现检查清单
- 所有移动逻辑都在
_physics_process(delta)中,而不是_process - 输入操作名称与 Project > Project Settings > Input Map 中定义的名称完全一致
- 重力值从
ProjectSettings(physics/2d/default_gravity或physics/3d/default_gravity)读取,而不是硬编码 - 每帧完成所有速度修改后调用
move_and_slide() - 平台游戏实现土狼时间和跳跃缓冲,以获得灵敏的操控手感
- FPS 控制器在
_ready()中捕获鼠标,并在按下 Escape 时释放 - 通过提前松开按键时降低速度来实现可变跳跃高度