Unityを触り始めたころは、つい何でもかんでも Update() に書いてしまいがちですよね。プレイヤーの移動、敵のAI、弾の移動、当たり判定、スコア計算……全部ひとつの巨大スクリプトに詰め込んでしまうと、
- どこを直せばいいか分からない
- 1つの修正が別の不具合を呼びやすい
- Prefab 化しづらく、再利用性が低い
といった問題が一気に出てきます。
そこでおすすめなのが、「役割ごとに小さなコンポーネントを作る」スタイルです。この記事では、
敵の弾が当たると、弾の所属を自分に変えて跳ね返す「Reflector(反射板)」コンポーネント
を作って、攻撃を跳ね返すギミックをシンプルに実装していきます。プレイヤーの周りを回るバリアや、ステージに設置された反射壁などにそのまま使える構成にしていきましょう。
【Unity】敵弾を味方に変えて跳ね返す!「Reflector」コンポーネント
まずはこのコンポーネントと連携するために、弾(Projectile)側に「誰の弾か」を表す所有者情報と、移動ロジックを持たせます。そのうえで Reflector が
- 敵の弾だけを検知
- 所有者を自分(または指定したオーナー)に書き換え
- 進行方向を反転させて跳ね返す
という処理を行います。
前提となる弾コンポーネント:Projectile
Reflector を活かすために、弾のスクリプトをコンポーネント指向で分離しておきます。
using UnityEngine;
/// <summary>弾の所属(誰の弾か)を表す列挙型</summary>
public enum ProjectileOwner
{
None,
Player,
Enemy
}
/// <summary>
/// シンプルな弾コンポーネント。
/// ・一定方向に移動
/// ・所有者(Owner)を持つ
/// ・Reflector から Owner と進行方向を書き換えられるようにする
/// </summary>
[RequireComponent(typeof(Collider))]
public class Projectile : MonoBehaviour
{
[SerializeField] private ProjectileOwner owner = ProjectileOwner.None;
[SerializeField] private float speed = 10f;
[SerializeField] private Vector3 moveDirection = Vector3.forward;
[SerializeField] private float lifeTime = 5f;
private float _timer;
/// <summary>現在の所有者を公開(読み取り専用)</summary>
public ProjectileOwner Owner => owner;
private void Reset()
{
// Collider をトリガーにしておくと扱いやすい
var col = GetComponent<Collider>();
col.isTrigger = true;
}
private void Update()
{
// シンプルな直進弾
transform.position += moveDirection.normalized * speed * Time.deltaTime;
// 寿命で自動破棄
_timer += Time.deltaTime;
if (_timer >= lifeTime)
{
Destroy(gameObject);
}
}
/// <summary>
/// 弾の所有者を変更する
/// </summary>
public void SetOwner(ProjectileOwner newOwner)
{
owner = newOwner;
}
/// <summary>
/// 進行方向を設定する
/// </summary>
public void SetDirection(Vector3 newDirection)
{
moveDirection = newDirection.normalized;
}
/// <summary>
/// 現在の進行方向を取得する
/// </summary>
public Vector3 GetDirection()
{
return moveDirection;
}
}
反射板コンポーネント:Reflector
いよいよ本題の Reflector です。役割は「弾に触れたときに、条件に応じて所有者と方向を反転させる」だけに絞ります。
using UnityEngine;
/// <summary>
/// 弾(Projectile)を反射し、所有者を自分側に書き換える反射板コンポーネント。
/// ・敵の弾だけを反射する
/// ・所有者を Player 側などに変更する
/// ・法線ベースの反射 or 単純反転を選べる
/// </summary>
[RequireComponent(typeof(Collider))]
public class Reflector : MonoBehaviour
{
[Header("反射設定")]
[SerializeField]
[Tooltip("この反射板が属する陣営。Enemy の弾だけを跳ね返して Player 側にするなど。")]
private ProjectileOwner reflectorOwner = ProjectileOwner.Player;
[SerializeField]
[Tooltip("どの所有者の弾を反射対象にするか。通常は Enemy。")]
private ProjectileOwner targetOwner = ProjectileOwner.Enemy;
[SerializeField]
[Tooltip("true の場合、接触面の法線ベクトルで物理的な反射を計算する")]
private bool useSurfaceNormalReflection = true;
[SerializeField]
[Tooltip("false の場合、この方向ベクトルを基準に単純反転させる")]
private Vector3 simpleReverseAxis = Vector3.forward;
[Header("デバッグ")]
[SerializeField]
[Tooltip("反射時にログを出す")]
private bool logOnReflect = false;
private Collider _collider;
private void Reset()
{
_collider = GetComponent<Collider>();
// 反射板はトリガーにしておくと扱いやすい(物理的な押し返しが不要な場合)
_collider.isTrigger = true;
}
private void Awake()
{
_collider = GetComponent<Collider>();
}
/// <summary>
/// 弾がトリガー内に侵入したときに呼ばれる
/// </summary>
private void OnTriggerEnter(Collider other)
{
TryReflectProjectile(other, null);
}
/// <summary>
/// 物理衝突を使いたい場合はこちら(isTrigger=false)
/// ※今回はトリガー前提なので必須ではないが、柔軟性のために実装しておく
/// </summary>
private void OnCollisionEnter(Collision collision)
{
// 最初のコンタクトポイントを使って法線を取得
ContactPoint contact = collision.GetContact(0);
TryReflectProjectile(collision.collider, contact.normal);
}
/// <summary>
/// 弾を反射できるなら反射するメイン処理
/// </summary>
private void TryReflectProjectile(Collider other, Vector3? hitNormal)
{
// Projectile コンポーネントを持っているか確認
if (!other.TryGetComponent<Projectile>(out var projectile))
{
return;
}
// 所有者がターゲットでなければ何もしない(例: 敵の弾だけ反射)
if (projectile.Owner != targetOwner)
{
return;
}
// 現在の進行方向を取得
Vector3 currentDir = projectile.GetDirection();
Vector3 newDir;
if (useSurfaceNormalReflection && hitNormal.HasValue)
{
// 物理的な反射:R = I - 2 * (I・N) * N
newDir = Vector3.Reflect(currentDir, hitNormal.Value.normalized);
}
else
{
// 単純反転:simpleReverseAxis に対して前後を反転させるイメージ
// ここでは simpleReverseAxis の符号を使って軸ごとに反転させる
Vector3 axis = simpleReverseAxis.normalized;
if (axis == Vector3.zero)
{
// 軸が 0 の場合は単純に逆向き
newDir = -currentDir;
}
else
{
// currentDir を axis 方向とそれ以外に分解し、axis 成分だけを反転
float dot = Vector3.Dot(currentDir, axis);
Vector3 axisComponent = axis * dot;
Vector3 tangentComponent = currentDir - axisComponent;
newDir = tangentComponent - axisComponent; // axis 成分を反転
}
}
// 所有者をこの反射板の陣営に変更
projectile.SetOwner(reflectorOwner);
// 新しい進行方向を設定
projectile.SetDirection(newDir);
if (logOnReflect)
{
Debug.Log(
$"[Reflector] {other.name} を反射しました。" +
$" Owner: {targetOwner} -> {reflectorOwner}, Dir: {currentDir} -> {newDir}",
this
);
}
}
}
オプション:プレイヤーに付ける「自分は Player だよ」コンポーネント
Reflector 自体は reflectorOwner に直接 ProjectileOwner.Player を指定すれば動きますが、プレイヤーなどに「自分の陣営」を持たせておくと、スクリプトの再利用性がさらに上がります。
using UnityEngine;
/// <summary>
/// ゲームオブジェクトの陣営情報を持つだけのシンプルなコンポーネント。
/// 弾生成時や Reflector 設定時に参照すると便利。
/// </summary>
public class Faction : MonoBehaviour
{
[SerializeField]
private ProjectileOwner owner = ProjectileOwner.Player;
public ProjectileOwner Owner => owner;
}
これを使う場合、Reflector 側を少しだけ改造して、Awake() で自動的に Faction から reflectorOwner を設定する、というパターンもアリです(後述の改造案で触れます)。
使い方の手順
ここからは、実際にシーンにコンポーネントを配置して動かす手順を見ていきましょう。
① 弾プレハブに Projectile を付ける
- 空の GameObject を作り、「EnemyBullet」などの名前を付けます。
- Add Component から
Projectileを追加します。 Projectileの設定例:- Owner:
Enemy - Speed:10 〜 20 くらい(お好みで)
- Move Direction:
(0, 0, 1)など - Life Time:3〜5 秒
- Owner:
- Collider(SphereCollider など)を追加し、
isTriggerをオンにします。ProjectileのReset()で自動的にオンになりますが、念のため確認しておきましょう。
- 必要なら MeshRenderer や SpriteRenderer を付けて見た目を整えます。
- この GameObject を Project ウィンドウにドラッグして Prefab 化します。
② 敵の発射スクリプトから弾を生成する
最低限の発射用スクリプト例です(敵にアタッチ)。
using UnityEngine;
/// <summary>単純に一定間隔で弾を撃つ敵の例</summary>
public class SimpleEnemyShooter : MonoBehaviour
{
[SerializeField] private Projectile projectilePrefab;
[SerializeField] private float interval = 1.0f;
[SerializeField] private Transform muzzle; // 発射位置
private float _timer;
private void Update()
{
_timer += Time.deltaTime;
if (_timer >= interval)
{
_timer = 0f;
Shoot();
}
}
private void Shoot()
{
if (projectilePrefab == null) return;
Transform spawnPoint = muzzle != null ? muzzle : transform;
var proj = Instantiate(projectilePrefab, spawnPoint.position, spawnPoint.rotation);
// 念のため Owner と Direction を明示的に設定
proj.SetOwner(ProjectileOwner.Enemy);
proj.SetDirection(spawnPoint.forward);
}
}
この敵が撃った弾が、Reflector に当たるとプレイヤー側の弾に変わって跳ね返る、という流れになります。
③ 反射板オブジェクトに Reflector を付ける
- シーン内に反射板用の GameObject を作成します。
- 例: 「ReflectWall」「PlayerShield」など。
- Collider(BoxCollider, SphereCollider など)を追加します。
- バリアなら SphereCollider、壁なら BoxCollider が扱いやすいです。
- Add Component から
Reflectorを追加します。 Reflectorの設定例:- Reflector Owner:
Player(プレイヤー側の弾にしたい場合) - Target Owner:
Enemy(敵の弾だけを反射) - Use Surface Normal Reflection:オン(壁などで物理的な反射をしたい場合)
- Simple Reverse Axis:
(0, 0, 1)(バリアなどで単純反転させたい場合) - Log On Reflect:デバッグしたいときだけオン
- Reflector Owner:
- Collider の
isTriggerをオンにしておきます。- 壁のように物理的に押し返したい場合はオフにして、
OnCollisionEnter経由で反射させることもできます。
- 壁のように物理的に押し返したい場合はオフにして、
④ 具体的な使用例
- プレイヤーの周りを回るバリア
- プレイヤーの子オブジェクトとして「Shield」を作成。
- SphereCollider + Reflector を付け、
Reflector Owner = Player、Target Owner = Enemy。 - Animator や簡単な回転スクリプトでバリアを回転させると、当たった敵弾が全部プレイヤーの弾として跳ね返ります。
- ステージの反射壁
- 床や壁に BoxCollider + Reflector を付ける。
Use Surface Normal Reflection = trueにすると、ビリヤードのような自然な反射になります。- 敵弾だけを反射して、プレイヤー弾は素通りさせるなどの調整も簡単です。
- ボス戦のギミック
- ボスの周りに回転する「反射パネル」を設置。
- プレイヤーがうまく位置取りすると、ボスの弾をボス自身に跳ね返せる、というパターンも作れます。
メリットと応用
Reflector をコンポーネントとして切り出しておくと、
- 弾のロジックと反射ロジックが分離されるので、どちらかを変更しても影響範囲を限定しやすい
- Prefab 化しやすい:バリア用、壁用、ボスギミック用など、Reflector 設定を変えたバリエーションを量産できる
- レベルデザインが楽:ステージ上に「反射板プレハブ」をポンポン置くだけで、弾幕の流れを簡単に変えられる
- 陣営の追加が容易:
ProjectileOwnerに新しい陣営を足せば、味方NPC同士で弾をやり取りするようなギミックも作れる
といったメリットがあります。巨大な「弾管理クラス」にすべての条件分岐を書き連ねるより、
- 弾は弾の移動と所有者だけを知っている
- Reflector は「弾に触れたら反射する」だけを担当する
という小さな責務に分割したほうが、保守性も拡張性も高くなります。
改造案:Faction から自動で Reflector の陣営を設定する
プレイヤーや味方NPCなどに Faction コンポーネントを付けている場合、Reflector 側で自動的にその陣営を拾ってくると、プレハブの再利用性がさらに上がります。
Reflector に、例えばこんなメソッドを追加するだけで実現できます。
private void AutoAssignOwnerFromFaction()
{
// 自分自身、もしくは親オブジェクトに Faction が付いていれば、それを ReflectorOwner にする
if (TryGetComponent<Faction>(out var faction))
{
reflectorOwner = faction.Owner;
}
else if (transform.parent != null &&
transform.parent.TryGetComponent<Faction>(out var parentFaction))
{
reflectorOwner = parentFaction.Owner;
}
}
Awake() の中で AutoAssignOwnerFromFaction() を呼べば、プレイヤーにだけ Faction を付けておいて、バリアはプレイヤーの子オブジェクトにするだけで、自動的に「Player 陣営の Reflector」として振る舞ってくれます。
こうした小さなコンポーネントを組み合わせる設計に慣れていくと、プロジェクトが大きくなっても破綻しづらいので、ぜひ Reflector から取り入れてみてください。
