Unityを触り始めた頃って、プレイヤーの移動も、ジャンプも、攻撃も、エフェクトも、全部ひとつの Update() に書いてしまいがちですよね。最初は動くので「これでいいじゃん」と思うのですが、少し機能が増えてくると地獄が始まります。

  • どこに何が書いてあるか分からない
  • ちょっとした修正で別の挙動が壊れる
  • プレイヤーと敵で似た処理をコピペしてバグが量産される

特に「飛び道具(プロジェクタイル)」まわりは、プレイヤー側スクリプトに直書きしがちな代表例です。
そこで今回は、「ブーメランの動きだけ」を担当する小さなコンポーネントとして BoomerangProjectile を用意して、

  • 一定距離まではまっすぐ飛ぶ
  • 一定距離を超えたら速度を反転して発射者の元へ戻る

という挙動を、どのオブジェクトにも簡単に付けられるようにしてみましょう。

【Unity】戻ってくる飛び道具を手軽に実装!「BoomerangProjectile」コンポーネント

フルコード(BoomerangProjectile.cs)


using UnityEngine;

namespace BoomerangSample
{
    /// <summary>
    /// ブーメランのように「一定距離までは前進し、その後は発射者の元へ戻る」プロジェクタイル。
    /// Rigidbody を使った物理ベースの移動を行います。
    /// 
    /// ・発射時に Init() を呼び出して発射者 Transform と初期方向を設定してください。
    /// ・RequireComponent により Rigidbody の付け忘れを防ぎます。
    /// </summary>
    [RequireComponent(typeof(Rigidbody))]
    public class BoomerangProjectile : MonoBehaviour
    {
        [Header("基本設定")]
        [SerializeField]
        private float forwardSpeed = 10f;    // 行きのスピード

        [SerializeField]
        private float returnSpeed = 12f;     // 戻りのスピード(少し速めにすると気持ちいい)

        [SerializeField]
        private float maxTravelDistance = 10f; // 何メートル進んだら折り返すか

        [SerializeField]
        private float destroyDistanceToOwner = 0.5f; // 発射者にどれだけ近づいたら破棄するか

        [Header("オプション")]
        [SerializeField]
        private bool autoDestroyIfNoOwner = true; // 発射者がいない状態で戻ろうとした場合に自動破棄するか

        [SerializeField]
        private float lifeTime = 10f; // 念のための寿命。長時間シーンに残り続けるのを防ぐ

        // 内部状態
        private Rigidbody rb;
        private Transform owner;       // 発射者(プレイヤーや敵など)
        private Vector3 spawnPosition; // 発射位置(距離計測用)
        private bool isReturning;      // 現在「戻りフェーズ」かどうか

        // Init() が呼ばれたかどうかをチェックするためのフラグ
        private bool isInitialized;

        private void Awake()
        {
            rb = GetComponent<Rigidbody>();

            // ブーメランは基本的に自分で回転するので、物理回転は固定しておくと扱いやすい
            rb.freezeRotation = true;
        }

        private void OnEnable()
        {
            // 有効化されたタイミングで寿命タイマーをリセット
            if (lifeTime > 0f)
            {
                CancelInvoke(nameof(DestroySelf));
                Invoke(nameof(DestroySelf), lifeTime);
            }
        }

        /// <summary>
        /// ブーメランを発射するための初期化関数。
        /// ・ownerTransform: 発射者(プレイヤーや敵など)の Transform
        /// ・direction: 発射方向(正規化されていなくてもOK。内部で正規化します)
        /// </summary>
        public void Init(Transform ownerTransform, Vector3 direction)
        {
            owner = ownerTransform;
            spawnPosition = transform.position;
            isReturning = false;
            isInitialized = true;

            // 発射方向を正規化
            Vector3 dir = direction.normalized;
            if (dir.sqrMagnitude < 0.0001f)
            {
                // 万が一ゼロベクトルが来た場合は、前方に飛ばす
                dir = transform.forward;
            }

            // 行きの速度を設定
            rb.velocity = dir * forwardSpeed;
        }

