横スクロールアクションを作っていると、どうしても気になるのが「ジャンプのシビアさ」ですね。
Godot標準の実装だと、だいたいこんな感じで書きがちです:
if Input.is_action_just_pressed("jump") and is_on_floor():
jump()
これだと、「着地の1フレーム前にジャンプボタンを押したのに、ジャンプしてくれない」という、いわゆる先行入力(ジャンプバッファ)問題が発生します。
そのたびに条件文を増やして、プレイヤーごと・敵ごとに似たようなコードをコピペして…とやっていると、すぐにスクリプトがカオスになります。
そこで今回は、どのキャラクターにもポン付けできる「コンポーネント」としての JumpBuffer を用意して、
「ジャンプボタンの先行入力」を プレイヤー本体のスクリプトから切り離す 形にしてみましょう。
継承や巨大な共通ベースクラスに頼らず、合成(Composition)でジャンプの気持ちよさを追加する、という発想ですね。
【Godot 4】ジャンプの「先行入力」で操作感アップ!「JumpBuffer」コンポーネント
この JumpBuffer コンポーネントは、
- 着地する直前に押されたジャンプボタンを記憶しておき、着地した瞬間にジャンプさせる
- 「バッファ時間(何秒前まで受け付けるか)」を
@exportで調整可能 - プレイヤーだけでなく、敵AIや動く床の「ジャンプ挙動」にも使い回せる
という、完全に独立した Node ベースのコンポーネントです。
キャラクター本体は「いつでも jump() を呼べばいいだけ」のシンプルな設計にしておき、
「いつ jump() を呼ぶか」のロジックをこのコンポーネントに任せる、という分業スタイルにしましょう。
フルコード:JumpBuffer.gd
extends Node
class_name JumpBuffer
## ジャンプの「先行入力(バッファ)」を扱うコンポーネント。
## - 入力されたジャンプを一定時間保存し、
## 所有者(親ノードなど)が着地した瞬間に JumpBuffer が自動でジャンプを要求します。
##
## 想定する親ノード:
## - CharacterBody2D / CharacterBody3D など「is_on_floor()」「jump()」を持つキャラ
## - もしくは、カスタムの is_on_floor() / perform_jump() を用意したノード
##
## 親に必要な最低限の API:
## - func is_on_floor() -> bool
## - func jump(): 実際にジャンプさせる処理
@export_category("Jump Buffer Settings")
@export_range(0.0, 0.5, 0.01, "or_greater")
var buffer_time: float = 0.15
## ジャンプの先行入力をどれくらいの時間保持するか(秒)。
## 例: 0.15 なら「着地の 0.15 秒前に押されたジャンプ」を有効にする。
@export var jump_action_name: StringName = &"jump"
## 対応する入力アクション名。
## Project Settings > Input Map で定義したアクションを指定します。
## 例: "ui_accept" や "jump" など。
@export var auto_consume_on_jump: bool = true
## true の場合:
## - ジャンプ成功時に内部バッファを自動クリアします。
## false の場合:
## - 外部から clear_buffer() を呼び出して管理することも可能です。
@export_category("Owner Settings")
@export var auto_find_owner: bool = true
## true の場合:
## - _ready() で親ノード(get_parent())を「所有者」として自動設定します。
## false の場合:
## - 外部から set_jump_owner() で明示的に渡してください。
@export var owner_path: NodePath
## 特定のノードを所有者にしたい場合に指定します。
## auto_find_owner が true かつ owner_path が空でない場合:
## - get_node(owner_path) を優先して所有者として使用します。
@export var owner_floor_method: StringName = &"is_on_floor"
## 所有者が「地面にいるかどうか」を返すメソッド名。
## デフォルトでは CharacterBody 系の is_on_floor() を想定。
@export var owner_jump_method: StringName = &"jump"
## 所有者に対してジャンプを実行させるメソッド名。
## デフォルトでは「jump()」という名前のメソッドを呼びます。
@export_category("Debug")
@export var debug_print: bool = false
## デバッグ用。true にするとバッファの状態を print() で出力します。
var _jump_buffer_timer: float = -1.0
## >= 0 のとき「ジャンプ入力がバッファに残っている」ことを意味する。
## 経過時間で減少し、0 未満になったら無効。
var _owner: Node
var _was_on_floor: bool = false
## 前フレームで地面にいたかどうか。着地判定に使う。
func _ready() -> void:
_setup_owner()
_was_on_floor = _is_owner_on_floor()
func _process(delta: float) -> void:
_update_input_buffer(delta)
_check_landing_and_consume()
func _setup_owner() -> void:
## 所有者ノードを決定する。
if owner_path != NodePath(""):
var node := get_node_or_null(owner_path)
if node:
_owner = node
return
if auto_find_owner:
_owner = get_parent()
if debug_print:
if _owner:
print("[JumpBuffer] owner set to: ", _owner.name)
else:
print("[JumpBuffer] owner not found. Please call set_jump_owner().")
func set_jump_owner(owner: Node) -> void:
## 外部から所有者を明示的に設定する場合に使用。
_owner = owner
if debug_print:
print("[JumpBuffer] owner manually set to: ", _owner.name)
func _update_input_buffer(delta: float) -> void:
## 1) 入力を検知してバッファに記録
if Input.is_action_just_pressed(jump_action_name):
_jump_buffer_timer = buffer_time
if debug_print:
print("[JumpBuffer] jump buffered for ", buffer_time, " sec")
## 2) 既存のバッファ時間を減少させる
if _jump_buffer_timer >= 0.0:
_jump_buffer_timer -= delta
if _jump_buffer_timer < 0.0:
# 負の値になったら無効化
_jump_buffer_timer = -1.0
if debug_print:
print("[JumpBuffer] buffer expired")
func _check_landing_and_consume() -> void:
if not _owner:
return
var on_floor_now := _is_owner_on_floor()
# 「前フレームは空中」「今フレームは地上」なら着地したとみなす
var just_landed := (not _was_on_floor) and on_floor_now
if just_landed and _jump_buffer_timer >= 0.0:
# バッファが生きている状態で着地したのでジャンプを実行
var jumped := _request_jump()
if jumped and auto_consume_on_jump:
clear_buffer()
_was_on_floor = on_floor_now
func _is_owner_on_floor() -> bool:
## 所有者の「地面判定メソッド」を呼び出す。
if not _owner:
return false
if _owner.has_method(owner_floor_method):
var result = _owner.call(owner_floor_method)
if typeof(result) == TYPE_BOOL:
return result
return false
func _request_jump() -> bool:
## 所有者にジャンプを要求する。
## 実際にジャンプしたかどうかを bool で返す。
if not _owner:
return false
if _owner.has_method(owner_jump_method):
_owner.call(owner_jump_method)
if debug_print:
print("[JumpBuffer] auto jump on landing!")
return true
if debug_print:
print("[JumpBuffer] owner has no method: ", owner_jump_method)
return false
func has_buffered_jump() -> bool:
## 現在バッファにジャンプが残っているかどうか。
return _jump_buffer_timer >= 0.0
func clear_buffer() -> void:
## バッファを手動でクリアしたいときに呼ぶ。
_jump_buffer_timer = -1.0
if debug_print:
print("[JumpBuffer] buffer cleared")
使い方の手順
ここからは、2Dのプレイヤーキャラ(CharacterBody2D)を例に、実際の使い方を見ていきましょう。
手順①:コンポーネントスクリプトを用意する
- 上記の
JumpBuffer.gdをそのままコピペして、
プロジェクトのres://components/JumpBuffer.gdなどに保存します。 - Godot エディタの右上の「クラス」タブに
JumpBufferが表示されていれば OK です。
手順②:プレイヤーシーンにアタッチする
プレイヤーのシーン構成例はこんな感じです:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── JumpBuffer (Node) ← これを追加
Playerシーンを開く。- 子ノードとして
Nodeを追加し、名前をJumpBufferに変更。 - そのノードに先ほどの
JumpBuffer.gdをアタッチする。 - インスペクタで以下を確認/設定:
buffer_time: 0.10~0.20 秒くらいがオススメ。jump_action_name: InputMap で定義したアクション(例:jump)。auto_find_owner: ON(デフォルト)なら親の Player を自動で所有者にしてくれます。
手順③:プレイヤー側のコードをシンプルに保つ
プレイヤー本体(Player.gd)は、「いつでも jump() を呼べばジャンプする」という最低限の責務だけを持たせます。
ジャンプの先行入力は JumpBuffer に丸投げです。
extends CharacterBody2D
@export var speed: float = 200.0
@export var jump_velocity: float = -350.0
@export var gravity: float = 900.0
func _physics_process(delta: float) -> void:
var input_dir := Input.get_axis("move_left", "move_right")
velocity.x = input_dir * speed
# 重力
if not is_on_floor():
velocity.y += gravity * delta
# 「通常のジャンプ入力」は任意でここに書いても良いが、
# JumpBuffer に任せる場合は「地上での即時ジャンプ」だけ扱うとシンプル。
if Input.is_action_just_pressed("jump") and is_on_floor():
jump()
move_and_slide()
func jump() -> void:
# 実際にジャンプするときの処理だけを定義
velocity.y = jump_velocity
ポイントは、Player 側は JumpBuffer の存在を知らなくても動くということです。
JumpBuffer は、親ノードの is_on_floor() と jump() を勝手に呼んでくれるので、
「付けたら勝手に先行入力対応になる」コンポーネントになっています。
手順④:他のキャラにもコピペなしで使い回す
例えば、ジャンプする敵キャラや、定期的にジャンプする動く床などにも同じように使えます。
EnemyJumper (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── JumpBuffer (Node)
EnemyJumper.gd 側では、jump() と is_on_floor() を用意しておけば OK。
敵の AI ロジックは「ジャンプボタンを押すタイミング」だけを考えればよく、
「着地直前に押されていたらどうするか」という細かい人間工学的な処理は JumpBuffer に任せられます。
メリットと応用
この JumpBuffer コンポーネントを導入することで、次のようなメリットがあります。
- ジャンプ入力まわりの if 文がプレイヤーコードから消える
「just_pressed のとき」「着地した瞬間」「空中で押していたら…」といった条件が
すべてコンポーネント側にまとまり、プレイヤー本体は「jump() するだけ」のシンプルな責務になります。 - シーン構造が浅くて済む
「ジャンプ用の共通ベースクラス」を作って全キャラに継承させる…というパターンに比べて、CharacterBody2D + JumpBufferのように「必要な機能だけを足す」スタイルになるので、
深いノード階層や複雑な継承ツリーに悩まされにくくなります。 - 別ゲームにもポータブル
このスクリプト1枚を別プロジェクトに持っていくだけで、同じ挙動をすぐ再利用できます。
「プレイヤーのジャンプ周りだけ別リポジトリで管理する」といった設計とも相性が良いですね。
さらに、「バッファ時間を変えるだけで難易度調整ができる」のもおいしいポイントです。
高難易度モードでは buffer_time = 0.05、カジュアルモードでは 0.20 など、
ゲームデザイナー視点での調整がしやすくなります。
改造案:空中での「ジャンプ猶予時間(コヨーテタイム)」もまとめて扱う
よくある拡張として、コヨーテタイム(地面から少し離れてもジャンプを許可する猶予)も同じコンポーネントで扱いたくなります。
簡易的なコヨーテタイムを追加するなら、こんな関数を足してみるのもアリです:
var _coyote_timer: float = 0.0
@export_range(0.0, 0.5, 0.01, "or_greater")
var coyote_time: float = 0.1
func _update_coyote_time(delta: float) -> void:
if not _owner:
return
if _is_owner_on_floor():
_coyote_timer = coyote_time
else:
_coyote_timer = max(_coyote_timer - delta, 0.0)
func can_coyote_jump() -> bool:
return _coyote_timer > 0.0
これを _process() の中から呼んで、ジャンプ要求時に can_coyote_jump() も考慮するようにすれば、
「先行入力(Jump Buffer)+コヨーテタイム」という、かなりリッチなジャンプ挙動を
それでもなお「1コンポーネント」に閉じ込めておくことができます。
継承でどんどん肥大化していく PlayerBase クラスを救うためにも、
こういう「1つの責務に特化したコンポーネント」をどんどん生やしていくスタイル、ぜひ試してみてください。
