mcpField
P
player-controller

player-controller

github.com/jame581开发889171 次安装5 天前更新
代码仓库

关于 player-controller

面向 Godot 4.3+ 的玩家移动控制器开发技能,提供 GDScript 和 C# 示例。内容涵盖俯视角与平台跳跃移动、第一人称控制,以及冲刺和蹬墙跳实现方案。

主要功能

  1. 012D 俯视角控制器
  2. 022D 平台跳跃控制器
  3. 033D 第一人称控制器
  4. 04物理帧移动循环
  5. 05冲刺与蹬墙跳方案

适用场景

  1. 01构建响应灵敏的俯视角移动
  2. 02实现平台跳跃与移动
  3. 03创建 3D 第一人称控制器
  4. 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 时释放
  • 通过提前松开按键时降低速度来实现可变跳跃高度

评论

支持 Markdown