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);
}
}
}
使い方の手順
ここでは「プレイヤーがブーメランを投げる」例で解説しますが、敵キャラやトラップにも同じコンポーネントをそのまま使えます。
-
プレハブの準備
- 空の GameObject を作成し、ブーメランの 3D モデルやスプライトを子オブジェクトとして配置します。
- 親オブジェクトに
Rigidbodyを追加します(Use Gravityはオフにするのが扱いやすいです)。 - 同じ親オブジェクトに
BoomerangProjectileコンポーネントを追加します。 - この親オブジェクトを Project ビューへドラッグしてプレハブ化します。
-
プレイヤー側に「投げる」スクリプトを用意
プレイヤーオブジェクトに、シンプルな発射スクリプトを追加します。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); } } } -
インスペクターで参照を設定
- プレイヤーに
PlayerBoomerangShooterをアタッチ。 Boomerang Prefabに、作成したブーメランのプレハブをドラッグ。Throw Originに、プレイヤーの手元など発射位置の Transform をドラッグ。
- プレイヤーに
-
Input の割り当て
- 新 Input System を使っている場合は、Input Actions アセットで「Throw」アクションを作成し、
PlayerBoomerangShooter.OnThrowにイベントを紐づけます。 - 旧 Input Manager を使う場合は、
Update()内でInput.GetButtonDown("Fire1")などを見て、押されたらThrow()的な関数を呼ぶ形にしてもOKです。
- 新 Input System を使っている場合は、Input Actions アセットで「Throw」アクションを作成し、
これで、プレイヤーがボタンを押すとブーメランが飛び出し、一定距離で折り返して手元に戻ってくるようになります。
同じブーメランプレハブを、敵キャラの「投げる位置」にもアタッチして 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 クラスを避けて、気持ちよく拡張できるプロジェクト構成を目指していきましょう。
