Unityを始めたばかりの頃は、プレイヤーの移動もカメラ追従も当たり判定も、つい Update() の中に全部書いてしまいがちですよね。最初はそれでも動きますが、機能が増えるにつれて「どこを直せばいいのか分からない」「ちょっと直すと別のバグが出る」という沼にハマりやすくなります。
そこでおすすめなのが、「役割ごとに小さなコンポーネントに分ける」やり方です。今回紹介する OrbitalShield(ビットシールド) コンポーネントは、
- 親オブジェクトの周囲をぐるぐる回転するオプションパーツ
- 触れた敵弾(Bullet)を消すシールド
という機能だけに責務を絞った、シンプルなコンポーネントです。プレイヤー本体のスクリプトを太らせず、「回転するシールドの挙動」だけを独立したコンポーネントとして実装していきましょう。
【Unity】親の周りをぐるぐる防御!「OrbitalShield」コンポーネント
フルコード:OrbitalShield.cs
using UnityEngine;
/// <summary>
/// 親オブジェクトの周囲を公転し、敵弾と接触したら破壊するビットシールド。
/// ・親の周囲を一定半径で回り続ける
/// ・親が動いても常に追従する
/// ・敵弾(タグ指定)と接触したら、その弾を消す
/// </summary>
[RequireComponent(typeof(Collider))]
public class OrbitalShield : MonoBehaviour
{
// ====== 公転設定 ======
[Header("公転設定")]
[Tooltip("公転の中心となる Transform。通常はプレイヤーや本体オブジェクトを指定します。")]
[SerializeField] private Transform orbitCenter;
[Tooltip("公転半径。中心からどれくらい離れた位置を回るか。")]
[SerializeField] private float orbitRadius = 2.0f;
[Tooltip("公転の角速度(度/秒)。正の値で反時計回り、負の値で時計回り。")]
[SerializeField] private float angularSpeed = 90.0f;
[Tooltip("公転する面の法線ベクトル。通常は (0,1,0) で水平面上を回転。")]
[SerializeField] private Vector3 orbitAxis = Vector3.up;
[Header("当たり判定設定")]
[Tooltip("敵弾として扱うタグ名。このタグを持つオブジェクトと接触すると破壊します。")]
[SerializeField] private string enemyBulletTag = "EnemyBullet";
[Tooltip("敵弾を消したときに生成するエフェクト。不要なら null のままでOK。")]
[SerializeField] private GameObject hitEffectPrefab;
[Tooltip("敵弾を消したときに再生するサウンド。不要なら null のままでOK。")]
[SerializeField] private AudioClip hitSeClip;
[Header("オプション")]
[Tooltip("ゲーム開始時に自動で orbitCenter を親 Transform に設定するか。")]
[SerializeField] private bool useParentAsCenterOnStart = true;
[Tooltip("ゲーム開始時に現在位置から中心への方向を初期位相として使うか。")]
[SerializeField] private bool useCurrentOffsetAsInitialAngle = true;
// 内部用:現在の角度(度)
private float _currentAngle;
// 内部用:オーディオ再生
private AudioSource _audioSource;
private void Awake()
{
// AudioSource がなければ自動追加(サウンド再生用)
if (hitSeClip != null)
{
_audioSource = gameObject.GetComponent<AudioSource>();
if (_audioSource == null)
{
_audioSource = gameObject.AddComponent<AudioSource>();
_audioSource.playOnAwake = false;
}
}
// Collider をトリガーにしておく(物理的な押し戻しは不要なため)
Collider col = GetComponent<Collider>();
col.isTrigger = true;
}
private void Start()
{
// 親を自動で公転中心にするオプション
if (useParentAsCenterOnStart && orbitCenter == null && transform.parent != null)
{
orbitCenter = transform.parent;
}
// 公転中心が未設定の場合、警告を出してこのコンポーネントを無効化
if (orbitCenter == null)
{
Debug.LogWarning(
$"[OrbitalShield] orbitCenter が設定されていません。ゲームオブジェクト: {name}",
this);
enabled = false;
return;
}
// 公転軸がゼロベクトルの場合は up に補正
if (orbitAxis == Vector3.zero)
{
orbitAxis = Vector3.up;
}
// 初期角度の計算
if (useCurrentOffsetAsInitialAngle)
{
// 現在位置から中心へのオフセットを元に、初期角度を求める
Vector3 offset = transform.position - orbitCenter.position;
// orbitAxis の平面に射影して、2D的な角度を求める
// 1. orbitAxis を正規化
Vector3 axis = orbitAxis.normalized;
// 2. 任意の基底ベクトルを作る(軸と直交するベクトル)
Vector3 basisX;
if (Vector3.Dot(axis, Vector3.up) > 0.99f)
{
// 軸がほぼ上向きなら、別の軸を使う
basisX = Vector3.right;
}
else
{
basisX = Vector3.Cross(axis, Vector3.up).normalized;
}
// 3. basisX と axis から、直交する basisY を作る
Vector3 basisY = Vector3.Cross(axis, basisX).normalized;
// 4. offset をこの2軸上に投影して角度を算出
float x = Vector3.Dot(offset, basisX);
float y = Vector3.Dot(offset, basisY);
_currentAngle = Mathf.Atan2(y, x) * Mathf.Rad2Deg;
// 半径も現在の距離を使う
orbitRadius = offset.magnitude;
}
else
{
// 初期角度は 0 度。位置を強制的に再配置する
_currentAngle = 0f;
UpdateShieldPosition(0f); // deltaTime=0 で一度だけ座標更新
}
}
private void Update()
{
if (orbitCenter == null) return;
// 経過時間に応じて角度を進める
UpdateShieldPosition(Time.deltaTime);
}
/// <summary>
/// 角度を進めて、シールドの位置を更新する。
/// </summary>
private void UpdateShieldPosition(float deltaTime)
{
// 角度を更新(度単位)
_currentAngle += angularSpeed * deltaTime;
// 角度を 0-360 に正規化(オーバーフロー防止)
if (_currentAngle >= 360f || _currentAngle <= -360f)
{
_currentAngle %= 360f;
}
// orbitAxis を軸として角度分回転させるクォータニオン
Quaternion rotation = Quaternion.AngleAxis(_currentAngle, orbitAxis.normalized);
// 基本となる位置(中心から orbitRadius 分だけ離れた位置)
// ここでは orbitAxis と直交する任意の方向を基準にする
Vector3 baseDirection = GetBaseDirectionOnOrbitPlane();
Vector3 offset = rotation * baseDirection * orbitRadius;
transform.position = orbitCenter.position + offset;
// シールドの向きは、中心の方向を向かせる例(好みで変更可)
// 中心に向かって常に「内向き」を向く
transform.rotation = Quaternion.LookRotation(-offset.normalized, orbitAxis.normalized);
}
/// <summary>
/// 公転平面上の基準方向ベクトルを取得する。
/// orbitAxis と直交する任意の正規化ベクトルを返す。
/// </summary>
private Vector3 GetBaseDirectionOnOrbitPlane()
{
Vector3 axis = orbitAxis.normalized;
// 軸とあまり平行でないベクトルを選ぶ
Vector3 temp = Mathf.Abs(Vector3.Dot(axis, Vector3.up)) > 0.9f
? Vector3.right
: Vector3.up;
Vector3 baseDir = Vector3.Cross(axis, temp).normalized;
return baseDir;
}
// ====== 当たり判定(敵弾を消す) ======
private void OnTriggerEnter(Collider other)
{
// タグが一致しない場合は何もしない
if (!other.CompareTag(enemyBulletTag)) return;
// 敵弾を消す
Destroy(other.gameObject);
// ヒットエフェクトを出す
if (hitEffectPrefab != null)
{
Instantiate(hitEffectPrefab, other.transform.position, Quaternion.identity);
}
// ヒットSEを鳴らす
if (hitSeClip != null && _audioSource != null)
{
_audioSource.PlayOneShot(hitSeClip);
}
}
}
使い方の手順
ここでは「プレイヤーの周りを回るビットシールド」として使う例で説明します。
-
プレイヤーのセットアップ
すでにプレイヤーオブジェクトがある前提で進めます(例:Player)。
プレイヤーには特にこのコンポーネントへの依存はありませんが、Playerを公転の中心にします。 -
ビットシールド用のプレハブを作成
- 空の GameObject を作成し、名前を
OrbitalShieldに変更 - 見た目用に Sphere や小さな Mesh、Sprite などを子オブジェクトとして配置
Collider(SphereCollider など)を追加し、Is Trigger にチェック- 上記の
OrbitalShield.csを追加 - 必要なら
AudioSourceは自動追加されるので、特に手動で付けなくてもOK - 作成した GameObject を Project ウィンドウにドラッグしてプレハブ化
- 空の GameObject を作成し、名前を
-
敵弾(Bullet)のタグ設定
- 敵弾用プレハブ(例:
EnemyBullet)を選択 - Inspector の「Tag」を
EnemyBulletに設定(存在しなければ Add Tag で追加) - このタグ名を
OrbitalShieldのenemyBulletTagと一致させる - 敵弾は
Collider(Is Trigger はオフでOK)とRigidbodyを持っていると扱いやすいです
- 敵弾用プレハブ(例:
-
シーンに配置して動作確認
- シーン上の
Playerオブジェクトの子として、OrbitalShieldプレハブを配置 OrbitalShieldコンポーネントの設定を確認:Use Parent As Center On Start:チェック(親を自動で中心に)Use Current Offset As Initial Angle:チェック(今の位置を初期位相に)Orbit Radius:プレイヤーからどれくらい離すか(例: 2.0)Angular Speed:回転速度(例: 120 で素早く回転)Orbit Axis:通常は(0,1,0)で水平面上を回転Enemy Bullet Tag:EnemyBulletHit Effect Prefab:パーティクルなどがあればアサインHit Se Clip:ヒット音があればアサイン
- ゲームを再生し、敵弾がプレイヤーに向かって飛んでくるようにしてテスト
- ビットシールドに触れた弾が消え、エフェクトやSEが再生されれば成功です
- シーン上の
同じ要領で、
- ボスの周囲を回る破壊可能なオーブ
- 動く床の周りを回るギミック(触れるとダメージ or 足場になる)
- 拠点の周囲を回る防衛ドローン
などにも簡単に流用できます。
メリットと応用
OrbitalShield をコンポーネントとして切り出しておくと、次のようなメリットがあります。
- プレイヤーのスクリプトが肥大化しない
「移動」「攻撃」「シールド」「UI更新」などを1つのクラスに押し込まず、
シールドの挙動はOrbitalShield単体に閉じ込められます。 - プレハブとして量産しやすい
回転速度や半径、当たり判定の挙動をプレハブ側でパラメータ調整できるので、
FastShield、WideShield、SlowButStrongShieldなどを量産しやすくなります。 - レベルデザインが楽になる
シーン上に「ボスの周りを回るオーブ」をぽんぽん配置するだけで、
それっぽいギミックが簡単に作れます。中心となるオブジェクトを差し替えるだけで挙動を使い回せます。 - テストしやすい
シールドだけを単体でシーンに置いて、中心をダミーオブジェクトにすれば、
プレイヤーのロジックに依存せずに挙動テストができます。
さらに、簡単な改造で「耐久値付きシールド」や「時間で消えるシールド」にもできます。例えば、
/// <summary>
/// 一定数の敵弾を受けたらシールド自体が消えるようにする例。
/// OrbitalShield に追記してもいいし、別コンポーネントとして分けてもOK。
/// </summary>
[SerializeField] private int maxHitCount = 5;
private int _currentHitCount = 0;
private void OnTriggerEnter(Collider other)
{
if (!other.CompareTag(enemyBulletTag)) return;
Destroy(other.gameObject);
// ヒット演出(元の処理)
if (hitEffectPrefab != null)
{
Instantiate(hitEffectPrefab, other.transform.position, Quaternion.identity);
}
if (hitSeClip != null && _audioSource != null)
{
_audioSource.PlayOneShot(hitSeClip);
}
// 耐久値を減らす
_currentHitCount++;
if (_currentHitCount >= maxHitCount)
{
// シールドを破壊
Destroy(gameObject);
}
}
このように、1つのコンポーネントに「公転」「敵弾消去」という明確な責務だけを持たせておけば、
耐久値・リチャージ・色変更などの追加要素は、別コンポーネントとして後からいくらでも足せます。
Godクラスを避けて、小さなコンポーネントを組み合わせる設計を意識していきましょう。
