【Godot 4】KillZ (落下死ライン) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

2D/3Dどちらでもアクションゲームを作っていると、「奈落に落ちたら即死」みたいな処理、ほぼ必須ですよね。
Godotの教科書的な実装だと、こんな感じになりがちです。

  • プレイヤーのスクリプトに if global_position.y > 1000: die() を直書き
  • 敵キャラにも同じようなコードをコピペ
  • ステージごとに「このステージは -2000 で落下死」みたいな魔法の数字が散乱

これ、最初はシンプルなんですが、

  • プレイヤーと敵で「落下死の閾値」が違ってバグる
  • 奈落ラインをステージごとに変えたいとき、全部のスクリプトを書き換えるハメになる
  • 「このオブジェクトは落下しても死なない」みたいな例外処理でスクリプトが汚れる

と、だんだんカオスになっていきます。

そこで今回は、「落下死」という概念をキャラ本体から切り離して、どのノードにもポン付けできるコンポーネントとして実装してみましょう。
その名も 「KillZ」コンポーネント。Y座標が一定以下になったら、HPが何であろうと即死させます。


【Godot 4】奈落はコンポーネントに任せよう!「KillZ」コンポーネント

今回の方針はシンプルです。

  • 「落下死ライン」はステージごとに 1 つだけ置く
  • 「落下死の対象になるかどうか」は各キャラに KillZ コンポーネントを付けるだけで決まる
  • ダメージシステムやHP管理とは疎結合にして、「kill() さえ用意しておけば勝手に即死してくれる」

つまり、継承で「KillableCharacter」みたいな巨大ベースクラスを作るのではなく、
「落下死」という単機能をコンポーネントに分離して、必要なノードにだけアタッチする設計ですね。


フルコード:KillZ.gd


extends Node
class_name KillZ
## 落下死コンポーネント
## 親ノードの Y 座標が一定以下になったら、即死処理を呼び出します。
##
## 想定する親ノード:
## - CharacterBody2D / CharacterBody3D
## - RigidBody2D / RigidBody3D
## - Node2D / Node3D など、global_position を持つノード
##
## 即死処理の呼び出し優先順位:
## 1. 親に "kill()" メソッドがあれば呼び出す
## 2. なければ "die()" メソッドを探して呼び出す
## 3. それもなければ、オプションで queue_free() する

@export_category("KillZ Settings")

@export var kill_y: float = 1000.0:
	set(value):
		kill_y = value
		_update_debug_line()

## Z ではなく Y 軸で判定するかどうか(2Dなら true 推奨)
## 3D で「高さ」で判定したい場合は true のまま Y を使い、
## 3D で「奥行き」で判定したいなら false にして Z を使う、などアレンジ可能。
@export var use_y_axis: bool = true

## 落下死判定を一時的に無効化したいときに使います(デバッグ・イベント用)。
@export var enabled: bool = true

## 親に kill()/die() が無かった場合、queue_free() するかどうか。
## false の場合は何もせず警告ログのみ出します。
@export var fallback_queue_free: bool = true

## デバッグ用に、エディタ上で「Kill ライン」を表示するかどうか
@export var show_debug_line: bool = true:
	set(value):
		show_debug_line = value
		_update_debug_line()

## デバッグラインの色
@export var debug_color: Color = Color(1, 0, 0, 0.5):
	set(value):
		debug_color = value
		_update_debug_line()

## デバッグラインの長さ(2D/3Dで意味合いが少し違います)
@export var debug_line_length: float = 5000.0:
	set(value):
		debug_line_length = max(0.0, value)
		_update_debug_line()

## 2D/3D どちらでも使えるように、内部的に Node2D/Node3D を探して描画します
var _debug_node: Node = null


func _ready() -> void:
	# 親を持っていることを前提とします
	if not get_parent():
		push_warning("KillZ コンポーネントは、必ず何かの子ノードとして配置してください。")
	
	_update_debug_line()


func _physics_process(delta: float) -> void:
	if not enabled:
		return
	
	var target := get_parent()
	if target == null:
		return
	
	# 2D / 3D 両対応のために、Dynamic に座標を取得する
	var global_pos := _get_global_position(target)
	if global_pos == null:
		return
	
	# 判定軸を決定
	var axis_value := global_pos.y if use_y_axis else global_pos.z if "z" in global_pos else global_pos.y
	
	if axis_value > kill_y:
		# 閾値を超えたので即死処理
		_execute_kill(target)


