Godot 4でアクションゲームを作っていると、プレイヤーの「2段ジャンプ」をどう実装するかで悩むことが多いですよね。素直にやると、プレイヤーのスクリプト(たいてい CharacterBody2D / CharacterBody3D)の中に
- ジャンプ入力
- 落下判定
- 2段目のジャンプ回数管理
- パーティクル再生
などが全部入りになって、あっという間に 500 行オーバーの「なんでも屋プレイヤースクリプト」になりがちです。
さらに悪いことに、敵キャラや動く足場にも「2段ジャンプっぽい挙動」を入れたくなったとき、
- プレイヤーのスクリプトをコピペして改造する
- ベースクラスを継承して
EnemyWithDoubleJump.gdを作る
…みたいな「継承地獄」に入りやすいんですよね。
そこで今回は、「2段ジャンプ」を 1 つの再利用可能なコンポーネントとして切り出した DoubleJump コンポーネントを用意しました。
プレイヤーにも敵にも、動く床にも、ノードにポン付けするだけで 2 段ジャンプ機能を追加できるようにしていきましょう。
【Godot 4】空中でもう一歩踏み出せ!「DoubleJump」コンポーネント
このコンポーネントの思想はシンプルです。
- 「ジャンプの実装」はホスト(プレイヤーなど)側に任せる
- 「2段目をいつ・何回・どんな演出で出すか」だけをコンポーネントが管理する
つまり、プレイヤーは「ジャンプする関数」を持っていて、
DoubleJump は「今ならもう一回ジャンプしていいよ」と判断して、その関数を呼ぶだけ、という分担です。
フルコード(GDScript / Godot 4)
extends Node
class_name DoubleJump
## 2段ジャンプ(空中で1回だけ追加ジャンプ)を提供するコンポーネント。
##
## 前提:
## - 親ノード(ホスト)は通常の「1段目ジャンプ」を自前で実装している。
## - ホストは `is_on_floor()` で接地判定ができる (CharacterBody2D/3D を想定)。
## - ホストは「ジャンプ処理用の関数」を持っている (デフォルト名: `perform_jump` だが変更可)。
##
## このコンポーネントは:
## - 空中で1回だけ2段目ジャンプを許可
## - ジャンプ時に任意のパーティクルを再生
## - 入力はホスト側から `request_jump()` を呼び出してもらう方式
## (入力処理とジャンプロジックを綺麗に分離するため)
@export_group("基本設定")
## ホストが持っている「実際にジャンプを行う関数名」。
## 例: プレイヤー側で `func perform_jump(): velocity.y = -jump_force` のように定義しておく。
@export var jump_method_name: StringName = &"perform_jump"
## 2段ジャンプを空中で許可するかどうか。
## (将来的に「地上でも2回までジャンプ」みたいな拡張をする場合のフラグ)
@export var allow_only_in_air: bool = true
## 2段ジャンプ可能回数。標準の「1回だけ」だが、2や3にすると3段ジャンプなども実現可能。
@export_range(1, 5, 1)
@export var max_extra_jumps: int = 1
@export_group("パーティクル演出")
## 2段ジャンプ時に再生するパーティクルノード。
## - CPUパーティクル: GPUParticles2D / GPUParticles3D / CPUParticles2D 等
## - Optional: 未設定ならパーティクルは再生しない。
@export var particle_node: NodePath
## パーティクルをホストの位置にスナップさせるかどうか。
@export var particle_follow_host: bool = true
## パーティクル再生時に、そのパーティクルの `restart()` を呼ぶかどうか。
## 連続でジャンプするゲームでは true 推奨。
@export var restart_particle_on_jump: bool = true
@export_group("デバッグ")
## 2段ジャンプの状態をログに出すかどうか。
@export var debug_log: bool = false
## 内部状態: 残りの追加ジャンプ回数。
var _remaining_extra_jumps: int = 0
## 親ノード(ホスト)への参照。
var _host: Node = null
## キャッシュされたパーティクルノード。
var _particle: Node = null
func _ready() -> void:
_host = get_parent()
if _host == null:
push_warning("[DoubleJump] 親ノードが存在しません。このコンポーネントは何もできません。")
# パーティクルノードをキャッシュ
if particle_node != NodePath():
_particle = get_node_or_null(particle_node)
if _particle == null:
push_warning("[DoubleJump] particle_node で指定されたノードが見つかりません。パーティクルは再生されません。")
# 初期化(接地していれば extra_jumps をリセット)
_reset_extra_jumps_if_needed()
func _physics_process(_delta: float) -> void:
# ホストが CharacterBody2D/3D を想定しているので、
# is_on_floor() を持っていれば接地判定として使う。
_reset_extra_jumps_if_needed()
func _reset_extra_jumps_if_needed() -> void:
if _host == null:
return
# is_on_floor() を持っているか確認
if not _host.has_method("is_on_floor"):
return
var on_floor := _host.is_on_floor()
if on_floor:
# 地上にいるなら、追加ジャンプ回数を全回復
if _remaining_extra_jumps != max_extra_jumps:
if debug_log:
print("[DoubleJump] Reset extra jumps to ", max_extra_jumps)
_remaining_extra_jumps = max_extra_jumps
## 入力側から呼び出す「ジャンプリクエスト」。
## 典型的にはホスト側の `_physics_process` で:
## if Input.is_action_just_pressed("jump"):
## if not double_jump.request_jump():
## perform_jump() # 1段目の処理
##
## のように使います。
func request_jump() -> bool:
if _host == null:
return false
# 2段ジャンプを許可できる状態かチェック
if not _can_use_extra_jump():
return false
# 実際にホスト側のジャンプ関数を呼ぶ
if not _host.has_method(jump_method_name):
push_warning("[DoubleJump] ホストがジャンプメソッド '%s' を持っていません。" % jump_method_name)
return false
# ここでホストのジャンプを実行
_host.call(jump_method_name)
# 2段ジャンプを1回消費
_remaining_extra_jumps -= 1
if debug_log:
print("[DoubleJump] Extra jump used. Remaining: ", _remaining_extra_jumps)
# パーティクルを再生
_play_particle()
return true
func _can_use_extra_jump() -> bool:
# まだ追加ジャンプが残っているか
if _remaining_extra_jumps <= 0:
return false
# 空中でだけ許可する場合、接地中はNG
if allow_only_in_air and _host != null and _host.has_method("is_on_floor"):
if _host.is_on_floor():
return false
return true
func _play_particle() -> void:
if _particle == null:
return
# パーティクルの位置をホストに合わせる
if particle_follow_host and _host is Node2D and _particle is Node2D:
(_particle as Node2D).global_position = (_host as Node2D).global_position
elif particle_follow_host and _host is Node3D and _particle is Node3D:
(_particle as Node3D).global_transform.origin = (_host as Node3D).global_transform.origin
# パーティクルの再生処理
# Godot 4 では `emitting` プロパティや `restart()` が使える。
if "emitting" in _particle:
_particle.emitting = false
_particle.emitting = true
if restart_particle_on_jump and _particle.has_method("restart"):
_particle.call("restart")
## --- 便利メソッド群(任意で利用) ---
## 残りの追加ジャンプ回数を取得
func get_remaining_extra_jumps() -> int:
return _remaining_extra_jumps
## 追加ジャンプ回数を外部からリセット(チェックポイントやアイテム取得時など)
func refill_extra_jumps() -> void:
_remaining_extra_jumps = max_extra_jumps
if debug_log:
print("[DoubleJump] Extra jumps refilled to ", max_extra_jumps)
使い方の手順
ここでは 2D のプレイヤーを例にしますが、3D でも基本は同じです。
手順①: プレイヤー側に「通常ジャンプ関数」を用意する
プレイヤー(CharacterBody2D)に、1段目ジャンプを行う関数を用意します。
デフォルトでは perform_jump という名前を想定しているので、その名前で定義すると楽です。
# Player.gd (例)
extends CharacterBody2D
@export var jump_force: float = 400.0
@export var gravity: float = 900.0
@export var move_speed: float = 200.0
var double_jump: DoubleJump
func _ready() -> void:
# 同じノード直下にアタッチした DoubleJump を取得
double_jump = $DoubleJump
func _physics_process(delta: float) -> void:
# 重力
if not is_on_floor():
velocity.y += gravity * delta
else:
# 着地時に縦速度をリセット
if velocity.y > 0:
velocity.y = 0
# 左右移動
var dir := Input.get_axis("move_left", "move_right")
velocity.x = dir * move_speed
# ジャンプ入力
if Input.is_action_just_pressed("jump"):
# まずは 2段ジャンプにお願いしてみる
var used_extra := false
if double_jump:
used_extra = double_jump.request_jump()
# 2段ジャンプが使えなかった場合は、通常ジャンプを試す
if not used_extra and is_on_floor():
perform_jump()
move_and_slide()
func perform_jump() -> void:
# 上向きに速度を与えるだけのシンプルなジャンプ処理
velocity.y = -jump_force
手順②: シーンに DoubleJump コンポーネントを追加する
プレイヤーシーンの構成例:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── JumpDust (GPUParticles2D) ← 2段ジャンプ用パーティクル └── DoubleJump (Node) ← 今回のコンポーネント
ポイント:
DoubleJump.gdを新規スクリプトとして作成し、NodeにアタッチしてDoubleJumpノードを生やす。- パーティクル用に
GPUParticles2D(ここではJumpDust)を用意しておく。
手順③: DoubleJump のエクスポート変数を設定する
エディタで DoubleJump ノードを選択し、インスペクタから:
- jump_method_name:
perform_jump(プレイヤー側の関数名) - max_extra_jumps:
1(標準の2段ジャンプ) - particle_node:
../JumpDustなど、パーティクルノードへのパス - particle_follow_host:
On(プレイヤーの位置に追従)
これで、プレイヤーは:
- 地上でジャンプ … 1段目
- 空中でもう一度ジャンプ … 2段目(
DoubleJumpが処理+パーティクル再生)
という挙動になります。
手順④: 敵や動く床にも「コピペなし」で再利用する
敵キャラにも 2 段ジャンプを与えたい場合、敵側も perform_jump を持っていれば、同じ DoubleJump コンポーネントをポン付けするだけで OK です。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── JumpDust (GPUParticles2D) └── DoubleJump (Node)
敵側のスクリプト例:
extends CharacterBody2D
@export var jump_force: float = 350.0
var double_jump: DoubleJump
func _ready() -> void:
double_jump = $DoubleJump
func _physics_process(delta: float) -> void:
# 何かのAIロジックでジャンプしたくなったとき:
if should_jump_now():
# まず2段ジャンプを試す
if not double_jump.request_jump() and is_on_floor():
perform_jump()
move_and_slide()
func perform_jump() -> void:
velocity.y = -jump_force
func should_jump_now() -> bool:
# 適当なAI条件
return false
このように、ジャンプの中身は各キャラクターが持ち、2段目の管理はコンポーネントに丸投げできるのがポイントです。
メリットと応用
DoubleJump コンポーネントを使うことで、次のようなメリットがあります。
- プレイヤースクリプトがスリムになる
2段ジャンプの回数管理やパーティクル制御がプレイヤーから消えるので、「移動」「攻撃」「入力処理」など他のロジックと混ざらずに済みます。 - 再利用性が高い
敵、動く足場、ギミックなど、CharacterBody2D/3Dならほぼそのまま使い回せます。
perform_jumpの中身を変えれば「ホバージャンプ」「ロケットジャンプ」などにも対応可能です。 - シーン構造がフラットで見通しが良い
「ジャンプ系の機能」はDoubleJumpノードに集約されるので、シーンツリーを見ただけで「このキャラは2段ジャンプできるんだな」と一目で分かります。 - 継承に縛られない
「2段ジャンプ付きプレイヤー」「2段ジャンプ付き敵」みたいなクラス階層を作らずに済むので、将来的な仕様変更にも強いです。
応用として、例えば「空中ダッシュ」と組み合わせるときも、AirDash コンポーネントを別途作り、Player に複数コンポーネントをアタッチするだけで済みます。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── JumpDust (GPUParticles2D) ├── DashTrail (GPUParticles2D) ├── DoubleJump (Node) └── AirDash (Node)
こういう「レゴブロック的な組み合わせ」ができるのが、コンポーネント指向の気持ちいいところですね。
改造案:ジャンプ直後だけ「ジャンプバッファ」を許可する
例えば、「落下中にジャンプボタンを押しておくと、着地直後に自動で2段ジャンプする」みたいな甘めの操作感を入れたい場合、DoubleJump に簡単なバッファ機能を追加できます。
# DoubleJump.gd のどこかに追加する例
@export_group("入力バッファ")
@export var jump_buffer_time: float = 0.1 # 秒
var _buffer_timer: float = 0.0
func _physics_process(delta: float) -> void:
_reset_extra_jumps_if_needed()
# バッファ時間のカウントダウン
if _buffer_timer > 0.0:
_buffer_timer -= delta
# 着地していて、バッファが残っているなら自動でジャンプ
if _buffer_timer >= 0.0 and _host and _host.has_method("is_on_floor") and _host.is_on_floor():
if request_jump():
_buffer_timer = 0.0
# 入力側から呼ぶ関数を少し拡張(元の request_jump をラップするイメージ)
func request_jump_with_buffer() -> bool:
# まずは通常の request_jump を試す
if request_jump():
return true
# 失敗したら、バッファに記録
_buffer_timer = jump_buffer_time
return false
プレイヤー側では double_jump.request_jump() の代わりに request_jump_with_buffer() を呼ぶようにすれば、
「ちょっと早めにボタンを押しても、着地直後に2段ジャンプが出る」気持ちいい操作感にできます。
こんな感じで、「2段ジャンプ」というよくある機能も、1つのコンポーネントに切り出すだけでシーン構造とコードの両方がかなりスッキリします。
継承ベースの巨大プレイヤークラスから卒業して、レゴブロック的に機能を足していく Godot ライフを楽しんでいきましょう。
