Godot 4 で「ヒップドロップ(グラウンドパンチ)」みたいなアクションを入れようとすると、ありがちな実装はこんな感じになりますよね。

  • プレイヤー用の Player.gd がどんどん巨大化する
  • state == STATE_GROUND_POUND みたいなフラグと分岐が増え続ける
  • 敵や動く床にも同じような処理を入れたくなった時、コピペ地獄になる

Godot はノードの継承が簡単なので「ヒップドロップ対応プレイヤー」「ヒップドロップ対応敵」みたいなシーンを増やしがちですが、そうするとシーンツリーもスクリプトもすぐにスパゲッティ化します。

そこで今回の記事では、どのキャラクターにもポン付けできる「GroundPound コンポーネント」を作ってみましょう。
「空中で下入力 → 急降下 → 着地時に衝撃波判定を出す」という一連の処理を、1つのコンポーネントにまとめて合成(Composition)するアプローチです。


【Godot 4】空中からドスン!汎用ヒップドロップ「GroundPound」コンポーネント

このコンポーネントは、親に CharacterBody2D がいる前提で動きます。
プレイヤーでも敵でも、「地面の上を走ってジャンプするキャラ」であれば、どこにでもアタッチできます。

コンポーネントの仕様

  • 空中で「下入力」が入った瞬間にヒップドロップ開始
  • ヒップドロップ中は、垂直速度を一定値(急降下速度)に固定
  • 着地した瞬間に、足元に「衝撃波当たり判定(Area2D)」を一時的に出す
  • 衝撃波の半径、ダメージ、クールダウン時間などを @export で調整可能
  • 親側には、必要であれば on_ground_pound_landed() シグナル(メソッド)で通知

ソースコード (Full Code)


extends Node
class_name GroundPound
## 汎用ヒップドロップ(グラウンドパンチ)コンポーネント
## 親に CharacterBody2D を想定しているコンポーネントです。
## 空中で下入力を検知して急降下し、着地時に衝撃波判定を出します。

signal ground_pound_started
signal ground_pound_landed
signal ground_pound_cooldown_finished

@export_group("Input")
## 下入力を検知するアクション名(InputMap で設定しておく)
@export var down_action: StringName = &"move_down"
## 任意の「ヒップドロップ開始」アクション(ボタン併用したい場合)
@export var ground_pound_action: StringName = &"ground_pound"
## ボタンも必須にするか? true なら「下入力 AND ボタン」で発動
@export var require_button: bool = false

@export_group("Ground Pound Settings")
## ヒップドロップ中の垂直速度(マイナスが上、プラスが下なので正の値)
@export var dive_speed: float = 1500.0
## 発動前に最低限必要な落下速度(ふわっとしたジャンプ直後には発動させない等)
@export var min_fall_speed_to_activate: float = 50.0
## 一度着地したあと、再度ヒップドロップできるまでのクールダウン時間
@export var cooldown_time: float = 0.5
## 着地時に親の速度をゼロにするか(バウンドさせたくない場合 true)
@export var stop_velocity_on_land: bool = true

@export_group("Shockwave")
## 衝撃波の半径(Area2D + CircleShape2D の半径)
@export var shockwave_radius: float = 64.0
## 衝撃波が有効な時間(秒)
@export var shockwave_duration: float = 0.1
## 衝撃波のダメージ量(敵側の処理で参照してもらう用)
@export var shockwave_damage: int = 1
## 衝撃波のコリジョンレイヤー
@export var shockwave_collision_layer: int = 1
## 衝撃波のコリジョンマスク
@export var shockwave_collision_mask: int = 1
## 衝撃波の位置オフセット(足元を少し下げたい場合など)
@export var shockwave_offset: Vector2 = Vector2(0, 8)

@export_group("Debug")
## デバッグ用: 現在状態をエディタで確認したい時用
@export var debug_state: String = "" setget _set_debug_state

# 内部状態
enum State {
	IDLE,
	FALLING,
	DIVING,
	COOLDOWN,
}

var _state: State = State.IDLE
var _body: CharacterBody2D
var _cooldown_timer: float = 0.0

func _ready() -> void:
	# 親は CharacterBody2D を想定
	_body = get_parent() as CharacterBody2D
	if _body == null:
		push_warning("GroundPound: parent is not a CharacterBody2D. This component will be disabled.")
		set_process(false)
		return

	set_process(true)


