横スクロールアクションを作っていると、だいたい「ダッシュ」「二段ジャンプ」の次くらいに欲しくなるのが壁ジャンプですよね。でも、素直に Player.gd に if 文を追加していくと…
- 「地上」「空中」「壁張り付き」「壁ジャンプ中」みたいな状態が増え続ける
- 最初はシンプルだった
_physics_process()が if/else だらけの沼になる - 敵や別キャラにも同じ挙動を入れたくなったとき、コピペ地獄になる
Godot 4 では CharacterBody2D を継承してプレイヤーを作るのが定番ですが、そこに全部の移動ロジックを詰め込むと、「継承ツリー1本勝負」な巨大クラスが出来上がってしまいます。これだと機能追加のたびに既存コードを壊すリスクも高いですね。
そこで今回は、「壁ジャンプ」だけを独立したコンポーネントとして切り出した WallJump コンポーネントを紹介します。
プレイヤー本体は「移動と重力」だけに集中させて、壁ジャンプの判定・ベクトル計算・入力処理は全部このコンポーネントに任せてしまいましょう。
【Godot 4】壁からビヨーンとスマートに跳ぶ!「WallJump」コンポーネント
この WallJump は、ざっくり言うと:
- 左右の壁を検知するレイキャスト
- 張り付き中の落下速度制限(スライドダウン)
- ジャンプ入力時に反対方向へ跳ね飛ばすベクトル計算
を全部まとめた「壁ジャンプ専用ノード」です。
プレイヤー側は velocity を公開しておくだけでOK、入力もこのコンポーネント側で完結します。
フルコード: WallJump.gd
extends Node
class_name WallJump
##
## 壁ジャンプ専用コンポーネント
## - CharacterBody2D にアタッチして使う
## - 左右の壁をレイキャストで検出
## - 壁に張り付いている間は落下速度を抑制
## - ジャンプ入力で反対側へ飛び上がる
##
@export_group("基本設定")
## 壁として判定するコリジョンレイヤー
@export_flags_2d_physics var wall_collision_mask: int = 1
## 壁張り付き中に落ちていく最大速度(絶対値)
@export var wall_slide_max_fall_speed: float = 80.0
## 壁ジャンプの上方向の強さ(マイナスで上向き)
@export var wall_jump_vertical_speed: float = -280.0
## 壁ジャンプの水平方向の強さ(壁と反対方向に加える)
@export var wall_jump_horizontal_speed: float = 200.0
## 壁ジャンプ直後、一定時間は左右入力を無視して慣性を維持する時間
@export var control_lock_time: float = 0.15
@export_group("入力設定")
## ジャンプに使う入力アクション名(InputMap で定義しておく)
@export var jump_action: StringName = &"ui_accept"
## 左右入力に使うアクション名(キャラ本体と合わせると楽)
@export var move_left_action: StringName = &"ui_left"
@export var move_right_action: StringName = &"ui_right"
@export_group("デバッグ表示")
## 壁検出状態などをデバッグプリントするか
@export var debug_print: bool = false
# 親の CharacterBody2D をキャッシュ
var _body: CharacterBody2D
# 壁判定用の RayCast2D を左右に2本用意
var _ray_left: RayCast2D
var _ray_right: RayCast2D
# 現在どちらの壁に張り付いているか: -1 = 左, 1 = 右, 0 = なし
var _wall_dir: int = 0
# 壁ジャンプ後の操作ロック用タイマー
var _control_lock_timer: float = 0.0
func _ready() -> void:
# 親が CharacterBody2D であることを確認
_body = owner as CharacterBody2D
if _body == null:
push_error("WallJump コンポーネントは CharacterBody2D にアタッチしてください。")
set_process(false)
set_physics_process(false)
return
# 左右レイキャストを自動生成(シーンに置いてもいいが、コンポーネント内完結にする)
_create_raycast_nodes()
func _create_raycast_nodes() -> void:
# すでに存在していれば再利用
_ray_left = get_node_or_null("WallRayLeft") as RayCast2D
_ray_right = get_node_or_null("WallRayRight") as RayCast2D
if _ray_left == null:
_ray_left = RayCast2D.new()
_ray_left.name = "WallRayLeft"
add_child(_ray_left)
if _ray_right == null:
_ray_right = RayCast2D.new()
_ray_right.name = "WallRayRight"
add_child(_ray_right)
# レイの長さと向き(プレイヤーの半径+少し余裕)
var ray_length := 10.0
_ray_left.target_position = Vector2.LEFT * ray_length
_ray_right.target_position = Vector2.RIGHT * ray_length
# 壁レイヤーのみヒットするように設定
_ray_left.collision_mask = wall_collision_mask
_ray_right.collision_mask = wall_collision_mask
_ray_left.enabled = true
_ray_right.enabled = true
func _physics_process(delta: float) -> void:
if _body == null:
return
# 壁との接触状態を更新
_update_wall_state()
# 壁張り付き中の落下速度制限
_apply_wall_slide()
# 壁ジャンプ入力の処理
_handle_wall_jump_input(delta)
# 操作ロック時間の更新
if _control_lock_timer > 0.0:
_control_lock_timer -= delta
func _update_wall_state() -> void:
_ray_left.force_raycast_update()
_ray_right.force_raycast_update()
var on_left_wall := _ray_left.is_colliding()
var on_right_wall := _ray_right.is_colliding()
# 地面にいるときは壁張り付き状態をリセット
if _body.is_on_floor():
_wall_dir = 0
return
if on_left_wall and not on_right_wall:
_wall_dir = -1
elif on_right_wall and not on_left_wall:
_wall_dir = 1
else:
# 両方 or どちらもなし → 壁張り付き解除
_wall_dir = 0
if debug_print:
if _wall_dir == -1:
print("WallJump: 左の壁に接触中")
elif _wall_dir == 1:
print("WallJump: 右の壁に接触中")
func _apply_wall_slide() -> void:
if _wall_dir == 0:
return
# 上向き速度(ジャンプ中)は制限しない
if _body.velocity.y > wall_slide_max_fall_speed:
_body.velocity.y = wall_slide_max_fall_speed
func _handle_wall_jump_input(delta: float) -> void:
if _wall_dir == 0:
return
# 壁に張り付いているが、すでに地面にいるなら何もしない
if _body.is_on_floor():
return
# 壁の反対方向に入力しているかどうか(任意: 必須にしたい場合はこのチェックを有効にする)
var move_dir := 0
if Input.is_action_pressed(move_left_action):
move_dir -= 1
if Input.is_action_pressed(move_right_action):
move_dir += 1
# ここを true にすると「反対方向入力+ジャンプ」でのみ壁ジャンプ可能
var require_opposite_input := false
if require_opposite_input:
if _wall_dir == -1 and move_dir >= 0:
return
if _wall_dir == 1 and move_dir <= 0:
return
# ジャンプボタンが押された瞬間だけ反応
if Input.is_action_just_pressed(jump_action):
_do_wall_jump()
func _do_wall_jump() -> void:
if _wall_dir == 0:
return
# 壁の反対方向へ水平方向の速度を与える
var horizontal_dir := -_wall_dir # 壁が左(-1)なら右(+1)へ、右なら左へ
_body.velocity.x = horizontal_dir * wall_jump_horizontal_speed
# 上方向の速度を与える(マイナスで上向き)
_body.velocity.y = wall_jump_vertical_speed
# 一瞬だけ左右入力を無視したい場合のためのロックタイマー
_control_lock_timer = control_lock_time
if debug_print:
print("WallJump: 壁ジャンプ! dir=", horizontal_dir,
" vel=", _body.velocity)
## 壁張り付き中かどうかを外部から参照できると便利
func is_on_wall() -> bool:
return _wall_dir != 0
## 壁の向き(-1: 左, 1: 右, 0: なし)を返す
func get_wall_direction() -> int:
return _wall_dir
## 壁ジャンプ直後の操作ロック中かどうか
func is_control_locked() -> bool:
return _control_lock_timer > 0.0
使い方の手順
① プレイヤーシーンにコンポーネントを追加する
まずはプレイヤーのシーン構成をこんな感じにしておきます:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── WallJump (Node)
PlayerはCharacterBody2D継承のノードWallJumpは上記のWallJump.gdをアタッチしたNode
ポイントは、プレイヤー本体と壁ジャンプロジックを完全に分離していることです。Player.gd は「移動・重力・アニメーション」に集中させて、壁ジャンプのことは一切知らなくても動くようにしておきます。
② Player.gd に最低限の velocity 管理を書く
プレイヤー側の移動コードは、なるべくシンプルに保ちます。例:
extends CharacterBody2D
@export var move_speed: float = 160.0
@export var gravity: float = 900.0
@export var jump_speed: float = -260.0
# 壁ジャンプコンポーネント (シーンからドラッグしても、get_node() でもOK)
@onready var wall_jump: WallJump = $WallJump
func _physics_process(delta: float) -> void:
var input_dir := 0.0
if Input.is_action_pressed("ui_left"):
input_dir -= 1.0
if Input.is_action_pressed("ui_right"):
input_dir += 1.0
# 地上ジャンプ(壁ジャンプとは独立)
if is_on_floor() and Input.is_action_just_pressed("ui_accept"):
velocity.y = jump_speed
# 重力適用
if not is_on_floor():
velocity.y += gravity * delta
# 壁ジャンプ直後の慣性を優先したい場合は、ロック中は横入力を無視
if wall_jump == null or not wall_jump.is_control_locked():
velocity.x = move_speed * input_dir
move_and_slide()
壁ジャンプの本体はコンポーネント側にあるので、Player.gd はこれ以上複雑になりません。
「壁に触れているか?」などの判定も全部 WallJump 側が面倒を見てくれます。
③ 入力マップとコリジョンレイヤーを設定する
- InputMap に以下のアクションを追加
ui_left/ui_right/ui_accept(ジャンプ)
- 壁の TileMap や StaticBody2D のコリジョンレイヤーを確認
- 例: 壁レイヤーを「1番」にしておき、
WallJump.wall_collision_mask = 1にする
- 例: 壁レイヤーを「1番」にしておき、
これで、プレイヤーが壁に接触しているときにだけ、WallJump が張り付き状態と壁ジャンプを処理してくれるようになります。
④ 敵や動く床にも再利用してみる
コンポーネントのいいところは、「プレイヤー専用」じゃないところです。
たとえば、壁を伝って移動する敵を作りたい場合:
WallCrawler (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── WallJump (Node)
WallCrawler.gdでは基本移動だけ書いておく- 「プレイヤーと同じ壁ジャンプ挙動」をそのまま流用できる
あるいは、動く床が壁にぶつかったら反対側にジャンプするようなギミックも、CharacterBody2D + WallJump の組み合わせで実現できます。
「壁ジャンプ能力を持ったオブジェクト」という概念を、ノード1つで表現できるのがコンポーネント指向の気持ち良さですね。
メリットと応用
WallJump コンポーネントを導入することで、次のようなメリットがあります。
- Player.gd がスリムになる
壁判定・張り付き・ベクトル計算などの条件分岐がごっそり外に出るので、
プレイヤー本体は「左右移動+地上ジャンプ+重力」くらいに収まります。 - 別キャラ・別ギミックへの転用が簡単
CharacterBody2Dならどれにでもポン付けできるので、
「壁ジャンプする敵」「壁ジャンプする足場」などもすぐ試せます。 - シーン構造がフラットで見通しが良い
「PlayerWithWallJump」「PlayerWithDashAndWallJump」みたいな継承ツリーを作らずに、Player+WallJump+Dash… とノードを足していくだけで機能拡張できます。
「継承で機能を盛る」のではなく、「コンポーネントを足して能力を付与する」という発想に切り替えると、
プロジェクト後半になってからの機能追加・仕様変更がかなり楽になります。
改造案: 壁張り付き中にアニメーションやエフェクトを出す
例えば、WallJump からプレイヤー側へ「今、壁に張り付いているよ」という状態を渡して、
アニメーションを切り替えたり、パーティクルを出したりするのも簡単です。Player.gd にこんな関数を足してみましょう:
func _update_animation() -> void:
if wall_jump != null and wall_jump.is_on_wall():
$AnimatedSprite2D.play("wall_slide")
elif not is_on_floor():
$AnimatedSprite2D.play("jump")
elif abs(velocity.x) > 0.1:
$AnimatedSprite2D.play("run")
else:
$AnimatedSprite2D.play("idle")
このように、WallJump は「壁ジャンプの状態マシン」としても使えるので、
アニメーション・エフェクト・SE などとの連携もすべてコンポーネント経由で行えます。
ぜひ、自分のプロジェクト流の「壁ジャンプ体験」にカスタマイズしてみてください。
