Unityを触り始めた頃は、つい何でもかんでも Update() に書いてしまいがちですよね。移動、入力、エフェクト、当たり判定、UI更新…すべてが1つのスクリプトに詰め込まれた「Godクラス」状態になると、次のような問題が出てきます。

  • どこを直せばいいか分からない(バグの原因が追いづらい)
  • 似たような処理を他のオブジェクトで使い回したいのに、コピペ地獄になる
  • ちょっとした仕様変更でも、大量のコードを読む必要が出てくる

そこでおすすめなのが、機能ごとに小さなコンポーネントに分ける設計です。今回紹介する 「LaserBeam」コンポーネント は、

  • Raycast2Dで障害物までの距離を計算し、
  • LineRendererでレーザーの見た目を描画し、
  • ヒットした相手に対して判定(ダメージなど)を行う

という「レーザー表現」にだけ責任を持つ、シンプルなコンポーネントです。プレイヤーでも敵でもタレットでも、「レーザーを撃ちたいオブジェクト」にこのコンポーネントをポン付けするだけで使い回せるようにしていきましょう。

【Unity】Raycast2Dでピタッと止まるレーザー!「LaserBeam」コンポーネント

フルコード


using UnityEngine;

/// <summary>
/// 2D物理を使ってレーザーを描画&判定するコンポーネント。
/// - Raycast2Dで障害物に当たるまでの距離を取得
/// - LineRendererでレーザーを描画
/// - ヒットした相手にIDamageableを通じてダメージを通知(任意)
///
/// 使い回しやすいように、
/// 「レーザーの見た目」と「レイの設定」と「ダメージ通知」だけに責任を持たせています。
/// </summary>
[RequireComponent(typeof(LineRenderer))]
public class LaserBeam : MonoBehaviour
{
    // レーザーの最大長さ(障害物がない場合はこの長さまで伸びる)
    [SerializeField] private float maxDistance = 10f;

    // レーザーが判定を行うLayerMask(例:Ground, Enemyなど)
    [SerializeField] private LayerMask hitLayers;

    // レーザーの太さ
    [SerializeField] private float lineWidth = 0.05f;

    // レーザーを撃つかどうか(外部からOn/Offできるように)
    [SerializeField] private bool isActive = true;

    // レーザーの向き(ローカル空間)。通常は右方向(Vector2.right)を基準にする。
    [SerializeField] private Vector2 localDirection = Vector2.right;

    // レーザーの開始オフセット(発射口の位置調整用)
    [SerializeField] private Vector2 localStartOffset = Vector2.zero;

    // 一定間隔でダメージを与えたい場合のクールタイム
    [SerializeField] private float damageInterval = 0.2f;

    // ダメージ量(IDamageableを実装した相手がいれば渡す)
    [SerializeField] private float damageAmount = 10f;

    // ヒット時にGizmosで可視化したい場合(デバッグ用)
    [SerializeField] private bool drawDebugGizmos = false;

    private LineRenderer lineRenderer;
    private float damageTimer;

    // 直近でヒットしたCollider2Dを保持(外部から参照したい場合に使える)
    public Collider2D LastHitCollider { get; private set; }

    // レーザーの現在のヒット位置(障害物がなければmaxDistance先)
    public Vector2 CurrentHitPoint { get; private set; }

    // レーザーの現在の開始位置(ワールド座標)
    public Vector2 CurrentStartPoint { get; private set; }

    private void Awake()
    {
        lineRenderer = GetComponent<LineRenderer>();

        // LineRendererの基本設定
        lineRenderer.positionCount = 2; // 始点と終点の2点
        lineRenderer.startWidth = lineWidth;
        lineRenderer.endWidth = lineWidth;
        lineRenderer.useWorldSpace = true; // ワールド座標で指定

        // マテリアルが未設定だと何も見えないことがあるので注意
        // ここではコード側からはマテリアルを設定せず、
        // インスペクター上で設定してもらう想定です。
    }

