Godot 4で2D/3Dアクションを作っていると、ジャンプ入力まわりでこんな悩みが出てきますよね。
- 「足場からちょっとだけ落ちたのに、ジャンプが出なくてストレス…」
- 「プレイヤーの入力タイミングがシビアすぎて、気持ちよくない」
- 「毎回Playerスクリプトにコヨーテタイム処理を書いていて、コピペ地獄」
多くのチュートリアルでは、Player.gd の中に「地上判定 + タイマー + フラグ管理」を全部書いてしまいます。でもそれをやっていると、
- ジャンプまわりのロジックが肥大化する
- 敵キャラや動く足場など、別のシーンで同じ仕組みを使いたいときにコピペになる
- 継承でまとめようとすると、ジャンプしないキャラまで同じベースクラスになりがち
そこで今回は、「コヨーテタイムだけ」を独立したコンポーネントに切り出します。
プレイヤーでも敵でも、CharacterBody2D でも CharacterBody3D でも、ノードにポン付けして使えるようにしてしまいましょう。
【Godot 4】ジャンプの許容時間をコンポーネント化!「CoyoteTime」コンポーネント
この「CoyoteTime」コンポーネントは、
- 「最後に地面にいた時刻」を記録する
- 「今ジャンプしていいか?」を問い合わせるAPIを提供する
- 2D/3D両方で使えるように、
CharacterBody2D/CharacterBody3Dに対応
という、超シンプルな責務だけを持つNodeです。
ジャンプ処理そのものはプレイヤー側に任せて、「コヨーテタイムの判定」だけを委譲する形ですね。
フルコード:CoyoteTime.gd
extends Node
class_name CoyoteTime
## コヨーテタイム(足場から落ちた直後の猶予時間)を管理するコンポーネント。
## CharacterBody2D / CharacterBody3D にアタッチして使うことを想定しています。
@export_range(0.0, 0.5, 0.01, "or_greater")
var coyote_time_duration: float = 0.12:
## コヨーテタイムの長さ(秒)。
## 0.1〜0.2秒くらいが「気持ちいい」ことが多いです。
set(value):
coyote_time_duration = max(value, 0.0)
@export var auto_detect_body: bool = true:
## true の場合、親ノードから自動的に CharacterBody2D / 3D を探します。
## false にして手動で body_2d / body_3d を設定することもできます。
set(value):
auto_detect_body = value
if is_inside_tree():
_detect_body()
@export var body_2d: CharacterBody2D:
## 2D用の対象ボディ。auto_detect_body が false のときに設定してください。
set(value):
body_2d = value
if value:
body_3d = null
@export var body_3d: CharacterBody3D:
## 3D用の対象ボディ。auto_detect_body が false のときに設定してください。
set(value):
body_3d = value
if value:
body_2d = null
## デバッグ用: 現在コヨーテタイム中かどうかをエディタ上で確認したいときに使えます。
@export var debug_print_state: bool = false
## 最後に「地上にいた」と判定された時刻(秒)
var _last_on_floor_time: float = -1.0
## 1フレーム前の is_on_floor() 結果
var _was_on_floor: bool = false
func _ready() -> void:
if auto_detect_body:
_detect_body()
if not body_2d and not body_3d:
push_warning("CoyoteTime: 対象の CharacterBody2D / 3D が見つかりませんでした。auto_detect_body を false にして手動設定するか、親に CharacterBody を置いてください。")
# 初期状態を記録
var on_floor := _is_on_floor()
_was_on_floor = on_floor
if on_floor:
_last_on_floor_time = Time.get_unix_time_from_system()
func _physics_process(_delta: float) -> void:
if not body_2d and not body_3d:
return
var on_floor := _is_on_floor()
# 地上にいる間は常に「最後に地上にいた時間」を更新
if on_floor:
_last_on_floor_time = Time.get_unix_time_from_system()
# デバッグログ(オプション)
if debug_print_state:
var state := "NONE"
if on_floor:
state = "ON_FLOOR"
elif can_use_coyote_time():
state = "COYOTE"
else:
state = "AIR"
print("CoyoteTime: ", state)
_was_on_floor = on_floor
func _detect_body() -> void:
## 親ノードから CharacterBody2D / 3D を自動検出します。
body_2d = null
body_3d = null
var parent := get_parent()
if not parent:
return
if parent is CharacterBody2D:
body_2d = parent
elif parent is CharacterBody3D:
body_3d = parent
else:
# 1階層上になくても、上位階層から探したい場合はここを拡張してもOK
for child in parent.get_children():
if child is CharacterBody2D:
body_2d = child
break
if child is CharacterBody3D:
body_3d = child
break
func _is_on_floor() -> bool:
## 対象ボディの is_on_floor() をラップします。
if body_2d:
return body_2d.is_on_floor()
if body_3d:
return body_3d.is_on_floor()
return false
func can_use_coyote_time() -> bool:
## 「今ジャンプしてよいか?」を判定するメインAPI。
##
## - 地上にいるなら true
## - 地上から離れていても、coyote_time_duration 秒以内なら true
## - それ以外は false
##
## 実際のジャンプ処理側では、こんな感じで使います:
## if Input.is_action_just_pressed("jump") and coyote_time.can_use_coyote_time():
## _do_jump()
##
if not body_2d and not body_3d:
return false
# まずは素直に「地上にいるか」
if _is_on_floor():
return true
# 地上から離れている場合、最後に地上にいた時間との差分をチェック
if _last_on_floor_time < 0.0:
return false
var now := Time.get_unix_time_from_system()
var elapsed := now - _last_on_floor_time
return elapsed <= coyote_time_duration
func reset_coyote_time() -> void:
## 外部から「もうコヨーテタイムを使い切った」としてリセットしたいときに呼びます。
## 例: 一度ジャンプしたら、残り時間があっても無効にする場合など。
_last_on_floor_time = -1.0
func consume_and_check() -> bool:
## 「コヨーテタイムが使えるなら true を返し、同時にリセットする」ヘルパー関数。
## - 地上にいるとき: true を返すが、リセットはしません(通常ジャンプ扱い)。
## - コヨーテタイム中: true を返し、_last_on_floor_time をリセットします。
## - それ以外: false。
if _is_on_floor():
return true
if can_use_coyote_time():
reset_coyote_time()
return true
return false
使い方の手順
ここでは 2D のプレイヤーキャラを例にしますが、3D でもほぼ同じ構成で使えます。
手順①:シーン構成に CoyoteTime を追加する
まずはプレイヤーシーンにコンポーネントを追加しましょう。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── CoyoteTime (Node)
PlayerはCharacterBody2DCoyoteTimeはNodeとして追加し、スクリプトに上記のCoyoteTime.gdをアタッチauto_detect_body = trueのままでOK(親がCharacterBody2Dなので自動で拾われます)
手順②:プレイヤースクリプトからコンポーネントを参照
Player.gd の例です。ジャンプ処理だけ抜粋した、最小限のサンプルを書いておきます。
extends CharacterBody2D
@export var speed: float = 200.0
@export var jump_velocity: float = -350.0
@export var gravity: float = 900.0
@onready var coyote_time: CoyoteTime = $CoyoteTime
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
# ジャンプ入力 + コヨーテタイム判定
if Input.is_action_just_pressed("jump"):
# CoyoteTime 側のロジックに丸投げ
if coyote_time.consume_and_check():
velocity.y = jump_velocity
move_and_slide()
ポイントは、「地上判定や経過時間の管理は CoyoteTime に全部任せる」ことです。
Player 側は consume_and_check() の結果だけ見て、ジャンプするかどうかを決めればOKです。
手順③:敵キャラや動く床にも流用する
同じコンポーネントを、敵キャラや特殊な足場にも簡単に使い回せます。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── CoyoteTime (Node)
例えば「崩れる足場」が、プレイヤーに踏まれてから少しだけジャンプを許容する、みたいなギミックも同じパターンで実現できます。
FallingPlatform (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── CoyoteTime (Node)
ジャンプ処理を持つかどうかは各シーンの自由ですが、「地面から離れてからの猶予判定」だけなら CoyoteTime をそのまま利用できます。
手順④:3Dキャラで使う場合
3D でも構成はほとんど同じです。
Player3D (CharacterBody3D) ├── MeshInstance3D ├── CollisionShape3D └── CoyoteTime (Node)
Player3D.gd では、CharacterBody3D 版に置き換えるだけです。
extends CharacterBody3D
@export var speed: float = 6.0
@export var jump_velocity: float = 5.0
@export var gravity: float = 12.0
@onready var coyote_time: CoyoteTime = $CoyoteTime
func _physics_process(delta: float) -> void:
# 簡易的な入力(WASD)
var input_dir := Vector2(
Input.get_action_strength("move_right") - Input.get_action_strength("move_left"),
Input.get_action_strength("move_backward") - Input.get_action_strength("move_forward")
).normalized()
var direction := (transform.basis * Vector3(input_dir.x, 0, input_dir.y))
velocity.x = direction.x * speed
velocity.z = direction.z * speed
# 重力
if not is_on_floor():
velocity.y -= gravity * delta
# ジャンプ + コヨーテタイム
if Input.is_action_just_pressed("jump"):
if coyote_time.consume_and_check():
velocity.y = jump_velocity
move_and_slide()
メリットと応用
この CoyoteTime コンポーネントを使うメリットは、単に「コヨーテタイムが実装できる」以上にあります。
- シーン構造がスッキリ
Player.gd から「地面から離れて何秒経ったか」の管理ロジックが消えるので、ジャンプ処理が読みやすくなります。 - 使い回しが簡単
敵キャラ、動く足場、特殊なギミックなど、「地上猶予」の概念が必要になったら、CoyoteTime ノードをポンとアタッチするだけでOKです。 - 継承ツリーに縛られない
「ジャンプするものは全部 BaseCharacter を継承して…」とやり始めると、すぐに継承ツリーがつらくなります。CoyoteTime は単なる Node なので、どんなシーン構成にも後付けできます。 - パラメータ調整が楽
各キャラごとにcoyote_time_durationを変えるだけで、「シビアなキャラ」「甘めのキャラ」を簡単に作り分けられます。
コンポーネント指向で設計しておくと、「このキャラはコヨーテタイムなし」「この敵だけ超長いコヨーテタイム」みたいなバリエーションを、スクリプトを一切いじらずにエディタ上で完結させられるのが気持ちいいですね。
改造案:入力バッファと組み合わせる
さらに気持ちよくするなら、「ジャンプボタンをちょっと早押ししても受け付ける」入力バッファと組み合わせるのが定番です。
CoyoteTime に軽く機能追加するなら、例えばこんな感じです。
@export_range(0.0, 0.3, 0.01, "or_greater")
var jump_buffer_duration: float = 0.08 # ジャンプの先行入力を受け付ける時間(秒)
var _last_jump_pressed_time: float = -1.0
func register_jump_pressed() -> void:
## ジャンプボタンが押された瞬間に呼び出してください。
_last_jump_pressed_time = Time.get_unix_time_from_system()
func can_perform_buffered_jump() -> bool:
## 「入力バッファ + コヨーテタイム + 地上判定」をまとめて判定。
if _last_jump_pressed_time < 0.0:
return false
var now := Time.get_unix_time_from_system()
if now - _last_jump_pressed_time > jump_buffer_duration:
return false
# ここで通常のコヨーテタイム判定を流用
if can_use_coyote_time():
_last_jump_pressed_time = -1.0 # 消費
return true
return false
Player 側では、
_input()でregister_jump_pressed()を呼ぶ_physics_process()の中でcan_perform_buffered_jump()をチェックしてジャンプ
という形にすれば、「ちょい早押し + ちょい遅押し」の両方を吸収できる、かなりリッチなジャンプ体験になります。
こんなふうに、「ジャンプの気持ちよさ」に関わるロジックを全部コンポーネントとして切り出していくと、プレイヤー本体のスクリプトはどんどんスリムになっていきます。継承ツリーに悩む前に、「まずはコンポーネント化できないか?」と考えてみると、Godot 4 の開発がかなり楽になりますよ。