func _execute_kill(target: Node) -> void:
	# 二重に呼ばれないように、自分を無効化
	enabled = false
	
	# 1. kill() メソッドがあればそれを使う
	if target.has_method("kill"):
		target.call("kill")
		return
	
	# 2. die() メソッドがあればそれを使う
	if target.has_method("die"):
		target.call("die")
		return
	
	# 3. フォールバックとして queue_free()
	if fallback_queue_free:
		target.queue_free()
	else:
		push_warning("KillZ: 親ノードに kill()/die() が見つかりませんでした。fallback_queue_free=false のため、何もしません。")


func _get_global_position(target: Node) -> Variant:
	# 2D 系
	if target is Node2D:
		return (target as Node2D).global_position
	# 3D 系
	if target is Node3D:
		return (target as Node3D).global_position
	# それ以外(Position3D, Marker2D など)にも対応したければここで拡張
	if "global_position" in target:
		return target.global_position
	
	push_warning("KillZ: 親ノードが global_position を持っていないため、落下判定ができません。")
	return null


func _update_debug_line() -> void:
	if not is_inside_tree():
		return
	
	# 既存のデバッグノードを削除
	if _debug_node and is_instance_valid(_debug_node):
		_debug_node.queue_free()
		_debug_node = null
	
	if not show_debug_line:
		return
	
	# 親が 2D か 3D かによって、描画ノードを変える
	var parent := get_parent()
	if parent == null:
		return
	
	if parent is Node2D:
		_debug_node = _create_debug_line_2d()
		parent.add_child(_debug_node)
		_debug_node.owner = get_tree().edited_scene_root
	elif parent is Node3D:
		_debug_node = _create_debug_line_3d()
		parent.add_child(_debug_node)
		_debug_node.owner = get_tree().edited_scene_root
	else:
		# どちらでもない場合は、特に何もしない(ログだけ出す)
		push_warning("KillZ: 親が Node2D/Node3D ではないため、デバッグラインは描画されません。")


func _create_debug_line_2d() -> Node2D:
	var line := Node2D.new()
	line.name = "KillZ_DebugLine2D"
	
	line.set_script(_get_debug_line_2d_script())
	line.set("kill_y", kill_y)
	line.set("debug_color", debug_color)
	line.set("debug_line_length", debug_line_length)
	
	return line


func _create_debug_line_3d() -> Node3D:
	var line := Node3D.new()
	line.name = "KillZ_DebugLine3D"
	
	line.set_script(_get_debug_line_3d_script())
	line.set("kill_y", kill_y)
	line.set("debug_color", debug_color)
	line.set("debug_line_length", debug_line_length)
	
	return line


func _get_debug_line_2d_script() -> Script:
	var src := '''
extends Node2D

@export var kill_y: float = 1000.0
@export var debug_color: Color = Color(1, 0, 0, 0.5)
@export var debug_line_length: float = 5000.0

func _draw() -> void:
	var half_len := debug_line_length * 0.5
	var from := Vector2(-half_len, kill_y)
	var to := Vector2(half_len, kill_y)
	draw_line(from, to, debug_color, 2.0)

func _notification(what: int) -> void:
	if what == NOTIFICATION_TRANSFORM_CHANGED or what == NOTIFICATION_VISIBILITY_CHANGED:
		queue_redraw()
'''
	return GDScript.new().from_source_code(src)


func _get_debug_line_3d_script() -> Script:
	var src := '''
extends Node3D

@export var kill_y: float = 1000.0
@export var debug_color: Color = Color(1, 0, 0, 0.5)
@export var debug_line_length: float = 5000.0

func _ready() -> void:
	# 単純な PlaneMesh を Y=kill_y の位置に置く
	var mesh_instance := MeshInstance3D.new()
	var plane := PlaneMesh.new()
	plane.size = Vector2(debug_line_length, debug_line_length)
	mesh_instance.mesh = plane
	
	var mat := StandardMaterial3D.new()
	mat.albedo_color = debug_color
	mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
	mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
	mesh_instance.material_override = mat
	
	add_child(mesh_instance)
	mesh_instance.global_position.y = kill_y
'''
	return GDScript.new().from_source_code(src)

使い方の手順

ここでは 2D アクションゲームの例で説明しますが、3D でもほぼ同じです。