    private void Update()
    {
        if (!isActive)
        {
            // 無効化時はLineRendererを非表示にする
            if (lineRenderer.enabled)
            {
                lineRenderer.enabled = false;
            }
            return;
        }

        // 有効時はLineRendererを表示
        if (!lineRenderer.enabled)
        {
            lineRenderer.enabled = true;
        }

        UpdateLaser();
        UpdateDamageTimer();
    }

    /// <summary>
    /// レーザーの描画とRaycast2D判定を更新する。
    /// </summary>
    private void UpdateLaser()
    {
        // レーザーの始点(ワールド座標)を計算
        // localStartOffsetをTransformのローカル空間からワールド空間に変換
        Vector3 worldStart = transform.TransformPoint(localStartOffset);

        // レーザーの向き(ローカル方向ベクトルをワールド空間に変換)
        Vector2 worldDir = transform.TransformDirection(localDirection).normalized;

        // Raycast2Dで障害物をチェック
        RaycastHit2D hit = Physics2D.Raycast(worldStart, worldDir, maxDistance, hitLayers);

        Vector3 worldEnd;

        if (hit.collider != null)
        {
            // 何かに当たった場合はその地点でレーザーを止める
            worldEnd = hit.point;
            LastHitCollider = hit.collider;
            CurrentHitPoint = hit.point;

            TryApplyDamage(hit.collider);
        }
        else
        {
            // 何にも当たらなかった場合は最大距離まで伸ばす
            worldEnd = worldStart + (Vector3)(worldDir * maxDistance);
            LastHitCollider = null;
            CurrentHitPoint = worldEnd;
        }

        CurrentStartPoint = worldStart;

        // LineRendererに始点と終点を設定
        lineRenderer.SetPosition(0, worldStart);
        lineRenderer.SetPosition(1, worldEnd);
    }

    /// <summary>
    /// ダメージ用のタイマーを更新する。
    /// 一定間隔でしかダメージを与えないようにするための簡易実装。
    /// </summary>
    private void UpdateDamageTimer()
    {
        if (damageInterval > 0f)
        {
            damageTimer -= Time.deltaTime;
        }
        else
        {
            // 0以下なら常に即時ダメージ許可
            damageTimer = 0f;
        }
    }

    /// <summary>
    /// ヒットしたCollider2Dに対してダメージを試みる。
    /// 相手がIDamageableを実装していればダメージを渡す。
    /// </summary>
    /// <param name="targetCollider">ヒットしたCollider2D</param>
    private void TryApplyDamage(Collider2D targetCollider)
    {
        if (targetCollider == null) return;
        if (damageAmount <= 0f) return;

        // クールタイム中なら何もしない
        if (damageTimer > 0f) return;

        // IDamageableを探す(Collider2DのGameObjectまたは親にアタッチされている想定)
        IDamageable damageable = targetCollider.GetComponentInParent<IDamageable>();
        if (damageable != null)
        {
            damageable.TakeDamage(damageAmount);
            damageTimer = damageInterval;
        }
    }

    /// <summary>
    /// 外部からレーザーのOn/Offを切り替えるための公開メソッド。
    /// 例:プレイヤー入力や敵AIから呼び出す。
    /// </summary>
    public void SetActive(bool active)
    {
        isActive = active;
    }

    /// <summary>
    /// レーザーの向きを外部から変更する。
    /// 例:マウス方向やターゲット方向に向けたいときなど。
    /// </summary>
    /// <param name="worldDirection">ワールド座標系での方向ベクトル</param>
    public void SetDirection(Vector2 worldDirection)
    {
        if (worldDirection.sqrMagnitude < Mathf.Epsilon) return;

        // ワールド方向をローカル方向に変換して保存
        Vector3 localDir3 = transform.InverseTransformDirection(worldDirection.normalized);
        localDirection = localDir3;
    }