func _process(delta: float) -> void:
	if _body == null:
		return

	_update_state(delta)
	_handle_cooldown(delta)
	_update_debug_state()


func _update_state(delta: float) -> void:
	match _state:
		State.IDLE:
			# 地上にいる間 or 上昇中など
			if not _body.is_on_floor() and _body.velocity.y > 0.0:
				_state = State.FALLING

		State.FALLING:
			# 地上に戻ったら IDLE
			if _body.is_on_floor():
				_state = State.IDLE
				return

			# 一定以上落下していて、入力条件を満たしたらヒップドロップ開始
			if _can_start_ground_pound():
				_start_ground_pound()

		State.DIVING:
			# ヒップドロップ中は縦速度を固定して真下に落ちる
			_body.velocity.y = dive_speed

			# 着地判定
			if _body.is_on_floor():
				_on_landed()

		State.COOLDOWN:
			# クールダウン中でも通常の移動は親が制御してOK
			# ここではクールダウン時間の管理だけ
			pass


func _handle_cooldown(delta: float) -> void:
	if _state == State.COOLDOWN:
		_cooldown_timer -= delta
		if _cooldown_timer <= 0.0:
			_state = State.IDLE
			_cooldown_timer = 0.0
			emit_signal("ground_pound_cooldown_finished")


func _can_start_ground_pound() -> bool:
	# 既にダイブ中 or クールダウン中ならNG
	if _state == State.DIVING or _state == State.COOLDOWN:
		return false

	# 空中で、かつ下向きにある程度落下していること
	if _body.is_on_floor():
		return false
	if _body.velocity.y < min_fall_speed_to_activate:
		return false

	# 入力条件チェック
	var down_pressed := Input.is_action_pressed(down_action)
	var button_ok := true
	if require_button:
		button_ok = Input.is_action_just_pressed(ground_pound_action)

	return down_pressed and button_ok


func _start_ground_pound() -> void:
	_state = State.DIVING
	emit_signal("ground_pound_started")


func _on_landed() -> void:
	# 着地時の処理
	if stop_velocity_on_land:
		_body.velocity = Vector2.ZERO

	# 衝撃波を生成
	_spawn_shockwave()

	_state = State.COOLDOWN
	_cooldown_timer = cooldown_time
	emit_signal("ground_pound_landed")

	# 親に「着地したよ」と伝えたい場合は、メソッドがあれば呼ぶ
	if _body.has_method("on_ground_pound_landed"):
		_body.call_deferred("on_ground_pound_landed")


func _spawn_shockwave() -> void:
	# 一時的な Area2D を生成して衝撃波判定を作る
	var area := Area2D.new()
	area.collision_layer = shockwave_collision_layer
	area.collision_mask = shockwave_collision_mask

	var shape := CollisionShape2D.new()
	var circle := CircleShape2D.new()
	circle.radius = shockwave_radius
	shape.shape = circle
	shape.position = shockwave_offset

	area.add_child(shape)
	add_child(area) # GroundPound の子にぶら下げる(親キャラのローカル座標系になる)

	# 衝撃波が当たった相手にダメージを通知する例
	# 相手側に「apply_damage(amount, source)」があれば呼び出す
	area.body_entered.connect(func(body: Node) -> void:
		if body.has_method("apply_damage"):
			body.call("apply_damage", shockwave_damage, _body)
	)

	# 一定時間後に自壊
	var timer := get_tree().create_timer(shockwave_duration)
	timer.timeout.connect(func() -> void:
		if is_instance_valid(area):
			area.queue_free()
	)


func _set_debug_state(value: String) -> void:
	# 外部から書き換えられないように無視
	pass


func _update_debug_state() -> void:
	match _state:
		State.IDLE:
			debug_state = "IDLE"
		State.FALLING:
			debug_state = "FALLING"
		State.DIVING:
			debug_state = "DIVING"
		State.COOLDOWN:
			debug_state = "COOLDOWN"

使い方の手順

① 入力マップの設定

