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);
}
使い方の手順
-
LineRenderer付きのレーザー発射オブジェクトを作る
- 空のGameObjectを作成し、名前を
LaserGunなどにします。 LineRendererコンポーネントを追加します。- Material に
Sprites/Defaultや任意のレーザー用マテリアルを設定し、色を赤や緑にしておきましょう。 - LineRenderer の
Position Countは 2 に、Use World Spaceにチェックを入れておきます(スクリプトでも上書きされますが、念のため)。
- 空のGameObjectを作成し、名前を
-
LaserBeamコンポーネントをアタッチ&設定
- 先ほどの
LaserGunにLaserBeamスクリプトを追加します。 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)
- 先ほどの
-
敵やプレイヤー側に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に含めてください。
- この
-
プレイヤーやタレットからレーザーを制御する
例として、「常に前方にレーザーを照射し続けるタレット」と「右クリックでレーザーを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 に、「いつ・どのくらい撃つか」は別コンポーネントに分けておくと、ゲームの仕様変更にも柔軟に対応しやすくなります。小さな責務ごとにコンポーネントを分けて、使い回ししやすいレーザーシステムを育てていきましょう。
