player-controller について
Godot 4.3以降でプレイヤー移動コントローラーを作るためのスキルです。GDScriptとC#の例で、見下ろし型、プラットフォーマー、FPS操作に加え、ダッシュや壁ジャンプの実装を扱います。
主な機能
- 012D見下ろし型コントローラー
- 022Dプラットフォーマー用コントローラー
- 033D一人称コントローラー
- 04物理フレームでの移動処理
- 05ダッシュと壁ジャンプのレシピ
活用シーン
- 01反応のよい見下ろし型移動の構築
- 02プラットフォーマーのジャンプと移動の実装
- 033D一人称コントローラーの作成
- 04ダッシュや壁ジャンプの追加
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ jame581/ godotprompter --skill player-controllerインストール後、Claude Code で/player-controllerを入力して読み込まれたか確認します。
同梱ファイルreferences/common-movement-recipes.md
詳細
Godot 4.3+ のプレイヤーコントローラー
すべての例は、非推奨の API を使用せずに Godot 4.3+ を対象としています。まず GDScript、次に C# を示します。
関連スキル: RigidBody、Area、レイキャスト、コリジョンシェイプについては physics-system、TileMaps、パララックス、2D ライティングについては 2d-essentials、CharacterBody3D と 3D 移動の設定については 3d-essentials、移動状態の管理については state-machine、カメラの追従とシェイクについては camera-system、ヒットボックス/ハートボックスの統合については component-system、移動状態に応じたアニメーションについては animation-system、InputMap アクションとコントローラー対応については input-handling、敵の移動と経路探索については 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. よく使う移動レシピ
上記の基本的な移動パターンに加えて、頻繁に使われる次の 2 つのレシピは、それぞれ独立した項目として扱う価値があります。ダッシュ(クールダウンがあり、空中にいる間は 1 回だけ使える、タイマー式の短時間の速度上書き)と、ウォールジャンプ(垂直な壁を滑り、ジャンプボタンが押されたときに GetWallNormal() を使って壁から跳ね返る動作)です。どちらも CharacterBody2D に適用でき、重力や水平方向の移動とともに標準の _physics_process ループに組み込めます。
両方のレシピを実装した GDScript と C# の全コードについては、references/common-movement-recipes.md を参照してください。
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. 実装チェックリスト
- すべての移動ロジックが
_processではなく_physics_process(delta)内にある - 入力アクション名が Project > Project Settings > Input Map で定義された名前と完全に一致している
- 重力の値をハードコードせず、
ProjectSettings(physics/2d/default_gravityまたはphysics/3d/default_gravity)から読み取っている - 毎フレーム、すべての速度変更の後に
move_and_slide()を呼び出している - プラットフォーマーでは、操作の応答性を高めるためにコヨーテタイムとジャンプバッファリングを実装している
- FPS コントローラーでは、
_ready()でマウスをキャプチャし、エスケープキーで解放している - ジャンプボタンを早く離したときに速度を減少させ、可変ジャンプの高さに対応している