Unityを触り始めた頃って、つい何でもかんでも Update() に書いてしまいがちですよね。
プレイヤーの移動も、UIの更新も、ライトの制御も、全部ひとつのスクリプトに詰め込んでしまう…。
その結果、ちょっと仕様を変えたいだけなのに、巨大なスクリプトの中から目的の処理を探し出し、慎重に修正して…と、どんどんメンテナンスがつらくなっていきます。
そこでこの記事では、「暗闇エリアで周囲を照らすランタン」という、よくあるゲームの要素を
単機能のコンポーネントとして切り出した LanternLight を実装してみます。
プレイヤーの移動ロジックとは切り離して、ライトの挙動と燃料管理だけを担当するコンポーネントにすることで、
・明るさの調整
・燃料制限のオン/オフ
・燃え尽きたときの演出
などを、プレハブ単位で簡単にチューニングできるようにしていきましょう。
【Unity】暗闇探索を気持ちよく演出!「LanternLight」コンポーネント
今回は 2D 用ライトコンポーネント Light2D(Point Light 2D)を前提にしています。
Universal Render Pipeline(URP)の 2D Renderer プロジェクトで動かしてください。
フルソースコード
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Rendering.Universal; // Light2D を使うため
/// <summary>
/// 暗闇エリアで周囲を照らすランタン用コンポーネント。
/// ・Light2D(Point) の明るさ・半径を制御
/// ・任意で「燃料制限」による点灯時間を管理
/// ・燃料残量を 0~1 の割合で取得可能
/// ・燃え尽きたときのイベントを発火
///
/// ランタンの「見た目(スプライト)」や「プレイヤーの移動」とは分離し、
/// このコンポーネントは「ライトの振る舞い」だけを担当します。
/// </summary>
[RequireComponent(typeof(Light2D))]
public class LanternLight : MonoBehaviour
{
[Header("ライト基本設定")]
[SerializeField]
private float baseIntensity = 1.2f; // 通常時の明るさ
[SerializeField]
private float baseOuterRadius = 5f; // 通常時の照射半径
[SerializeField]
private float minOuterRadius = 1.5f; // 燃料ゼロ付近での最小半径
[SerializeField]
private bool startTurnedOn = true; // 開始時に点灯しておくか
[Header("燃料設定")]
[SerializeField]
private bool useFuel = true; // 燃料制限を使うかどうか(オフなら無限に点灯)
[SerializeField]
private float maxFuelSeconds = 60f; // 最大燃料量(秒)
[SerializeField]
private float initialFuelSeconds = 60f; // 開始時の燃料量(秒)
[SerializeField]
private float fuelConsumePerSecond = 1f; // 点灯中に毎秒消費する燃料量
[Header("フェード演出")]
[SerializeField]
private bool useFadeOutOnEmpty = true; // 燃料切れ時にフェードアウトさせるか
[SerializeField]
private float fadeOutDuration = 1.0f; // フェードアウトにかける時間
[Header("イベント")]
[SerializeField]
private UnityEvent onFuelEmpty; // 燃料が尽きた瞬間に呼ばれるイベント
// 内部状態
private Light2D _light2D;
private float _currentFuelSeconds;
private bool _isOn = false;
private bool _isFadingOut = false;
private float _fadeTimer = 0f;
private float _initialIntensity;
private float _initialOuterRadius;
/// <summary>現在の燃料割合(0~1)。燃料未使用モードなら常に 1 を返す。</summary>
public float FuelNormalized
{
get
{
if (!useFuel || maxFuelSeconds <= 0f) return 1f;
return Mathf.Clamp01(_currentFuelSeconds / maxFuelSeconds);
}
}
/// <summary>現在ランタンが点灯しているかどうか。</summary>
public bool IsOn => _isOn;
private void Awake()
{
_light2D = GetComponent<Light2D>();
// Light2D の初期値を記録(エディタ上で調整した値をベースにする)
_initialIntensity = _light2D.intensity;
_initialOuterRadius = _light2D.pointLightOuterRadius;
// baseIntensity / baseOuterRadius が 0 以下なら、Light2D の値をそのまま使う
if (baseIntensity > 0f)
{
_initialIntensity = baseIntensity;
}
if (baseOuterRadius > 0f)
{
_initialOuterRadius = baseOuterRadius;
}
// 燃料初期化
_currentFuelSeconds = Mathf.Clamp(initialFuelSeconds, 0f, maxFuelSeconds);
// 開始時の点灯状態を反映
if (startTurnedOn)
{
TurnOn();
}
else
{
TurnOff(immediate: true);
}
}
private void Update()
{
if (!_isOn)
{
// 消灯中は何もしない
return;
}
// 燃料を使う設定なら消費を進める
if (useFuel)
{
UpdateFuel();
}
// フェードアウト中なら進行
if (_isFadingOut)
{
UpdateFadeOut();
}
else
{
// 燃料が減るにつれて半径を少しずつ縮める演出
UpdateLightByFuel();
}
}
/// <summary>
/// ランタンを点灯させる。
/// 燃料が 0 の場合は点灯できない。
/// </summary>
public void TurnOn()
{
if (useFuel && _currentFuelSeconds <= 0f)
{
// 燃料切れなら点灯不可
return;
}
_isOn = true;
_isFadingOut = false;
_fadeTimer = 0f;
// ライトを有効化し、明るさ・半径を元に戻す
_light2D.enabled = true;
_light2D.intensity = _initialIntensity;
_light2D.pointLightOuterRadius = Mathf.Lerp(minOuterRadius, _initialOuterRadius, FuelNormalized);
}
/// <summary>
/// ランタンを消灯させる。
/// immediate = true の場合、フェードなしで即座に消灯。
/// </summary>
public void TurnOff(bool immediate = false)
{
_isOn = false;
_isFadingOut = false;
_fadeTimer = 0f;
if (immediate)
{
_light2D.enabled = false;
}
else
{
// フェードアウトを開始(燃料切れと同じ演出を使う)
if (useFadeOutOnEmpty && fadeOutDuration > 0f)
{
_isFadingOut = true;
}
else
{
_light2D.enabled = false;
}
}
}
/// <summary>
/// 燃料を追加する(秒単位)。負の値を渡すと減らすことも可能。
/// </summary>
public void AddFuel(float seconds)
{
if (!useFuel) return;
_currentFuelSeconds = Mathf.Clamp(_currentFuelSeconds + seconds, 0f, maxFuelSeconds);
// 燃料が追加されたタイミングで、点灯中なら見た目を更新
if (_isOn && !_isFadingOut)
{
UpdateLightByFuel();
}
}
/// <summary>
/// 燃料を最大まで回復させる。
/// </summary>
public void RefillFuel()
{
if (!useFuel) return;
_currentFuelSeconds = maxFuelSeconds;
if (_isOn && !_isFadingOut)
{
UpdateLightByFuel();
}
}
/// <summary>
/// 燃料を経過時間に応じて減らす。
/// </summary>
private void UpdateFuel()
{
if (_currentFuelSeconds <= 0f)
{
// 既に燃料ゼロ
return;
}
_currentFuelSeconds -= fuelConsumePerSecond * Time.deltaTime;
if (_currentFuelSeconds <= 0f)
{
_currentFuelSeconds = 0f;
HandleFuelEmpty();
}
}
/// <summary>
/// 燃料が尽きたときの処理。
/// </summary>
private void HandleFuelEmpty()
{
// 一度だけ呼びたいので、明示的に状態を切り替える
_isOn = false;
if (useFadeOutOnEmpty && fadeOutDuration > 0f)
{
// フェードアウト開始
_isFadingOut = true;
_fadeTimer = 0f;
}
else
{
// 即座に消灯
_light2D.enabled = false;
}
// イベント発火(UI で「ランタンが消えた!」表示などに使える)
onFuelEmpty?.Invoke();
}
/// <summary>
/// 燃料残量に応じてライトの半径を変化させる。
/// </summary>
private void UpdateLightByFuel()
{
// 燃料を使わないモードなら常に一定
if (!useFuel)
{
_light2D.intensity = _initialIntensity;
_light2D.pointLightOuterRadius = _initialOuterRadius;
return;
}
float t = FuelNormalized;
// 残量 1.0 -> 半径最大、0.0 -> 最小半径まで縮む
float radius = Mathf.Lerp(minOuterRadius, _initialOuterRadius, t);
_light2D.intensity = _initialIntensity; // 明るさ自体は一定にしておく
_light2D.pointLightOuterRadius = radius;
}
/// <summary>
/// フェードアウト演出を進める。
/// </summary>
private void UpdateFadeOut()
{
if (!_isFadingOut)
return;
_fadeTimer += Time.deltaTime;
float t = Mathf.Clamp01(_fadeTimer / fadeOutDuration);
float alpha = 1f - t;
// 明るさと半径を徐々に落とす
_light2D.intensity = _initialIntensity * alpha;
_light2D.pointLightOuterRadius = Mathf.Lerp(minOuterRadius, 0.1f, t);
if (t >= 1f)
{
// 完全に消灯
_isFadingOut = false;
_light2D.enabled = false;
}
}
}
使い方の手順
ここでは「プレイヤーが持つランタン」として使う例で説明しますが、敵キャラや動く足場にも同じ要領で付けられます。
-
Point Light 2D を用意する
- Unity で URP(2D Renderer)のプロジェクトを作成します。
- シーン上に空の GameObject を作成し、名前を
Lanternなどにします。 Add ComponentからLight2Dを追加し、Light Typeを Point に設定します。- 色や半径、Intensity はだいたいの値で構いません(後でコンポーネント側から上書きできます)。
-
LanternLight コンポーネントをアタッチ
- 同じ GameObject に、上記の
LanternLightスクリプトを追加します。 - インスペクターで以下を調整します:
Base Intensity:ランタンの明るさ(例: 1.2)Base Outer Radius:照らす範囲(例: 5)Min Outer Radius:燃料ゼロ近くでの最小範囲(例: 1.5)Use Fuel:燃料制限を使うならチェックMax Fuel Seconds:最大燃料(秒)。60 なら 1 分持続Initial Fuel Seconds:開始時の燃料量Fuel Consume Per Second:1 秒あたりの消費量Use Fade Out On Empty:燃料切れ時にフェードアウトしたい場合はチェック
On Fuel Emptyイベントに、UI の表示や SE 再生などを登録しておくと、ランタンが消えたときの演出が簡単に作れます。
- 同じ GameObject に、上記の
-
プレイヤーにランタンを持たせる
- プレイヤーの GameObject の子として
Lanternを配置します(Transform の位置をプレイヤーの少し前あたりに)。 - これで、プレイヤーが移動するだけでランタンも一緒に動き、周囲を照らしてくれます。
- プレイヤーの移動スクリプト側では、ランタンのことは一切意識しなくてOKです。
- プレイヤーの GameObject の子として
-
ゲーム中に燃料を回復させる(例:拾ったアイテムで補充)
例えば「オイルボトル」を拾ったときにランタンの燃料を回復させたい場合、
アイテム側のスクリプトから以下のように呼び出せます。public class LanternFuelPickup : MonoBehaviour { [SerializeField] private float fuelAmountSeconds = 20f; // 何秒分の燃料を回復させるか private void OnTriggerEnter2D(Collider2D other) { // プレイヤーにぶつかったら燃料補充 var lantern = other.GetComponentInChildren<LanternLight>(); if (lantern == null) return; lantern.AddFuel(fuelAmountSeconds); // 必要なら即点灯させる if (!lantern.IsOn) { lantern.TurnOn(); } Destroy(gameObject); // アイテムを消す } }これで、プレイヤーがオイルを拾うたびにランタンの寿命が延びる仕組みが簡単に作れます。
メリットと応用
LanternLight をプレイヤーや敵、ギミックのプレハブに仕込んでおくと、レベルデザインとバランス調整がかなり楽になります。
- プレハブ単位で光源をコントロール
敵ごとに「視界の広い見張り」「視界の狭い雑魚」などを作りたい場合、
それぞれのプレハブにLanternLightを付けてBase Outer Radiusだけ変えれば OK です。 - 燃料制限のオン/オフをトグルするだけ
「チュートリアルステージでは燃料無限」「本編では燃料制限あり」のような切り替えも、
シーンごとにUse Fuelのチェックを変えるだけで完了します。 - 責務が分かれているので拡張しやすい
ランタンは「光を出す」だけに専念しているので、
「拾ったときのアニメーション」「プレイヤーの持ち替えモーション」などは別コンポーネントに切り出しやすくなります。
さらに、ちょっとした改造で「プレイヤーの恐怖度」をライトの明るさに反映させる、といった演出も可能です。
例えば、以下のようなメソッドを LanternLight に追加して、外部から呼び出す形にしてみましょう。
/// <summary>
/// 恐怖度(0~1)に応じて一時的にランタンを暗くする。
/// fear = 0 なら通常、1 なら半分の明るさまで下げる。
/// </summary>
public void ApplyFearEffect(float fear)
{
float clamped = Mathf.Clamp01(fear);
float factor = Mathf.Lerp(1f, 0.5f, clamped); // 最大で 50% まで暗くする
_light2D.intensity = _initialIntensity * factor;
}
プレイヤーの HP や「SAN 値」などを管理する別コンポーネントから、この ApplyFearEffect() を呼ぶだけで、
心理状態がライト演出に自然に反映されるようになります。
このように、小さなコンポーネント同士を組み合わせていくと、巨大な God クラスに頼らない設計がどんどんやりやすくなりますね。