例1: プレイヤーキャラに落下死を付ける

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── KillZ (Node)
  1. KillZ.gd を用意
    上記のコードを res://components/KillZ.gd などに保存します。
  2. Player シーンを開く
    ルートが CharacterBody2D のプレイヤーシーンを想定します。
  3. 子ノードとして KillZ を追加
    • Player の子に Node を追加
    • そのノードに KillZ.gd をアタッチ
    • ノード名を KillZ にしておくと分かりやすいです
  4. プレイヤーに kill() を実装
    Player のスクリプトに、落下死時の処理を書きます。

# Player.gd(抜粋)
extends CharacterBody2D

var is_dead: bool = false

func kill() -> void:
	if is_dead:
		return
	is_dead = true
	
	# アニメーション再生や SE 再生など
	print("Player: 落下死しました")
	
	# ここでは単純にシーンをリスタート
	get_tree().reload_current_scene()

あとは、KillZ コンポーネントのインスペクタで kill_y を調整すれば、
その高さより下に落ちたときに、自動的に Player.kill() が呼ばれます。

例2: 敵キャラにも同じコンポーネントを再利用

EnemyGoblin (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── KillZ (Node)

敵シーンにもまったく同じように KillZ を子ノードとして追加し、
敵スクリプト側に kill()die() を実装するだけです。


# EnemyGoblin.gd(抜粋)
extends CharacterBody2D

func die() -> void:
	print("ゴブリンが奈落に消えた…")
	queue_free()

プレイヤーは kill()、敵は die() と名前が違っていても、
KillZ コンポーネントが両方探してくれるので、そのまま動きます。

例3: 動く足場も奈落で消したい場合

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── KillZ (Node)

# MovingPlatform.gd(抜粋)
extends Node2D

func kill() -> void:
	# 落下したら消えるだけで OK
	queue_free()

これで、プレイヤー・敵・動く床など、どのシーンにも同じコンポーネントをアタッチするだけで奈落処理が統一できます。


メリットと応用

この KillZ コンポーネントを使うメリットを整理してみましょう。

  • シーン構造がスッキリする
    「落下死」のロジックがキャラ本体のスクリプトから分離されるので、
    プレイヤーのコードは「移動」「攻撃」「入力処理」に集中できます。
  • 使い回しが圧倒的に楽
    新しい敵キャラを作るときも、KillZ を子ノードとして追加するだけ。
    「この敵は奈落で死なない」みたいな仕様なら、単に KillZ を付けなければいいだけです。
  • レベルデザインに強い
    ステージごとに kill_y を変えたい場合も、
    シーンインスタンスごとにパラメータを変えるだけで対応できます。
    「地下ステージは -3000 まで落ちてもセーフ」みたいな調整も簡単ですね。
  • HP システムと疎結合
    KillZ は「HP を 0 にする」のではなく、「kill()/die() を呼ぶ」だけ。
    その先で HP を減らすのか、アニメーションを再生するのか、リスポーンするのかは、
    各キャラ側の責務として分離されています。

「継承より合成」の観点で見ると、

  • 巨大な BaseCharacter クラスに「落下死」「HP」「移動」など全部入りにするのではなく
  • KillZ のような小さなコンポーネントを組み合わせて機能を構成する

という設計になるので、長期的に見てかなり保守が楽になります。

改造案:シグナルで「落下死イベント」を飛ばす

「死ぬ前に UI にエフェクトを出したい」「統計用にカウンタを増やしたい」など、
もう少し柔軟にフックしたい場合は、signal を追加するのがおすすめです。


signal fell_below_kill_line(target: Node, axis_value: float)

func _execute_kill(target: Node) -> void:
	enabled = false
	
	# シグナルで事前に通知
	var axis_value := _get_global_position(target).y
	fell_below_kill_line.emit(target, axis_value)
	
	if target.has_method("kill"):
		target.call("kill")
		return
	
	if target.has_method("die"):
		target.call("die")
		return
	
	if fallback_queue_free:
		target.queue_free()

これで、ゲーム全体のマネージャーが KillZ.fell_below_kill_line を接続しておけば、
「何かが奈落に落ちた瞬間」にスコアを増やしたり、ログを残したりできます。
コンポーネントの責務は「落下死を検知して通知・実行する」とだけ決めておくと、
あとからの拡張もしやすいですね。

こんな感じで、「落下死」みたいな一見どこにでも書けそうな処理ほど、
コンポーネントに切り出しておくと、プロジェクトが大きくなったときに効いてきます。
ぜひ、自分のプロジェクトでも KillZ を試してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!