Godot 4 で空中移動を作ろうとすると、つい「プレイヤーシーンを継承して、その中にジェットパック用の処理を書き足す」みたいな実装をしがちですよね。
最初はそれでも動きますが、だんだんと…
- プレイヤーのスクリプトが 500 行を超えてカオス
- 敵や動く足場にも同じような「浮遊機能」を入れたくなるが、コピペ地獄
- 「このシーンだけジェットパック無効にしたい」みたいな時の条件分岐が肥大化
といった「継承ベタベタ&巨大スクリプト問題」にぶつかりがちです。
そこで今回は、「ジェットパック機能だけを切り出したコンポーネント」として実装してみましょう。
プレイヤー、敵、動く床、なんでも「Jetpack コンポーネント」をアタッチするだけで、ボタンを押している間だけ上昇力を加え続ける、燃料付きジェットパックが使えるようになります。
【Godot 4】空も飛べるさコンポジション!「Jetpack」コンポーネント
この Jetpack コンポーネントは、
- ボタン(アクション)を押している間だけ上昇力を付与
- 燃料を消費し、0 になったら噴射不可
- 地面に着いたら燃料を自動回復(オプション)
CharacterBody2D/CharacterBody3Dなど「速度ベクトルを持つノード」にアタッチして使う
というシンプルな設計です。
GDScript フルコード(2D 用 Jetpack コンポーネント)
extends Node
class_name Jetpack
"""
Jetpack コンポーネント(2D 用)
・親ノードの「velocity.y」に上向きの力を加えることで、ジェットパック挙動を実現します。
・親は CharacterBody2D を想定していますが、
velocity プロパティ (Vector2) を持っていれば他のノードでも動きます。
"""
@export_category("Input")
## 噴射に使う InputMap のアクション名
@export var jetpack_action: StringName = &"ui_accept"
@export_category("Jetpack Settings")
## 上向きの加速度(マイナス方向が上)。絶対値を大きくすると強く上昇します。
@export var jetpack_acceleration: float = -900.0
## 噴射中に消費する燃料量(1秒あたり)
@export var fuel_consumption_per_sec: float = 25.0
## 最大燃料量
@export var max_fuel: float = 100.0
## 地面に着地しているときに自動回復する燃料量(1秒あたり)
@export var fuel_recharge_per_sec: float = 40.0
## 空中でも燃料を少しずつ回復させるか
@export var recharge_in_air: bool = false
## 空中回復する場合の回復量(1秒あたり)
@export var fuel_recharge_in_air_per_sec: float = 10.0
@export_category("Limits")
## 噴射中に上方向速度がこの値より速くならないように制限(例: -600 なら上方向最大 600)
@export var max_upward_speed: float = -600.0
## 燃料が 0 になった後、再噴射できるようになるまでの待ち時間(秒)
@export var empty_cooldown: float = 0.3
@export_category("Debug")
## デバッグ用に現在の燃料値をインスペクタに表示するためのプロパティ
@export var debug_current_fuel: float:
get:
return _current_fuel
## デバッグ用: 現在噴射中かどうか
@export var debug_is_thrusting: bool:
get:
return _is_thrusting
# 内部状態
var _current_fuel: float
var _is_thrusting: bool = false
var _empty_timer: float = 0.0
# 親ノードのキャッシュ(CharacterBody2D を想定)
var _body: Node = null
func _ready() -> void:
# 初期燃料は最大値
_current_fuel = max_fuel
# 親ノードをキャッシュ
_body = get_parent()
if _body == null:
push_warning("Jetpack: 親ノードがありません。このコンポーネントは CharacterBody2D などの子として使ってください。")
return
# velocity プロパティが存在するか軽くチェック
if not _body.has_method("get"):
push_warning("Jetpack: 親ノードに get() メソッドがありません。velocity プロパティを持つノードにアタッチしてください。")
elif not _body.has_variable("velocity"):
push_warning("Jetpack: 親ノードに 'velocity' プロパティが見つかりません。CharacterBody2D などを想定しています。")
func _physics_process(delta: float) -> void:
if _body == null:
return
_update_cooldown(delta)
_update_fuel_recharge(delta)
_process_input_and_thrust(delta)
func _update_cooldown(delta: float) -> void:
if _empty_timer > 0.0:
_empty_timer -= delta
if _empty_timer < 0.0:
_empty_timer = 0.0
func _update_fuel_recharge(delta: float) -> void:
# 燃料が最大なら何もしない
if _current_fuel >= max_fuel:
_current_fuel = max_fuel
return
# 親が CharacterBody2D で is_on_floor() を持つ場合のみ着地判定
var on_floor := false
if _body.has_method("is_on_floor"):
on_floor = _body.is_on_floor()
if on_floor:
# 地面にいるときは高速回復
_current_fuel += fuel_recharge_per_sec * delta
elif recharge_in_air:
# 空中回復を許可している場合
_current_fuel += fuel_recharge_in_air_per_sec * delta
# 上限を超えないようにクランプ
_current_fuel = clamp(_current_fuel, 0.0, max_fuel)
func _process_input_and_thrust(delta: float) -> void:
_is_thrusting = false
# クールダウン中、または燃料がないなら噴射できない
if _empty_timer > 0.0 or _current_fuel <= 0.0:
return
# 入力チェック
if not Input.is_action_pressed(jetpack_action):
return
# ここまで来たら噴射可能
_is_thrusting = true
# 燃料を消費
_current_fuel -= fuel_consumption_per_sec * delta
if _current_fuel <= 0.0:
_current_fuel = 0.0
_empty_timer = empty_cooldown
_is_thrusting = false
return
# 親の velocity に上向き加速度を加える
var v := _body.velocity
v.y += jetpack_acceleration * delta
# 上方向速度を制限(より「上に速く」なりすぎないように)
if v.y < max_upward_speed:
v.y = max_upward_speed
_body.velocity = v
# --- 公開 API ---
## 現在の燃料を 0.0〜1.0 の割合で返す(UI のゲージ用)
func get_fuel_ratio() -> float:
if max_fuel <= 0.0:
return 0.0
return clamp(_current_fuel / max_fuel, 0.0, 1.0)
## 現在燃料を直接設定する(デバッグ・回復アイテムなどから呼ぶ用)
func set_fuel(value: float) -> void:
_current_fuel = clamp(value, 0.0, max_fuel)
## 最大燃料を増やしたり減らしたりしたい場合に使う
func set_max_fuel(value: float, keep_ratio: bool = true) -> void:
value = max(value, 0.0)
if keep_ratio and max_fuel > 0.0:
var ratio := get_fuel_ratio()
max_fuel = value
_current_fuel = max_fuel * ratio
else:
max_fuel = value
_current_fuel = clamp(_current_fuel, 0.0, max_fuel)
## 現在噴射中かどうか(アニメーション制御などで使う)
func is_thrusting() -> bool:
return _is_thrusting
使い方の手順
ここでは 2D のプレイヤーにジェットパックを付ける例で説明します。
(3D 版にしたい場合は velocity を Vector3 に変えるだけでほぼ同じ構造にできます)
手順①: スクリプトを保存してコンポーネント化
- 上記の GDScript を
res://components/jetpack.gdなどに保存します。 class_name Jetpackを宣言しているので、エディタから「ノードを追加」するときに Jetpack と検索すれば出てきます。
手順②: プレイヤーシーンに Jetpack ノードを追加
典型的な 2D プレイヤーシーン構成はこんな感じだと思います:
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── Jetpack (Node)
PlayerはCharacterBody2Dベース- その子として
Jetpackノードを追加し、スクリプトにjetpack.gdをアタッチ
重要なのは「Jetpack は プレイヤーの子ノード」であることです。
親の velocity を直接いじることで、プレイヤーに上昇力を与えています。
手順③: 入力マップを設定
デフォルトでは jetpack_action = "ui_accept" にしてあるので、
- プロジェクト設定 > Input Map を開く
ui_acceptにスペースキーなどを割り当てる(既に割り当てがある場合はそのままでもOK)- もしくは、Jetpack ノードのインスペクタで
jetpack_actionを"jetpack"など別名に変え、Input Map に同名のアクションを追加
これで、そのアクションを押している間だけジェットパックが噴射されるようになります。
手順④: プレイヤースクリプト側は「普通に移動」するだけ
プレイヤーのスクリプトは、いつも通り move_and_slide() を呼ぶだけで OK です。
ジェットパックは velocity に対する「追加の加速度」として動きます。
extends CharacterBody2D
@export var move_speed: float = 200.0
@export var gravity: float = 1200.0
@export var jump_speed: float = -400.0
func _physics_process(delta: float) -> void:
# 水平移動
var dir := Input.get_axis("ui_left", "ui_right")
velocity.x = dir * move_speed
# 重力
if not is_on_floor():
velocity.y += gravity * delta
else:
# ジャンプ
if Input.is_action_just_pressed("ui_up"):
velocity.y = jump_speed
# Jetpack コンポーネントが velocity を上書き・加速してくれる
# ここでは特に意識せず move_and_slide() するだけ
move_and_slide()
ポイントは、「プレイヤー側はジェットパックのことを一切知らない」ことです。
Jetpack コンポーネントが「親の velocity を勝手にいじる」ことで、合成(Composition)による再利用性の高い設計になっています。
別の使用例: 敵や動く足場にもそのまま付けられる
同じ Jetpack コンポーネントを、敵や動く足場に付けることもできます。
例1: 空中をふわふわ飛ぶ敵
FlyingEnemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── Jetpack (Node)
- 敵 AI スクリプトからは、特に Jetpack を意識せず水平移動だけ制御
- Jetpack の
jetpack_actionを"enemy_jetpack"にして、AI スクリプトからInput.action_press()/action_release()を使ってもよいですし、 - あるいは Jetpack を少し改造して「自動噴射モード」を作っても OK(後述の改造案参照)
例2: 上昇する動く床
MovingPlatform (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── Jetpack (Node)
動く床に Jetpack を付けると、「ボタンを押している間だけ上昇するエレベーター」のようなギミックも簡単に作れます。
このときも、MovingPlatform 側は「通常の横移動」だけを書いておき、
上下の力は Jetpack コンポーネントに任せる、という分担にするとスクリプトがスッキリします。
メリットと応用
この Jetpack コンポーネントを使うメリットを整理すると:
- プレイヤー・敵・ギミックなど、どのシーンにも同じ機能をポン付けできる
→ 「ジェットパック付き敵」「ジェットパック付き動く床」などのバリエーションが合成だけで作れます。 - 親スクリプトが肥大化しない
→ ジャンプ・ダッシュ・二段ジャンプ・ジェットパック…と増えても、各機能をコンポーネントとして分離できます。 - シーン構造が読みやすい
→ シーンツリーを見れば「このキャラは Jetpack を持っている」と一目で分かり、レベルデザイン時の把握が楽になります。 - パラメータ調整が楽
→ Jetpack ノードのインスペクタで「燃料量」「加速度」「クールダウン」などを個別に調整できるので、
プレイヤーと敵で挙動を変えるのも簡単です。
「継承で全部入りプレイヤー」を作るのではなく、
「歩くコンポーネント」「ダッシュコンポーネント」「Jetpack コンポーネント」のように分けておくと、
後から「このステージでは Jetpack だけ禁止」「この敵はダッシュだけ使える」などの調整が圧倒的にやりやすくなります。
改造案: 自動噴射モードを追加する
最後に、ちょっとした改造案として「入力なしで常に噴射し続けるモード」を追加する例を載せておきます。
敵やギミック用に、「常にふわふわ浮いている」挙動を作りたいときに便利です。
@export_category("Auto Thrust")
## 入力なしで自動噴射するかどうか
@export var auto_thrust: bool = false
func _process_input_and_thrust(delta: float) -> void:
_is_thrusting = false
if _empty_timer > 0.0 or _current_fuel <= 0.0:
return
var should_thrust := false
if auto_thrust:
# 自動噴射モードなら常に噴射
should_thrust = true
else:
# 通常モードなら入力に応じて噴射
should_thrust = Input.is_action_pressed(jetpack_action)
if not should_thrust:
return
_is_thrusting = true
# 以降の燃料消費 & 加速度処理は元の実装と同じ
_current_fuel -= fuel_consumption_per_sec * delta
if _current_fuel <= 0.0:
_current_fuel = 0.0
_empty_timer = empty_cooldown
_is_thrusting = false
return
var v := _body.velocity
v.y += jetpack_acceleration * delta
if v.y < max_upward_speed:
v.y = max_upward_speed
_body.velocity = v
このように、Jetpack コンポーネント自体を少しずつ育てていけば、
「空中挙動は全部 Jetpack に任せる」という設計にできて、他のスクリプトがどんどんシンプルになっていきます。
ぜひ、自分のプロジェクト用にカスタマイズしてみてください。
