Unityを触り始めた頃、「敵AIもプレイヤーも、とりあえず全部Updateに書いて動いたからOK!」となりがちですよね。
でも時間が経つと、Update() が巨大化して「どこを触ると何が壊れるのか分からない」「敵の挙動だけ少し変えたいのに地獄を見る」という状態になりやすいです。
こういうときに意識したいのが、1つのスクリプトに1つの責務(Single Responsibility) を持たせることです。
「移動はこのコンポーネント」「攻撃はこのコンポーネント」「ワープはこのコンポーネント」というように役割を分けておくと、挙動の追加・削除・差し替えがかなり楽になります。
この記事では、「歩かずに、数秒ごとにプレイヤーの背後や死角へワープする」 という、敵AIらしいイヤらしい挙動を
TeleportAI という1つのコンポーネントに切り出して実装してみます。
【Unity】背後からヌルっと出現!「TeleportAI」コンポーネント
今回の TeleportAI は、以下のようなことを行います。
- 指定したプレイヤー(ターゲット)を常に追跡
- 一定間隔ごとに、ターゲットの背後 or 斜め後ろの死角へワープ
- ワープ位置が壁の中や地面の下にならないように簡易的な補正
- ワープの前後でイベント(SE再生やエフェクト再生など)をフックできる
フルコード(TeleportAI.cs)
using UnityEngine;
/// <summary>
/// 指定したターゲット(主にプレイヤー)の背後・死角へ、
/// 一定間隔でワープするシンプルなAIコンポーネント。
/// 歩きモーションを持たない敵や、ホラー系の不意打ちに便利です。
/// </summary>
[DisallowMultipleComponent]
public class TeleportAI : MonoBehaviour
{
// --- 参照系 ---
[Header("ターゲット設定")]
[Tooltip("ワープ先の基準となるターゲット(例: プレイヤー)。未設定の場合、Tag \"Player\" を自動検索します。")]
[SerializeField] private Transform target;
[Header("ワープ間隔")]
[Tooltip("ワープするまでの最小待機時間(秒)。")]
[SerializeField] private float minTeleportInterval = 2.0f;
[Tooltip("ワープするまでの最大待機時間(秒)。")]
[SerializeField] private float maxTeleportInterval = 4.0f;
[Header("ワープ位置オフセット")]
[Tooltip("ターゲットの背後にどれだけ離れて出現するか(メートル)。")]
[SerializeField] private float behindDistance = 2.0f;
[Tooltip("ターゲットの左右どちらにどれだけずらすか(死角感を出したいときに使用)。0 なら真後ろ。")]
[SerializeField] private float sideOffset = 0.5f;
[Tooltip("ワープ後の高さオフセット。キャラクターの足元位置に合わせて調整します。")]
[SerializeField] private float heightOffset = 0.0f;
[Header("ワープ安全設定")]
[Tooltip("地面を検出するためのレイヤーマスク。地面となるレイヤーを指定してください。")]
[SerializeField] private LayerMask groundLayer = ~0; // デフォルト: 全レイヤー
[Tooltip("地面を探す最大距離(ワープ予定位置から下向きに飛ばすレイの長さ)。")]
[SerializeField] private float groundCheckDistance = 5.0f;
[Tooltip("ターゲットとの最小距離。これより近い位置にはワープしないようにします。")]
[SerializeField] private float minDistanceFromTarget = 1.0f;
[Header("回転設定")]
[Tooltip("ワープ後にターゲットの方を向くかどうか。")]
[SerializeField] private bool lookAtTargetAfterTeleport = true;
[Tooltip("Y軸のみを回転させる(2D/TopDown でない3Dゲーム向け)。")]
[SerializeField] private bool rotateOnlyY = true;
[Header("デバッグ表示")]
[Tooltip("Sceneビューにワープ予定位置などのガイドを表示します。")]
[SerializeField] private bool drawGizmos = true;
// --- 内部状態 ---
// 次にワープするまでの残り時間
private float teleportTimer;
// 乱数用:左右どちら側にワープするか
private int lastSideSign = 1;
// Gizmo用に最後に計算したワープ先
private Vector3 lastPlannedTeleportPosition;
private bool hasValidLastTeleportPosition = false;
private void Reset()
{
// コンポーネント追加時に、よく使いそうなデフォルト値をセット
minTeleportInterval = 2.0f;
maxTeleportInterval = 4.0f;
behindDistance = 2.0f;
sideOffset = 0.5f;
heightOffset = 0.0f;
groundCheckDistance = 5.0f;
minDistanceFromTarget = 1.0f;
lookAtTargetAfterTeleport = true;
rotateOnlyY = true;
drawGizmos = true;
groundLayer = ~0;
}
private void Awake()
{
// ターゲットが未設定なら Tag "Player" を自動で探す
if (target == null)
{
GameObject player = GameObject.FindGameObjectWithTag("Player");
if (player != null)
{
target = player.transform;
}
}
// 最初のワープまでの時間をランダムに設定
teleportTimer = GetRandomInterval();
}
private void Update()
{
if (target == null)
{
// ターゲットがいない場合は何もしない
return;
}
// タイマーを減少
teleportTimer -= Time.deltaTime;
if (teleportTimer
使い方の手順
例として、「プレイヤーの背後にワープしてくる幽霊型の敵」を作る手順を説明します。
-
プレイヤーにTagを設定する
- プレイヤーキャラクターのGameObjectを選択
- Inspector 上部の「Tag」を「Player」に設定
- まだ「Player」タグが無ければ「Add Tag...」から追加しておきましょう
-
敵キャラクターのPrefab(またはScene上のオブジェクト)を用意
- 3Dモデル or 2Dスプライトなど、敵の見た目となるGameObjectを作成
- 必要なら
AnimatorやRigidbodyを追加(このコンポーネント自体は必須ではありません) - 敵の足元が「Y=0」あたりになるように調整しておくと、地面へのスナップが綺麗に決まります
-
TeleportAI コンポーネントを追加して設定
- 敵のGameObjectに
TeleportAIスクリプトをアタッチ Targetを手動で設定しなくても、Tag「Player」を自動で探してくれます- 以下のパラメータを調整しながらプレイしてみましょう:
Min Teleport Interval / Max Teleport Interval
→ ワープ頻度。ホラー寄りなら 3〜6 秒、アクション寄りなら 1〜3 秒など。Behind Distance
→ どれだけ後ろに出現するか。近すぎると理不尽に感じるので 2〜4m くらいがおすすめ。Side Offset
→ 0 だと完全な真後ろ。0.5〜1.5 くらいにすると「斜め後ろの死角」感が出ます。Min Distance From Target
→ ワープ位置が近すぎるのを防ぐための最低距離。Look At Target After Teleport
→ ワープした直後にプレイヤーの方を向くかどうか。Ground Layer
→ 地面のレイヤーを指定(例: 「Ground」レイヤーを作って地面に割り当てておく)。
- 敵のGameObjectに
-
具体的な使用例
- プレイヤーを追い詰める幽霊
物理移動を一切させず、TeleportAIだけを付けた「幽霊」敵を作ると、
常に背後に回り込んでくる不気味な存在になります。 - ボスの第2形態のギミック
通常は歩いて追ってくるボスに、体力が一定以下になるとTeleportAIを有効化して、
「瞬間移動モード」に切り替える、といった演出も簡単です。 - 動く床+ワープ敵
動く床の上にこの敵をスポーンさせると、プレイヤーが床を渡っている最中に背後にワープし、
プレイヤーを落とそうとするギミックなども作りやすくなります。
- プレイヤーを追い詰める幽霊
メリットと応用
TeleportAI を1つのコンポーネントとして切り出しておくと、次のようなメリットがあります。
- プレハブの再利用性が高い
「ワープする敵」を作りたいときは、このコンポーネントをアタッチするだけ。
既存の敵プレハブに追加するだけで、「通常移動+ワープ」の複合パターンも簡単に作れます。 - レベルデザインが楽になる
「この部屋の敵は2秒ごとにワープ」「このエリアの敵は6秒ごとにゆっくりワープ」など、
パラメータを変えたプレハブを複数用意しておけば、ステージごとに違う緊張感を演出できます。 - 責務がはっきりしている
「このコンポーネントはワープだけを担当」という明確な役割があるので、
AIロジックが増えても「歩きAI」「射撃AI」「ワープAI」といった形で分割しやすく、
Godクラス化を防ぎやすいです。
さらに、イベント用のフック(OnBeforeTeleport / OnAfterTeleport)を
別コンポーネントから呼び出したり、TeleportAI を継承して
「ワープするときにだけ無敵になる」などのバリエーションも作りやすくなります。
改造案:ワープ前に一瞬だけフェードアウトする
例えば、ワープ直前に敵を半透明にして「消える」演出を入れたい場合、
以下のようなメソッドを別コンポーネントに追加して、TeleportAI のフックから呼び出すと良いです。
using UnityEngine;
/// <summary>
/// Renderer のマテリアルカラーを使って簡易的にフェードさせるコンポーネント。
/// TeleportAI の OnBeforeTeleport / OnAfterTeleport から呼び出して使います。
/// </summary>
[DisallowMultipleComponent]
public class SimpleFadeEffect : MonoBehaviour
{
[SerializeField] private Renderer targetRenderer;
[SerializeField] private float fadedAlpha = 0.1f;
private Color originalColor;
private void Awake()
{
if (targetRenderer == null)
{
targetRenderer = GetComponentInChildren<Renderer>();
}
if (targetRenderer != null)
{
originalColor = targetRenderer.material.color;
}
}
/// <summary>
/// 一瞬だけ薄くする(ワープ前に呼ぶ想定)。
/// </summary>
public void FadeOutInstant()
{
if (targetRenderer == null) return;
Color c = originalColor;
c.a = fadedAlpha;
targetRenderer.material.color = c;
}
/// <summary>
/// 元の不透明度に戻す(ワープ後に呼ぶ想定)。
/// </summary>
public void FadeInInstant()
{
if (targetRenderer == null) return;
targetRenderer.material.color = originalColor;
}
}
このように、「ワープするAI」と「見た目の演出」を別コンポーネントに分ける ことで、
後から「この敵はワープするけどフェードはしない」「この敵はフェードだけする」など、
組み合わせのバリエーションを柔軟に増やしていけるようになります。