    private void OnDrawGizmosSelected()
    {
        if (!drawDebugGizmos) return;

        // エディタ上で選択しているときに、Raycastのラインを可視化
        Gizmos.color = Color.red;

        Vector3 worldStart = Application.isPlaying
            ? (Vector3)CurrentStartPoint
            : transform.TransformPoint(localStartOffset);

        Vector2 worldDir = Application.isPlaying
            ? (Vector2)transform.TransformDirection(localDirection).normalized
            : (Vector2)transform.TransformDirection(localDirection).normalized;

        Vector3 worldEnd = worldStart + (Vector3)(worldDir * maxDistance);
        Gizmos.DrawLine(worldStart, worldEnd);
    }
}

/// <summary>
/// レーザーからダメージを受け取りたいオブジェクトが実装するインターフェース。
/// 非必須ですが、あるとLaserBeamの再利用性が高まります。
/// </summary>
public interface IDamageable
{
    void TakeDamage(float amount);
}

使い方の手順

  1. LineRenderer付きのレーザー発射オブジェクトを作る
    • 空のGameObjectを作成し、名前を LaserGun などにします。
    • LineRenderer コンポーネントを追加します。
    • Material に Sprites/Default や任意のレーザー用マテリアルを設定し、色を赤や緑にしておきましょう。
    • LineRenderer の Position Count は 2 に、Use World Space にチェックを入れておきます(スクリプトでも上書きされますが、念のため)。
  2. LaserBeamコンポーネントをアタッチ&設定
    • 先ほどの LaserGunLaserBeam スクリプトを追加します。
    • Max Distance: レーザーの最大射程(例: 15)
    • Hit Layers: Ground や Enemy など、レーザーを当てたいレイヤーを選択
    • Line Width: レーザーの太さ(例: 0.05)
    • Local Direction: デフォルトで右向き(1,0)ならそのままでOK。オブジェクトの右方向にレーザーが伸びます。
    • Local Start Offset: 発射口の位置を少し前に出したい場合は (0.2, 0) などに設定
    • Damage Interval: 0.2 なら 0.2秒ごとにダメージを与えます。常時ダメージにしたい場合は 0 にします。
    • Damage Amount: 与えるダメージ量(例: 5)
  3. 敵やプレイヤー側にIDamageableを実装する
    例えば、敵キャラに簡単なダメージ処理を追加します。
    
    using UnityEngine;
    
    public class SimpleEnemy : MonoBehaviour, IDamageable
    {
        [SerializeField] private float maxHp = 30f;
        private float currentHp;
    
        private void Awake()
        {
            currentHp = maxHp;
        }
    
        public void TakeDamage(float amount)
        {
            currentHp -= amount;
            Debug.Log($"{name} は {amount} ダメージを受けた。残りHP: {currentHp}");
    
            if (currentHp <= 0f)
            {
                Die();
            }
        }
    
        private void Die()
        {
            Debug.Log($"{name} は倒された!");
            Destroy(gameObject);
        }
    }
        
    • この SimpleEnemy を敵のプレハブにアタッチし、Collider2D(Box, Circle, Polygon など)を付けておきます。
    • 敵のレイヤーを Enemy に設定し、LaserBeam の Hit Layers に含めてください。
  4. プレイヤーやタレットからレーザーを制御する
    例として、「常に前方にレーザーを照射し続けるタレット」と「右クリックでレーザーをON/OFFするプレイヤー」の2パターンを紹介します。

    例1: 常時レーザーを出す固定タレット

    
    using UnityEngine;
    
    public class LaserTurret : MonoBehaviour
    {
        [SerializeField] private LaserBeam laserBeam;
    
        private void Reset()
        {
            // 同じGameObjectに付いているLaserBeamを自動取得(忘れ防止)
            if (laserBeam == null)
            {
                laserBeam = GetComponent<LaserBeam>();
            }
        }
    
        private void Update()
        {
            // タレットは常にレーザーを照射し続ける例
            if (laserBeam != null)
            {
                laserBeam.SetActive(true);
            }
        }
    }
        

    例2: マウス方向を向いて右クリックでレーザーON/OFFするプレイヤー

    
    using UnityEngine;
    
    public class PlayerLaserController : MonoBehaviour
    {
        [SerializeField] private LaserBeam laserBeam;
        [SerializeField] private Camera mainCamera;
    
        private void Awake()
        {
            if (mainCamera == null)
            {
                mainCamera = Camera.main;
            }
        }
    
        private void Update()
        {
            if (laserBeam == null || mainCamera == null) return;
    
            // マウス位置方向にレーザーを向ける
            Vector3 mousePos = Input.mousePosition;
            Vector3 worldMouse = mainCamera.ScreenToWorldPoint(mousePos);
            worldMouse.z = transform.position.z;
    
            Vector2 dir = (worldMouse - transform.position).normalized;
            laserBeam.SetDirection(dir);
    
            // 右クリックでレーザーON/OFF
            if (Input.GetMouseButtonDown(1))
            {
                laserBeam.SetActive(true);
            }
            else if (Input.GetMouseButtonUp(1))
            {
                laserBeam.SetActive(false);
            }
        }
    }
        
    • PlayerLaserController をプレイヤーオブジェクトにアタッチし、LaserBeam に LaserGun(レーザー発射口のオブジェクト)を割り当てます。
    • これで、右クリックを押している間だけレーザーが照射され、敵に当たれば IDamageable を通じてHPが減る仕組みになります。