        private void FixedUpdate()
        {
            // Init() が呼ばれていない場合は何もしない(誤動作防止)
            if (!isInitialized)
            {
                return;
            }

            // 現在位置から発射位置までの距離を計測
            float traveledDistance = Vector3.Distance(spawnPosition, transform.position);

            // まだ戻りフェーズでなく、一定距離を超えたら戻り開始
            if (!isReturning && traveledDistance >= maxTravelDistance)
            {
                StartReturn();
            }

            if (isReturning)
            {
                UpdateReturnMovement();
            }
        }

        /// <summary>
        /// 戻りフェーズ開始時の処理。
        /// </summary>
        private void StartReturn()
        {
            isReturning = true;

            // 発射者が存在しなければ、その場で速度を反転させるだけ
            if (owner == null)
            {
                // シンプルに現在の速度を反転
                rb.velocity = -rb.velocity.normalized * returnSpeed;

                // 発射者がいない状態で戻りフェーズになった場合、
                // 設定に応じて一定時間後に自動破棄するなどの対応を行う
                if (autoDestroyIfNoOwner)
                {
                    // ここでは「寿命タイマー任せ」にして特別な処理はしない
                }

                return;
            }

            // 発射者の方向を向くように速度を設定
            Vector3 toOwner = (owner.position - transform.position).normalized;
            rb.velocity = toOwner * returnSpeed;
        }

        /// <summary>
        /// 戻りフェーズ中の移動処理。
        /// 発射者を追いかけるように速度ベクトルを更新します。
        /// </summary>
        private void UpdateReturnMovement()
        {
            if (owner == null)
            {
                // 発射者が破棄された / シーンからいなくなった場合
                if (autoDestroyIfNoOwner)
                {
                    DestroySelf();
                }
                return;
            }

            // 発射者の方向へ向かうベクトルを計算
            Vector3 toOwner = owner.position - transform.position;
            float distanceToOwner = toOwner.magnitude;

            // 一定距離まで近づいたらブーメランを破棄(=手元に戻った扱い)
            if (distanceToOwner <= destroyDistanceToOwner)
            {
                DestroySelf();
                return;
            }

            // 発射者の方向へ速度ベクトルを更新
            Vector3 dir = toOwner.normalized;
            rb.velocity = dir * returnSpeed;

            // 見た目用に、進行方向を向かせる(任意)
            // ブーメランのモデルの「前」が Z+ 方向の場合はこれでOK
            if (dir.sqrMagnitude > 0.0001f)
            {
                transform.rotation = Quaternion.LookRotation(dir, Vector3.up);
            }
        }

        /// <summary>
        /// 自身を安全に破棄するための関数。
        /// 将来的にオブジェクトプールに対応したい場合は、この関数の中身を書き換えればOK。
        /// </summary>
        private void DestroySelf()
        {
            // ここではシンプルに Destroy していますが、
            // オブジェクトプールを使う場合は SetActive(false) にするなどに差し替えましょう。
            Destroy(gameObject);
        }

        /// <summary>
        /// デバッグ用に、エディタ上で飛距離の目安を表示。
        /// </summary>
        private void OnDrawGizmosSelected()
        {
            Gizmos.color = Color.yellow;

            // シーンビュー上で最大飛距離の目安を描画
            Gizmos.DrawWireSphere(transform.position, maxTravelDistance);

            Gizmos.color = Color.cyan;
            Gizmos.DrawWireSphere(transform.position, destroyDistanceToOwner);
        }
    }
}

使い方の手順