まずは InputMap にアクションを登録します。

  1. Project > Project Settings > Input Map を開く
  2. move_down(デフォルトで使う前提)を追加して、下キー / S キー / スティック下 などを割り当てる
  3. もし「専用ボタン+下入力」で発動したい場合は、ground_pound も追加してボタンを割り当てる

コンポーネント側の @export でアクション名は変えられるので、プロジェクトの命名規約に合わせて調整してください。

② プレイヤーシーンに GroundPound をアタッチ

典型的な 2D アクションのプレイヤー構成はこんな感じだと思います。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── GroundPound (Node)
  1. Player シーンを開く
  2. Player(CharacterBody2D)を右クリック > 「子ノードを追加」
  3. Node を追加し、名前を GroundPound に変更
  4. その Node に上記の GroundPound.gd をアタッチ

これでプレイヤー側の移動処理はそのままに、GroundPound コンポーネントだけでヒップドロップを合成できます。

③ パラメータ調整と簡単な連携

GroundPound ノードを選択すると、インスペクタに以下のようなパラメータが出てきます。

  • dive_speed – どれくらいの速さで落下するか(大きいほどドスン感UP)
  • min_fall_speed_to_activate – どれくらい落下してからでないと発動しないか
  • shockwave_radius – 足元の衝撃波の届く範囲
  • shockwave_damage – 当たった相手に渡すダメージ値
  • cooldown_time – 連続で連打させたくない場合に調整

例えば、プレイヤー側で着地演出を入れたい場合は、Player.gd にこんなメソッドを追加しておきます。


func on_ground_pound_landed() -> void:
	# カメラシェイクやパーティクル再生など
	print("Ground pound landed!")

GroundPound コンポーネントは、親にこのメソッドがあれば自動で呼んでくれるので、コンポーネント側とプレイヤー側の依存がゆるくて済むのがポイントですね。

④ 敵や動く床にもそのまま流用

同じコンポーネントを、敵キャラや動く床にもそのまま付けられます。

敵キャラの例:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── GroundPound (Node)

敵の AI は EnemyAI.gd など別スクリプトで書いておいて、
「一定条件で Input を偽装する」か「GroundPound に API を追加して外部から発動させる」といった形で連携できます。

例えば、敵が一定高度以上から落ちている時だけヒップドロップさせたい場合、
AI スクリプトから GroundPound ノードを取得して、後述の改造案のように start_ground_pound() を呼び出す、などが考えられます。


メリットと応用

この GroundPound コンポーネントを使う最大のメリットは、プレイヤーや敵のスクリプトを「ヒップドロップの実装詳細」から解放できることです。

  • プレイヤーの Player.gd は「移動・ジャンプ・入力受付」などのコアロジックだけに集中できる
  • ヒップドロップの挙動(速度・当たり判定・クールダウン)はコンポーネント側に完結
  • 別キャラに同じアクションを追加したくなったら、GroundPound ノードをコピペしてパラメータ調整するだけ
  • 「衝撃波のダメージ処理」も apply_damage() を用意しておけば、敵・ギミック・壊れるブロックなどどこでも再利用可能

シーン構造も、Godot でありがちな「深いノード階層」「肥大化した親クラスの継承地獄」を避けて、

CharacterBody2D
 ├── Visual系
 ├── Collision系
 ├── Movementコンポーネント
 ├── GroundPoundコンポーネント
 └── その他アクションコンポーネント

のように横にコンポーネントを並べていくスタイルにできます。
これがまさに「継承より合成」のおいしいところですね。

簡単な改造案:外部から強制発動できる API を追加する

敵 AI やスクリプトから「今すぐヒップドロップさせたい!」というケース向けに、
GroundPound に公開メソッドを 1 つ追加しておくと便利です。


## 外部から強制的にヒップドロップを開始させる API
func force_start_ground_pound() -> void:
	# 状態チェックだけ軽くして、後は内部の開始処理を使い回す
	if _state == State.DIVING or _state == State.COOLDOWN:
		return
	if _body.is_on_floor():
		return

	_start_ground_pound()

これを入れておけば、敵側のスクリプトから


var gp := $GroundPound
if gp:
	gp.force_start_ground_pound()

と呼ぶだけで、AI の判断に応じたヒップドロップが簡単に実装できます。
入力依存のロジックと AI 依存のロジックをきれいに分離できるので、コンポーネント指向の設計とも相性がいいですね。