横スクロールアクションを作っていると、どうしても気になるのが「ジャンプのシビアさ」ですね。
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)を例に、実際の使い方を見ていきましょう。

手順①:コンポーネントスクリプトを用意する

  1. 上記の JumpBuffer.gd をそのままコピペして、
    プロジェクトの res://components/JumpBuffer.gd などに保存します。
  2. Godot エディタの右上の「クラス」タブに JumpBuffer が表示されていれば OK です。

手順②:プレイヤーシーンにアタッチする

プレイヤーのシーン構成例はこんな感じです:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── JumpBuffer (Node)   ← これを追加
  1. Player シーンを開く。
  2. 子ノードとして Node を追加し、名前を JumpBuffer に変更。
  3. そのノードに先ほどの JumpBuffer.gd をアタッチする。
  4. インスペクタで以下を確認/設定:
    • 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つの責務に特化したコンポーネント」をどんどん生やしていくスタイル、ぜひ試してみてください。