Godotでちょっと大きめのステージやオープンワールドっぽい構成を作り始めると、こんな悩みが出てきますよね。
- 遠くにいる敵やギミックまで 常に物理演算&_process が走っていて、なんとなく重そう…
- 「プレイヤーが近づいたら動き出す敵」を毎回
_ready()や_process()にベタ書きしていて、シーンごとにコピペ地獄 - 敵ごとにプレイヤー探索のロジックを書いていて、共通化できていない
- シーン階層が「PlayerDetector」「ActivationArea」みたいな補助ノードだらけで深くなりがち
Godot標準でも VisibilityNotifier2D や VisibleOnScreenNotifier2D を使えばある程度は制御できますが、「プレイヤーとの距離」をベースにしたアクティベーションをきれいに共通化しようとすると、結局各シーンに似たようなコードを書きがちです。
そこで今回は、「プレイヤーが一定距離に近づくまで、親の処理や物理演算を止めておく」ためのコンポーネント、DistanceActivator を用意しました。敵でも動く床でも、親ノードにペタっと付けるだけで、距離ベースのオンデマンド起動が実現できます。
【Godot 4】距離でオンデマンド起動!「DistanceActivator」コンポーネント
DistanceActivator は、親ノードを「スリープ状態」にしておき、プレイヤーが指定距離以内に入ったら 自動で有効化 してくれるコンポーネントです。
- 対象: 2D/3Dどちらでも利用可能(距離計算はベクトル長)
- 親ノードの
process_modeやset_physics_process()をまとめて制御 - 「一度起動したら止めない」か「離れたらまたスリープさせる」かを選択可能
- プレイヤーの取得方法も柔軟に設定(グループ / NodePath / 自動探索)
「敵AI」「動く足場」「パーティクルの発生源」「遠くのギミック」などにアタッチしておくと、プレイヤー周辺だけがアクティブな軽いワールドを作れます。
フルコード: DistanceActivator.gd
extends Node
class_name DistanceActivator
## 距離で親ノードの処理・物理演算をON/OFFするコンポーネント
##
## 親ノード(owner)を「スリープ状態」にしておき、
## プレイヤーが一定距離に近づいたら有効化します。
## Godot 4.x 用。
@export_category("Activation Settings")
## プレイヤーと親ノードの距離がこの値以下になったら「有効化」する
@export var activate_distance: float = 600.0
## プレイヤーと親ノードの距離がこの値以上になったら「無効化」する
## use_deactivation が false の場合は無視されます
@export var deactivate_distance: float = 800.0
## 距離判定の頻度(秒)。値を大きくすると軽くなるが反応が遅くなる
@export_range(0.01, 5.0, 0.01)
@export var check_interval: float = 0.2
## 一度有効化したら、もう二度と無効化しないかどうか
@export var activate_once: bool = true
## 離れたらまたスリープさせるかどうか
@export var use_deactivation: bool = true
@export_category("Player Detection")
## プレイヤーを自動で探すかどうか
## false の場合、player_path か player_group_name で指定する
@export var auto_find_player: bool = true
## プレイヤーの NodePath を直接指定したい場合に使用
@export var player_path: NodePath
## プレイヤーが所属しているグループ名
## 例: "player" としておき、シーン側で Player をそのグループに入れておく
@export var player_group_name: StringName = &"player"
## 2D / 3D を自動判定するかどうか
## 通常は true でOK。false にして is_3d を明示的に指定することも可能。
@export var auto_detect_dimension: bool = true
## 2D か 3D かを強制指定したい場合に使用(auto_detect_dimension = false のとき)
@export var is_3d: bool = false
@export_category("Debug")
## 現在の状態をエディタ上で確認したいとき用
@export var debug_print_state_change: bool = false
## 内部状態
var _player: Node3D: set = _set_player
var _player_2d: Node2D: set = _set_player_2d
var _is_active: bool = true
var _time_accum: float = 0.0
var _resolved_is_3d: bool = false
func _ready() -> void:
# 親ノード(このコンポーネントをアタッチしたノード)を取得
if owner == null:
push_warning("DistanceActivator: owner が存在しません。このノードはシーンの直下に置かないでください。")
return
# 2D / 3D の自動判定
if auto_detect_dimension:
_resolved_is_3d = owner is Node3D
else:
_resolved_is_3d = is_3d
# プレイヤーを探す
_find_player()
# 初期状態として「スリープ状態」にしておく
_set_active(false, force := true)
func _process(delta: float) -> void:
if owner == null:
return
_time_accum += delta
if _time_accum < check_interval:
return
_time_accum = 0.0
var player_pos: Vector3
var self_pos: Vector3
if _resolved_is_3d:
if _player == null:
return
player_pos = _player.global_position
self_pos = (owner as Node3D).global_position
else:
if _player_2d == null:
return
var p2: Node2D = _player_2d
var s2: Node2D = owner as Node2D
player_pos = Vector3(p2.global_position.x, p2.global_position.y, 0.0)
self_pos = Vector3(s2.global_position.x, s2.global_position.y, 0.0)
var dist: float = self_pos.distance_to(player_pos)
if not _is_active and dist <= activate_distance:
# まだ非アクティブで、距離が閾値以下になったら有効化
_set_active(true)
elif _is_active and use_deactivation and not activate_once and dist >= deactivate_distance:
# アクティブ状態で、一定距離以上離れたら再びスリープ
_set_active(false)
func _set_active(active: bool, force: bool = false) -> void:
if not force and _is_active == active:
return
_is_active = active
# 親ノードの処理モードを変更
# Node.PROCESS_MODE_DISABLED にすると _process/_physics_process が呼ばれなくなります
if owner is Node:
var node_owner: Node = owner
node_owner.process_mode = active \
? Node.PROCESS_MODE_INHERIT \
: Node.PROCESS_MODE_DISABLED
# 物理ボディをまとめて有効/無効にする(2D/3D 両対応)
_set_physics_enabled(owner, active)
if debug_print_state_change:
var state_text := active ? "ACTIVATED" : "DEACTIVATED"
print("DistanceActivator: %s - owner=%s" % [state_text, owner])
func _set_physics_enabled(node: Node, enabled: bool) -> void:
# 再帰的に子孫ノードを巡回して、物理ボディやアニメーション等を止めることも可能
# 今回はシンプルに親ノード自身に対してのみ行う
if node is PhysicsBody2D:
(node as PhysicsBody2D).set_physics_process(enabled)
elif node is CharacterBody2D:
(node as CharacterBody2D).set_physics_process(enabled)
elif node is RigidBody2D:
(node as RigidBody2D).set_physics_process(enabled)
elif node is PhysicsBody3D:
(node as PhysicsBody3D).set_physics_process(enabled)
elif node is CharacterBody3D:
(node as CharacterBody3D).set_physics_process(enabled)
elif node is RigidBody3D:
(node as RigidBody3D).set_physics_process(enabled)
# 必要に応じて、AnimationPlayer や Particles などもここで制御可能
func _find_player() -> void:
if not auto_find_player:
# 手動指定モード
if player_path != NodePath():
var node := get_node_or_null(player_path)
if node is Node3D:
_player = node
elif node is Node2D:
_player_2d = node
elif player_group_name != StringName():
# グループから検索
var candidates := get_tree().get_nodes_in_group(player_group_name)
for c in candidates:
if _resolved_is_3d and c is Node3D:
_player = c
break
elif not _resolved_is_3d and c is Node2D:
_player_2d = c
break
return
# auto_find_player = true の場合、グループ優先で自動検索
if player_group_name != StringName():
var nodes := get_tree().get_nodes_in_group(player_group_name)
for n in nodes:
if _resolved_is_3d and n is Node3D:
_player = n
return
elif not _resolved_is_3d and n is Node2D:
_player_2d = n
return
# グループが見つからない場合、シーンツリー全体からそれっぽい名前を探す(保険)
var root := get_tree().root
var candidate := root.find_child("Player", true, false)
if candidate:
if _resolved_is_3d and candidate is Node3D:
_player = candidate
elif not _resolved_is_3d and candidate is Node2D:
_player_2d = candidate
func _set_player(value) -> void:
_player = value
func _set_player_2d(value) -> void:
_player_2d = value
使い方の手順
ここからは、2Dゲームの例(プレイヤーが近づくまで眠っている敵)を題材に、実際の使い方を見ていきましょう。
手順①: スクリプトを用意する
res://components/DistanceActivator.gdのようなパスで、上記のコードを保存します。- Godot エディタを再読み込みすると、ノード追加ダイアログの「スクリプト」タブなどから
DistanceActivatorが補完されるようになります(class_nameのおかげ)。
手順②: プレイヤーにグループを設定する
プレイヤーシーン例:
Player (CharacterBody2D) ├── Sprite2D └── CollisionShape2D
- Player ノードを選択し、インスペクタ右の「ノード」タブ → 「グループ」タブを開きます。
playerというグループ名を追加します。- これで
DistanceActivatorが自動でプレイヤーを検出できるようになります(デフォルトでplayer_group_name = "player")。
手順③: 敵に DistanceActivator をアタッチする
敵シーン例:
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── DistanceActivator (Node)
- Enemy シーンを開き、Enemy(親ノード) を選択した状態で右クリック → 「子ノードを追加」。
Nodeを追加し、名前をDistanceActivatorに変更。- そのノードに先ほどの
DistanceActivator.gdスクリプトをアタッチします。 - インスペクタで次のように設定してみましょう:
activate_distance = 500.0deactivate_distance = 650.0check_interval = 0.2activate_once = false(近づいたら起動・離れたら停止を繰り返す)use_deactivation = trueauto_find_player = true(デフォルトのまま)
これで、プレイヤーが 500px 以内に来るまで、Enemy の _process / _physics_process は止まったままになります。
遠くで待機している敵が大量にいても、実際に負荷がかかるのはプレイヤーの近くの敵だけになります。
手順④: 他のオブジェクトにも使い回す
同じコンポーネントを、例えば「動く床」にも付けてみましょう。
MovingPlatform (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── DistanceActivator (Node)
- MovingPlatform のスクリプトでは、普段通り
_physics_processなどで移動ロジックを書きます。 DistanceActivatorがprocess_modeと物理処理を OFF にしてくれるので、プレイヤーが近づくまで完全に停止します。- レベルの奥に大量の足場を配置しても、距離ベースでオンデマンドに動き出すので、パフォーマンスと管理がかなり楽になります。
3D の場合も考え方は同じで、親ノードが Node3D/CharacterBody3D になり、距離計算が 3D ベクトルになるだけです。
Enemy3D (CharacterBody3D) ├── MeshInstance3D ├── CollisionShape3D └── DistanceActivator (Node)
この構成にしておけば、敵AIのスクリプト自体は「常に動いている前提」で書いてOK で、起動/停止の責務は DistanceActivator に丸投げできます。
メリットと応用
このコンポーネントを使うメリットは、単に「ちょっと軽くなる」だけではありません。
- シーン構造がシンプル
敵やギミックは「本体ノード + Sprite + Collider + DistanceActivator」程度で済み、
「検知用の Area2D を別途置く」みたいな深い階層が不要になります。 - ロジックの再利用性が高い
「距離で起動する」という仕組みはコンポーネント側に閉じ込めているので、
敵AIやギミックのスクリプトは「いつも通り動くコード」のままでOKです。 - レベルデザインが楽
ステージ上に敵やギミックをポンポン配置しても、
「遠くのやつ、ちゃんと止めてるかな?」と心配する必要がありません。 - コンポーネント指向の恩恵
「DistanceActivator を付けるかどうか」で挙動を切り替えられるので、
継承でクラスを分ける必要がなく、合成(Composition)で機能を足し引きできます。
応用として、例えば「起動時に一回だけエフェクトを出す」「無効化時にアニメーションを止める」などを追加したくなると思います。その場合は、_set_active にフックを追加するのもいいですが、よりコンポーネント指向にするなら、シグナルを生やして他のコンポーネントから反応するのがおすすめです。
例えば、こんな改造をしてみると便利です:
signal activated
signal deactivated
func _set_active(active: bool, force: bool = false) -> void:
if not force and _is_active == active:
return
_is_active = active
if owner is Node:
var node_owner: Node = owner
node_owner.process_mode = active \
? Node.PROCESS_MODE_INHERIT \
: Node.PROCESS_MODE_DISABLED
_set_physics_enabled(owner, active)
if active:
activated.emit()
else:
deactivated.emit()
こうしておけば、別の「EffectOnActivate」コンポーネントを作って DistanceActivator.activated に接続し、起動時だけパーティクルを出す…といった 合成ベースの拡張がどんどんやりやすくなります。
継承で「DistanceActivatingEnemy」「DistanceActivatingPlatform」みたいなクラスを増やしていくより、
「Enemy + DistanceActivator + EffectOnActivate」といった組み合わせで構成していく方が、長期的にメンテしやすいですね。
