Unityを触り始めた頃にありがちなのが、プレイヤー操作も、カメラ制御も、弾の挙動も、敵AIも、全部ひとつの Update() に書いてしまうパターンですね。
動き始めたときはうまくいっているように見えますが、少し仕様変更が入ると「どこを直せばいいのか分からない」「別のシーンで使い回せない」「バグが出ても原因を切り分けられない」といった問題が一気に噴き出します。
そこでおすすめなのが、「弾の誘導ロジックだけ」を担当する小さなコンポーネントを用意してしまうやり方です。
今回は、ターゲットに向かって徐々に旋回して追いかける 誘導弾 を実装する HomingMissile コンポーネントを作ってみましょう。
【Unity】徐々に曲がる追尾弾をコンポーネント化!「HomingMissile」コンポーネント
このコンポーネントは、
- ターゲットの方向を毎フレーム計算
- 最大旋回角度を超えないように少しずつ向きを変える
- 常に「前方向」に進む
という役割だけに絞っています。
爆発処理やダメージ処理は別コンポーネントに分けることで、きれいなコンポーネント指向な構成にできます。
HomingMissile.cs(フルコード)
using UnityEngine;
/// <summary>
/// ターゲットに向かって徐々に旋回しながら進む誘導弾コンポーネント。
/// 「移動」と「旋回」のみを担当し、ダメージやエフェクトは別コンポーネントに任せる想定。
/// </summary>
[RequireComponent(typeof(Rigidbody))]
public class HomingMissile : MonoBehaviour
{
// --- 基本設定 ---
[Header("移動設定")]
[SerializeField]
[Tooltip("ミサイルの前方向(transform.forward)に対する移動速度(m/s)")]
private float moveSpeed = 10f;
[SerializeField]
[Tooltip("1秒あたりに回転できる最大角度(度)。大きいほど急カーブできる")]
private float turnSpeedDegPerSec = 180f;
[Header("ターゲット設定")]
[SerializeField]
[Tooltip("追尾するターゲット。プレハブ生成時に外部からセットするのがおすすめ")]
private Transform target;
[SerializeField]
[Tooltip("ターゲットを見失ったと判定する最大距離。0以下なら無制限")]
private float maxChaseDistance = 0f;
[Header("寿命設定")]
[SerializeField]
[Tooltip("この秒数が経過したら自動で破棄する。0以下なら無効")]
private float lifeTime = 10f;
[Header("その他オプション")]
[SerializeField]
[Tooltip("ターゲットが消えた/範囲外になった後も、最後の向きのまま直進し続けるか")]
private bool keepStraightWhenLost = true;
[SerializeField]
[Tooltip("Y軸を固定して水平面上だけで旋回させる(トップダウン・見下ろしゲーム向け)")]
private bool lockRotationToY = false;
// --- 内部状態 ---
private Rigidbody rb;
private float lifeTimer;
private bool hasLostTarget;
private void Awake()
{
// 必須コンポーネントを取得
rb = GetComponent<Rigidbody>();
// 物理挙動はスクリプトで制御したいので、慣性は無効化しておく
rb.useGravity = false;
rb.isKinematic = false;
}
private void OnEnable()
{
// 有効化されるたびに寿命タイマーをリセット
lifeTimer = 0f;
hasLostTarget = false;
}
private void FixedUpdate()
{
// 物理系の移動なので FixedUpdate で制御する
UpdateLifeTime();
UpdateRotation();
UpdateMovement();
}
/// <summary>
/// 寿命タイマーの更新と自動破棄
/// </summary>
private void UpdateLifeTime()
{
if (lifeTime <= 0f)
{
return;
}
lifeTimer += Time.fixedDeltaTime;
if (lifeTimer >= lifeTime)
{
Destroy(gameObject);
}
}
/// <summary>
/// ターゲットの方向に向かって徐々に旋回する処理
/// </summary>
private void UpdateRotation()
{
// すでにターゲットを見失っていて、直進モードの場合は何もしない
if (hasLostTarget && keepStraightWhenLost)
{
return;
}
// ターゲットがセットされていない場合は見失い扱い
if (target == null)
{
hasLostTarget = true;
return;
}
// 距離制限がある場合は、範囲外なら見失い扱い
if (maxChaseDistance > 0f)
{
float sqrDistance = (target.position - transform.position).sqrMagnitude;
if (sqrDistance > maxChaseDistance * maxChaseDistance)
{
hasLostTarget = true;
return;
}
}
// 現在位置からターゲットまでの方向ベクトル(正規化)
Vector3 toTarget = (target.position - transform.position).normalized;
// Y軸固定モードの場合は、垂直方向の差分を無視して水平面だけで追尾
if (lockRotationToY)
{
toTarget.y = 0f;
// 完全に0だと方向が定まらないのでチェック
if (toTarget.sqrMagnitude < 0.0001f)
{
return;
}
toTarget.Normalize();
}
// 現在の forward から toTarget へ向けて、一定角速度で回転させる
Quaternion currentRot = transform.rotation;
Quaternion targetRot = Quaternion.LookRotation(toTarget, Vector3.up);
// 1ステップで回転できる最大角度(ラジアンではなく度)を計算
float maxStep = turnSpeedDegPerSec * Time.fixedDeltaTime;
// Slerp ではなく RotateTowards を使うと「最大何度まで回せるか」を指定できる
Quaternion newRot = Quaternion.RotateTowards(currentRot, targetRot, maxStep);
transform.rotation = newRot;
}
/// <summary>
/// 現在の forward 方向に進ませる処理
/// </summary>
private void UpdateMovement()
{
// Rigidbody の velocity を直接設定して前進させる
Vector3 velocity = transform.forward * moveSpeed;
rb.velocity = velocity;
}
/// <summary>
/// 外部からターゲットをセットするための公開メソッド。
/// プレハブ生成直後に呼び出す想定。
/// </summary>
/// <param name="newTarget">追尾したい Transform</param>
public void SetTarget(Transform newTarget)
{
target = newTarget;
hasLostTarget = false;
}
/// <summary>
/// 外部から弾速を変更したい場合用。
/// </summary>
public void SetMoveSpeed(float newSpeed)
{
moveSpeed = Mathf.Max(0f, newSpeed);
}
/// <summary>
/// 外部から旋回速度を変更したい場合用(度/秒)。
/// </summary>
public void SetTurnSpeed(float newTurnSpeedDegPerSec)
{
turnSpeedDegPerSec = Mathf.Max(0f, newTurnSpeedDegPerSec);
}
}
使い方の手順
ここでは「プレイヤーを追いかける敵の誘導ミサイル」を例に、基本的な使い方を見ていきます。
-
プレハブの準備
- 空の GameObject を作成し、名前を
HomingMissileに変更 - 子オブジェクトとしてミサイルの見た目用 Mesh や Sprite を配置
- 親オブジェクトに
Rigidbodyを追加(Use Gravity はオフ推奨) - 親オブジェクトに上記の
HomingMissileスクリプトをアタッチ - 必要なら
Colliderも追加しておく(当たり判定用) - この GameObject を Project ビューにドラッグしてプレハブ化
- 空の GameObject を作成し、名前を
-
プレイヤー(ターゲット)の用意
すでにプレイヤーオブジェクトがある場合は、それをターゲットとして使います。
プレイヤーの Transform を、ミサイル生成時にSetTarget()で渡す形にすると、シーンごとに柔軟に差し替えられます。 -
敵からミサイルを発射するスクリプトを書く
例として、敵が一定間隔でプレイヤーに向けて誘導ミサイルを撃つコンポーネントを作ってみます。using UnityEngine; public class EnemyMissileShooter : MonoBehaviour { [SerializeField] private HomingMissile missilePrefab; [SerializeField] private Transform firePoint; // 発射位置(敵の子オブジェクトなど) [SerializeField] private Transform player; // プレイヤーの Transform [SerializeField] private float fireInterval = 2f; private float fireTimer; private void Update() { fireTimer += Time.deltaTime; if (fireTimer >= fireInterval) { fireTimer = 0f; FireMissile(); } } private void FireMissile() { if (missilePrefab == null || firePoint == null || player == null) { return; } // ミサイルを生成 HomingMissile missile = Instantiate(missilePrefab, firePoint.position, firePoint.rotation); // 追尾ターゲットをプレイヤーに設定 missile.SetTarget(player); // 必要に応じて速度や旋回速度を調整 // missile.SetMoveSpeed(15f); // missile.SetTurnSpeed(240f); } }このスクリプトを敵オブジェクトにアタッチし、
Missile Prefabに先ほど作ったミサイルプレハブFire Pointに発射位置の Transform(敵の子オブジェクトなど)Playerにプレイヤーの Transform
をドラッグ&ドロップでセットすれば、一定間隔で誘導弾がプレイヤーを追いかけるようになります。
-
別の用途への使い回し
- ボスが撃つ「ゆっくり曲がる巨大ミサイル」:
moveSpeedを遅く、turnSpeedDegPerSecも小さめに設定。 - プレイヤーのホーミングショット:
プレイヤーの攻撃スクリプトからSetTarget(ロックオン中の敵)を呼び出すだけでOK。 - 動く床に追従するエネルギーボール:
床の Transform をターゲットにすれば、「床の前方を常に少し先回りする球体」なども実現できます。
- ボスが撃つ「ゆっくり曲がる巨大ミサイル」:
メリットと応用
HomingMissile をコンポーネントとして切り出すメリットはかなり多いです。
-
プレハブ化して量産できる
動き方がコンポーネントに閉じているので、「見た目だけ変えた別スキンの誘導弾」プレハブを簡単に増やせます。
レベルデザイナーはmoveSpeedやturnSpeedDegPerSecをインスペクターから調整するだけで、
「緩やかに曲がる弾」「超高機動の弾」などをノーコードで作り分けられます。 -
責務がはっきりしていてバグを追いやすい
このコンポーネントは「ターゲットを追いかける動き」だけを担当しているので、
爆発しない、ダメージが入らない、といった問題は別コンポーネント側を疑えばよく、原因の切り分けがしやすくなります。 -
他のシステムと組み合わせやすい
ロックオンシステム、弾幕生成システム、オブジェクトプールなど、
どんな仕組みから生成されても、SetTarget()さえ呼べば同じように誘導してくれるので、
プロジェクト全体の構造がシンプルになります。
応用として、「ターゲットに十分近づいたら爆発する」処理を別コンポーネントに切り出すのもおすすめです。
例として、ミサイルに取り付ける簡単な自爆コンポーネントを示します。
using UnityEngine;
/// <summary>
/// 一定距離まで近づいたら自動で爆発(ここでは Destroy)するコンポーネント。
/// ダメージ処理やエフェクトは別途追加してください。
/// </summary>
public class ProximityDetonator : MonoBehaviour
{
[SerializeField]
private Transform target;
[SerializeField]
[Tooltip("この距離以内に入ったら爆発する")]
private float explodeDistance = 1.5f;
private void Update()
{
if (target == null)
{
return;
}
float sqrDist = (target.position - transform.position).sqrMagnitude;
if (sqrDist <= explodeDistance * explodeDistance)
{
// 本来はここでエフェクト再生やダメージ処理を行う
Destroy(gameObject);
}
}
public void SetTarget(Transform newTarget)
{
target = newTarget;
}
}
このように、「追尾するコンポーネント」と「爆発するコンポーネント」を分けておけば、
・追尾しかしないデコイ弾
・着弾時にだけ爆発する直進弾
なども簡単にバリエーション展開できます。
小さな責務ごとにコンポーネントを分けていくと、プロジェクトがかなり扱いやすくなるので、ぜひ意識してみてください。
