Unityを触り始めた頃によくあるのが、Update() にゲームロジックを全部書いてしまうパターンですね。
プレイヤーの移動、敵AI、アニメーション制御、エフェクト更新……すべてが1つの巨大スクリプトに詰め込まれていくと、だんだん何がどこで動いているのか分からなくなります。
さらに、画面外にいる敵やオブジェクトまでずっと Update() を回し続けていると、処理負荷も地味に積もっていきます。
「見えていないなら止めたいけど、どうやってキレイに止めるの?」というところで、また巨大な if 文が増えてしまいがちです。
そこでこの記事では、「画面に映っている間だけ動かす」 という責務だけを持った小さなコンポーネント
VisibilityOptimizer を作って、VisibleOnScreenNotifier2D と組み合わせて
「画面外に出たら親の処理(Process)を停止する」仕組みをコンポーネント指向で実現してみましょう。
【Unity】見えているときだけ動かす!「VisibilityOptimizer」コンポーネント
今回の設計のポイントは次の2つです。
- 処理の中身(何をするか)は別コンポーネントに任せる
- 「見えているかどうか」の情報だけを元に、処理コンポーネントをON/OFFする
つまり、VisibilityOptimizer は「見えているかどうかを監視して、
親オブジェクトにある Process コンポーネントを有効/無効にする」だけのコンポーネントです。
このおかげで、敵の行動ロジックや動く床の制御ロジックはそれぞれシンプルな Process コンポーネントとして分離できます。
前提:Process 側のインターフェース
この記事では、「親の処理(Process)」を次のようなシンプルなコンポーネントとして定義します。
MonoBehaviourを継承した任意のスクリプト- 有効化されている間だけ
Update()やFixedUpdate()などが動く
例えば、敵の移動処理ならこんな感じです。
// これは「処理の中身」側の例:敵を左右に動かすだけのコンポーネント
using UnityEngine;
public class EnemyProcess : MonoBehaviour
{
[SerializeField] private float moveSpeed = 2f;
[SerializeField] private float moveRange = 3f;
private Vector3 _startPosition;
private void Awake()
{
_startPosition = transform.position;
}
private void Update()
{
// sin波で左右に動く簡単な処理
float offset = Mathf.Sin(Time.time * moveSpeed) * moveRange;
transform.position = _startPosition + new Vector3(offset, 0f, 0f);
}
}
この EnemyProcess は、enabled = false にされると Update() が呼ばれなくなります。
VisibilityOptimizer は、この enabled のON/OFFだけを担当します。
VisibilityOptimizer 本体コード
それでは本題の VisibilityOptimizer コンポーネントのフルコードです。
using UnityEngine;
using UnityEngine.Rendering.Universal; // VisibleOnScreenNotifier2D が含まれている名前空間
/// <summary>
/// 画面に映っている間だけ、指定したコンポーネント(Process)を有効にする最適化コンポーネント。
/// VisibleOnScreenNotifier2D のイベントを利用して、
/// 画面外に出たら自動で Process を停止します。
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(VisibleOnScreenNotifier2D))]
public class VisibilityOptimizer : MonoBehaviour
{
// 画面内/外の判定を行うコンポーネント(必須)
[SerializeField]
private VisibleOnScreenNotifier2D visibleNotifier2D;
// 有効/無効を切り替えたい対象コンポーネント(親の処理 Process)
// 例:EnemyProcess, MovingPlatformProcess など
[SerializeField]
private MonoBehaviour targetProcess;
// 初期状態で画面外にいても、起動時だけは有効化しておきたい場合に使う
[SerializeField]
private bool enableProcessOnStart = false;
// デバッグ用:状態変化をログに出すかどうか
[SerializeField]
private bool showDebugLog = false;
// キャッシュ用フラグ:現在 Process が有効かどうか
private bool _isProcessEnabled;
private void Reset()
{
// コンポーネントがアタッチされたときに自動で VisibleOnScreenNotifier2D をセット
visibleNotifier2D = GetComponent<VisibleOnScreenNotifier2D>();
// 親または自分にある MonoBehaviour を自動で探して、最初に見つかったものを targetProcess に設定
// (もちろん、後からインスペクタで差し替え可能)
if (targetProcess == null)
{
// 自分自身以外のコンポーネントから探す
MonoBehaviour[] candidates = GetComponentsInParent<MonoBehaviour>(includeInactive: true);
foreach (var mb in candidates)
{
if (mb == this) continue;
if (mb is VisibleOnScreenNotifier2D) continue;
targetProcess = mb;
break;
}
}
}
private void Awake()
{
// インスペクタでセットされていなければ、自動取得を試みる
if (visibleNotifier2D == null)
{
visibleNotifier2D = GetComponent<VisibleOnScreenNotifier2D>();
}
if (visibleNotifier2D == null)
{
Debug.LogError(
$"[VisibilityOptimizer] VisibleOnScreenNotifier2D が見つかりません。ゲームオブジェクト: {name}",
this
);
}
if (targetProcess == null)
{
Debug.LogWarning(
$"[VisibilityOptimizer] targetProcess が設定されていません。画面内/外の制御対象がありません。ゲームオブジェクト: {name}",
this
);
}
}
private void OnEnable()
{
// VisibleOnScreenNotifier2D のイベントに登録
if (visibleNotifier2D != null)
{
visibleNotifier2D.onBecameVisible += HandleBecameVisible;
visibleNotifier2D.onBecameInvisible += HandleBecameInvisible;
}
// 起動時の Process 状態をセット
InitializeProcessState();
}
private void OnDisable()
{
// イベント登録解除(メモリリーク・多重登録防止)
if (visibleNotifier2D != null)
{
visibleNotifier2D.onBecameVisible -= HandleBecameVisible;
visibleNotifier2D.onBecameInvisible -= HandleBecameInvisible;
}
}
/// <summary>
/// 起動時の Process の状態を初期化。
/// enableProcessOnStart が true なら一度だけ有効化し、
/// そうでなければ現在の可視状態に合わせる。
/// </summary>
private void InitializeProcessState()
{
if (targetProcess == null) return;
if (enableProcessOnStart)
{
SetProcessEnabled(true, reason: "Start");
return;
}
if (visibleNotifier2D != null)
{
bool isVisible = visibleNotifier2D.isVisible;
SetProcessEnabled(isVisible, reason: "Initialize (by current visibility)");
}
else
{
// Notifier が無い場合は安全側として有効化しておく
SetProcessEnabled(true, reason: "Initialize (no notifier)");
}
}
/// <summary>
/// 画面内に入ったときに呼ばれるハンドラ。
/// </summary>
private void HandleBecameVisible(VisibleOnScreenNotifier2D notifier)
{
SetProcessEnabled(true, reason: "OnBecameVisible");
}
/// <summary>
/// 画面外に出たときに呼ばれるハンドラ。
/// </summary>
private void HandleBecameInvisible(VisibleOnScreenNotifier2D notifier)
{
SetProcessEnabled(false, reason: "OnBecameInvisible");
}
/// <summary>
/// Process の有効/無効を切り替える共通関数。
/// すでに同じ状態なら何もしない。
/// </summary>
private void SetProcessEnabled(bool enabled, string reason)
{
if (targetProcess == null) return;
if (_isProcessEnabled == enabled)
{
// 状態が変わっていなければ何もしない
return;
}
_isProcessEnabled = enabled;
targetProcess.enabled = enabled;
if (showDebugLog)
{
Debug.Log(
$"[VisibilityOptimizer] Process: {targetProcess.GetType().Name} を " +
$"{(enabled ? "有効化" : "無効化")} ({reason}) - GameObject: {name}",
this
);
}
}
}
このコンポーネント自体はかなり小さいですが、責務は明確です。
- 何をするか:VisibleOnScreenNotifier2D のイベントを受けて、
targetProcess.enabledを切り替える - 何をしないか:移動、攻撃、アニメーションなどのゲームロジックは一切持たない
これにより、「見えているときだけ動かす」 という関心事を1つのコンポーネントに閉じ込められます。
使い方の手順
ここからは、実際の利用シーンを例に手順を見ていきましょう。
例として「2Dアクションゲームの敵キャラ」を使いますが、動く床やギミックにも同じ手順で使えます。
手順①:Process 側のコンポーネントを用意する
まずは「親の処理(Process)」を表すコンポーネントを作ります。
敵キャラがプレイヤーに向かって歩いてくるだけの簡単な例です。
using UnityEngine;
/// <summary>
/// 画面に映っている間だけ動かしたい「処理の中身」の例。
/// プレイヤーの方向に向かって歩き続ける単純な敵AI。
/// </summary>
public class SimpleChaseEnemyProcess : MonoBehaviour
{
[SerializeField] private float moveSpeed = 2f;
[SerializeField] private Transform player;
private void Update()
{
if (player == null) return;
// プレイヤーの方向へ移動
Vector3 direction = (player.position - transform.position).normalized;
transform.position += direction * (moveSpeed * Time.deltaTime);
// 向きの調整(2D想定で左右反転)
if (direction.x != 0f)
{
Vector3 scale = transform.localScale;
scale.x = Mathf.Sign(direction.x) * Mathf.Abs(scale.x);
transform.localScale = scale;
}
}
}
ポイントは、何も特別なインターフェースを実装していない普通の MonoBehaviour であることです。
VisibilityOptimizer は MonoBehaviour を受け取って enabled を切り替えるだけなので、既存のスクリプトにもそのまま適用できます。
手順②:VisibleOnScreenNotifier2D をアタッチする
次に、敵キャラの GameObject(例:Enemy プレハブ)に VisibleOnScreenNotifier2D を追加します。
- Hierarchy で敵オブジェクトを選択
- Inspector の「Add Component」から
VisibleOnScreenNotifier2Dを検索して追加 - 2D Renderer(Universal Render Pipeline 2D)環境で動かしていることを確認
VisibleOnScreenNotifier2D は、カメラに映った/映らなくなったタイミングで
onBecameVisible / onBecameInvisible のイベントを発火してくれるコンポーネントです。
これを VisibilityOptimizer が購読します。
手順③:VisibilityOptimizer をアタッチし、Process を指定する
- 同じ敵オブジェクトに
VisibilityOptimizerを追加 -
Inspector 設定
Visible Notifier 2D:自動で自分のVisibleOnScreenNotifier2Dが入るはずですTarget Process:SimpleChaseEnemyProcess(または動かしたい任意のスクリプト)をドラッグ&ドロップEnable Process On Start:起動直後から動かしたいなら ON、画面に入るまで完全停止でいいなら OFFShow Debug Log:挙動確認したいときだけ ON にしておくと、状態切り替えが Console に出ます
これで、ゲーム再生時に「敵がカメラに映った瞬間に Process が有効化され、画面外に出たら自動で停止する」ようになります。
手順④:他のオブジェクトにも再利用する(動く床やギミック)
同じ仕組みは、動く床やトラップなどのギミックにもそのまま使えます。
例えば、一定距離を往復する動く床を MovingPlatformProcess として作っておき、
- 床のオブジェクトに
VisibleOnScreenNotifier2Dを追加 - 同じオブジェクトに
VisibilityOptimizerを追加し、Target ProcessにMovingPlatformProcessを指定
とするだけで、画面外の動く床の Update() を止めることができます。
レベル全体に何十個も動く床があっても、画面に映っているものだけ処理されるのでパフォーマンス的にも安心ですね。
メリットと応用
VisibilityOptimizer を導入することで、次のようなメリットがあります。
1. プレハブの責務がハッキリする
敵プレハブを見たときに、
SimpleChaseEnemyProcess:敵の行動ロジックVisibilityOptimizer:見えているときだけ動かす最適化ロジックVisibleOnScreenNotifier2D:可視状態の検知
というように、役割がコンポーネント単位ではっきり分かれます。
巨大な EnemyController.cs 1本に全部詰め込むより、どこを触れば何が変わるか が分かりやすくなります。
2. レベルデザイン時に「重さ」を気にしなくてよくなる
ステージの端から端まで敵やギミックを並べても、
「画面外にいる間は止まっている」ので、Update の数が爆発的に増えにくくなります。
レベルデザイナーは「ここに敵を置いたら面白いか?」に集中できて、
エンジニアは「見えているものだけ動かす仕組み」があるのでパフォーマンス面の不安を減らせます。
3. 既存のスクリプトにも簡単に後付けできる
VisibilityOptimizer は MonoBehaviour の enabled を切り替えるだけなので、
既にある EnemyAI や MovingPlatform スクリプトにも、
Inspector から Target Process に設定するだけで適用できます。
コードを書き換える必要がないのは大きな利点ですね。
4. 応用:距離ベースの最適化や LOD と組み合わせる
画面内/外だけでなく、「プレイヤーから一定距離以上離れたら止める」といった
距離ベースの最適化と組み合わせると、さらに効率的にできます。
その場合は、VisibilityOptimizer のような「ON/OFF 管理コンポーネント」を別途用意して、
条件を増やしていくと良いですね。
改造案:フェードアウトしてから停止する
単に処理を止めるだけでなく、画面外に出るときにフェードアウトしてから停止したいというケースもあります。
その場合は、VisibilityOptimizer に次のようなメソッドを追加してみましょう。
/// <summary>
/// 画面外に出たタイミングで、一定時間かけてフェードアウトしてから Process を停止する改造例。
/// SpriteRenderer を前提としているので、必要に応じて MeshRenderer などに置き換えてください。
/// </summary>
private void FadeOutAndDisableProcess(float duration)
{
if (targetProcess == null) return;
SpriteRenderer sprite = GetComponentInParent<SpriteRenderer>();
if (sprite == null)
{
// SpriteRenderer が無ければ即停止
SetProcessEnabled(false, reason: "FadeOut (no sprite)");
return;
}
// コルーチンでフェードアウトを実装
StartCoroutine(FadeOutCoroutine(sprite, duration));
}
private System.Collections.IEnumerator FadeOutCoroutine(SpriteRenderer sprite, float duration)
{
float time = 0f;
Color startColor = sprite.color;
while (time < duration)
{
time += Time.deltaTime;
float t = Mathf.Clamp01(time / duration);
// アルファ値だけを 1 → 0 に補間
Color c = startColor;
c.a = Mathf.Lerp(1f, 0f, t);
sprite.color = c;
yield return null;
}
// 完全に透明にしたあと、Process を停止
SetProcessEnabled(false, reason: "FadeOut completed");
}
このように、「見えている間だけ動かす」という責務を1つのコンポーネントに切り出しておくと、
後から「フェードさせたい」「消える前にSEを鳴らしたい」などの要件が出てきても、
VisibilityOptimizer 周りだけを拡張すれば済むようになります。
巨大な God クラスを作るのではなく、小さなコンポーネントを組み合わせてゲームの挙動を作っていくと、
保守性も再利用性もぐっと上がります。ぜひ、自分のプロジェクトでも VisibilityOptimizer 的なコンポーネントを取り入れてみてください。
