Godotでちょっと大きめのステージやオープンワールドっぽい構成を作り始めると、こんな悩みが出てきますよね。

  • 遠くにいる敵やギミックまで 常に物理演算&_process が走っていて、なんとなく重そう…
  • 「プレイヤーが近づいたら動き出す敵」を毎回 _ready()_process() にベタ書きしていて、シーンごとにコピペ地獄
  • 敵ごとにプレイヤー探索のロジックを書いていて、共通化できていない
  • シーン階層が「PlayerDetector」「ActivationArea」みたいな補助ノードだらけで深くなりがち

Godot標準でも VisibilityNotifier2DVisibleOnScreenNotifier2D を使えばある程度は制御できますが、「プレイヤーとの距離」をベースにしたアクティベーションをきれいに共通化しようとすると、結局各シーンに似たようなコードを書きがちです。

そこで今回は、「プレイヤーが一定距離に近づくまで、親の処理や物理演算を止めておく」ためのコンポーネント、DistanceActivator を用意しました。敵でも動く床でも、親ノードにペタっと付けるだけで、距離ベースのオンデマンド起動が実現できます。

【Godot 4】距離でオンデマンド起動!「DistanceActivator」コンポーネント

DistanceActivator は、親ノードを「スリープ状態」にしておき、プレイヤーが指定距離以内に入ったら 自動で有効化 してくれるコンポーネントです。

  • 対象: 2D/3Dどちらでも利用可能(距離計算はベクトル長)
  • 親ノードの process_modeset_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ゲームの例(プレイヤーが近づくまで眠っている敵)を題材に、実際の使い方を見ていきましょう。

手順①: スクリプトを用意する

  1. res://components/DistanceActivator.gd のようなパスで、上記のコードを保存します。
  2. Godot エディタを再読み込みすると、ノード追加ダイアログの「スクリプト」タブなどから DistanceActivator が補完されるようになります(class_name のおかげ)。

手順②: プレイヤーにグループを設定する

プレイヤーシーン例:

Player (CharacterBody2D)
 ├── Sprite2D
 └── CollisionShape2D
  1. Player ノードを選択し、インスペクタ右の「ノード」タブ → 「グループ」タブを開きます。
  2. player というグループ名を追加します。
  3. これで DistanceActivator が自動でプレイヤーを検出できるようになります(デフォルトで player_group_name = "player")。

手順③: 敵に DistanceActivator をアタッチする

敵シーン例:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DistanceActivator (Node)
  1. Enemy シーンを開き、Enemy(親ノード) を選択した状態で右クリック → 「子ノードを追加」。
  2. Node を追加し、名前を DistanceActivator に変更。
  3. そのノードに先ほどの DistanceActivator.gd スクリプトをアタッチします。
  4. インスペクタで次のように設定してみましょう:
    • activate_distance = 500.0
    • deactivate_distance = 650.0
    • check_interval = 0.2
    • activate_once = false(近づいたら起動・離れたら停止を繰り返す)
    • use_deactivation = true
    • auto_find_player = true(デフォルトのまま)

これで、プレイヤーが 500px 以内に来るまで、Enemy の _process / _physics_process は止まったままになります。
遠くで待機している敵が大量にいても、実際に負荷がかかるのはプレイヤーの近くの敵だけになります。

手順④: 他のオブジェクトにも使い回す

同じコンポーネントを、例えば「動く床」にも付けてみましょう。

MovingPlatform (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── DistanceActivator (Node)
  • MovingPlatform のスクリプトでは、普段通り _physics_process などで移動ロジックを書きます。
  • DistanceActivatorprocess_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」といった組み合わせで構成していく方が、長期的にメンテしやすいですね。