メリットと応用

LaserBeam をコンポーネントとして切り出すことで、次のようなメリットがあります。

  • プレハブ化しやすい:レーザーの見た目・射程・ダメージ量などをまとめた「レーザー発射口」プレハブを1つ作っておけば、プレイヤー・敵・ギミックに簡単に再利用できます。
  • レベルデザインが楽になる:シーン上にタレット用の空オブジェクトをポンポン配置して、向きとレイヤーだけ調整すれば、レーザー罠ステージを素早く組み立てられます。
  • 責務が明確:レーザーの描画と当たり判定は LaserBeam に任せ、入力処理はプレイヤー用スクリプト、AI制御は敵用スクリプト…と分けられるので、巨大な Update() を避けられます。
  • 見た目の調整がしやすい:LineRendererのマテリアルや幅、色をインスペクターから変えるだけで、ビーム・レーザー・電撃など表現を差し替えられます。

応用としては、

  • ヒットしている間だけ、敵をノックバックさせる
  • レーザーの長さに応じてエネルギー消費や音量を変える
  • チャージショットとして、一定時間チャージ後に太いレーザーを一瞬だけ出す

など、いろいろなギミックに発展させられます。

最後に、簡単な「チャージレーザー」用の改造案を載せておきます。LaserBeam 自体はそのままにして、外側のコントローラだけを差し替えるイメージです。


/// <summary>
/// 一定時間チャージしてから、短時間だけレーザーを照射するコントローラの例。
/// LaserBeamコンポーネントはそのまま再利用し、責務を分離しています。
/// </summary>
public class ChargedLaserController : MonoBehaviour
{
    [SerializeField] private LaserBeam laserBeam;
    [SerializeField] private float chargeTime = 1.5f;
    [SerializeField] private float fireDuration = 0.5f;

    private float chargeTimer;
    private float fireTimer;
    private bool isCharging;
    private bool isFiring;

    private void Update()
    {
        if (laserBeam == null) return;

        // 左クリックでチャージ開始
        if (Input.GetMouseButtonDown(0) && !isCharging && !isFiring)
        {
            isCharging = true;
            chargeTimer = 0f;
        }

        if (isCharging)
        {
            chargeTimer += Time.deltaTime;
            if (chargeTimer >= chargeTime)
            {
                // チャージ完了で発射
                isCharging = false;
                isFiring = true;
                fireTimer = 0f;
                laserBeam.SetActive(true);
            }
        }

        if (isFiring)
        {
            fireTimer += Time.deltaTime;
            if (fireTimer >= fireDuration)
            {
                // 発射終了
                isFiring = false;
                laserBeam.SetActive(false);
            }
        }
    }
}

このように、「レーザーそのものの挙動」は LaserBeam に、「いつ・どのくらい撃つか」は別コンポーネントに分けておくと、ゲームの仕様変更にも柔軟に対応しやすくなります。小さな責務ごとにコンポーネントを分けて、使い回ししやすいレーザーシステムを育てていきましょう。