ここでは「プレイヤーがブーメランを投げる」例で解説しますが、敵キャラやトラップにも同じコンポーネントをそのまま使えます。

  1. プレハブの準備
    • 空の GameObject を作成し、ブーメランの 3D モデルやスプライトを子オブジェクトとして配置します。
    • 親オブジェクトに Rigidbody を追加します(Use Gravity はオフにするのが扱いやすいです)。
    • 同じ親オブジェクトに BoomerangProjectile コンポーネントを追加します。
    • この親オブジェクトを Project ビューへドラッグしてプレハブ化します。
  2. プレイヤー側に「投げる」スクリプトを用意
    プレイヤーオブジェクトに、シンプルな発射スクリプトを追加します。
    
    using UnityEngine;
    using UnityEngine.InputSystem; // 新 Input System を使う場合
    
    namespace BoomerangSample
    {
        public class PlayerBoomerangShooter : MonoBehaviour
        {
            [SerializeField]
            private BoomerangProjectile boomerangPrefab; // 先ほど作成したプレハブ
    
            [SerializeField]
            private Transform throwOrigin; // ブーメランの発射位置(手元など)
    
            [SerializeField]
            private float throwCooldown = 0.5f; // 連射間隔
    
            private float lastThrowTime;
    
            // Input System の Action から呼ばれる想定の関数
            // 旧 Input Manager を使う場合は Update() で Input.GetButtonDown を見る形でもOK
            public void OnThrow(InputAction.CallbackContext context)
            {
                if (!context.performed)
                    return;
    
                if (Time.time - lastThrowTime < throwCooldown)
                    return;
    
                lastThrowTime = Time.time;
    
                // ブーメランを生成
                BoomerangProjectile boomerang =
                    Instantiate(boomerangPrefab, throwOrigin.position, throwOrigin.rotation);
    
                // プレイヤーの前方に向かって発射
                Vector3 direction = transform.forward;
    
                // 発射者(this.transform)と方向を渡して初期化
                boomerang.Init(transform, direction);
            }
        }
    }
        
  3. インスペクターで参照を設定
    • プレイヤーに PlayerBoomerangShooter をアタッチ。
    • Boomerang Prefab に、作成したブーメランのプレハブをドラッグ。
    • Throw Origin に、プレイヤーの手元など発射位置の Transform をドラッグ。
  4. Input の割り当て
    • 新 Input System を使っている場合は、Input Actions アセットで「Throw」アクションを作成し、PlayerBoomerangShooter.OnThrow にイベントを紐づけます。
    • 旧 Input Manager を使う場合は、Update() 内で Input.GetButtonDown("Fire1") などを見て、押されたら Throw() 的な関数を呼ぶ形にしてもOKです。

これで、プレイヤーがボタンを押すとブーメランが飛び出し、一定距離で折り返して手元に戻ってくるようになります。
同じブーメランプレハブを、敵キャラの「投げる位置」にもアタッチして Init(enemyTransform, direction) を呼べば、敵専用ブーメランも簡単に作れます。

メリットと応用

BoomerangProjectile をコンポーネントとして分離しておくと、

  • プレイヤーのスクリプトが「投げる」ロジックだけに集中できる(移動ロジックはブーメラン側)
  • 敵やギミックでも同じブーメラン挙動を再利用できる(発射者と方向を変えるだけ)
  • プレハブ単位でパラメータを変えやすい(速いブーメラン、遅いブーメラン、戻り距離が短いものなど)
  • レベルデザイン時に「ここにブーメラントラップを置きたい」だけで完結(専用コードを書かなくて済む)

といったメリットがあります。
特に「行きの速度」「戻りの速度」「最大距離」「戻ってきたとみなす距離」をすべてインスペクターから調整できるので、レベルデザイナーやゲームデザイナーがプレハブを複製してパラメータを変えるだけで、さまざまなバリエーションを作れます。

さらに応用として、

  • 戻ってくる途中で敵に当たったらダメージを与える
  • ブーメランが戻ってきたら、プレイヤー側で「キャッチ」アニメーションを再生する
  • オブジェクトプールに対応させて GC 負荷を下げる

といった拡張も簡単です。

最後に、「戻ってくる途中で回転アニメーションを付ける」改造案を一つ紹介します。
以下のようなメソッドを BoomerangProjectile に追加し、FixedUpdate() の末尾か Update() から呼び出すだけで、ブーメランがクルクル回転するようになります。


        /// <summary>
        /// 見た目用の回転アニメーション。
        /// モデルのローカル X 軸まわりに回転させる例です。
        /// </summary>
        private void SpinVisual(float spinSpeedDegreesPerSecond = 720f)
        {
            // 子オブジェクトにモデルを持っている場合は、その Transform を回転させるのがおすすめです。
            // ここでは簡単のため、ルートオブジェクト自体を回転させています。
            transform.Rotate(Vector3.right, spinSpeedDegreesPerSecond * Time.deltaTime, Space.Self);
        }

こうした小さな機能をひとつずつコンポーネントに分解しておくと、あとから「この回転だけ別の飛び道具にも使いたい」というときにも再利用しやすくなります。
巨大な God クラスを避けて、気持ちよく拡張できるプロジェクト構成を目指していきましょう。