diff --git a/unity/Assets/2.5D Engine/Scripts/Player.cs b/unity/Assets/2.5D Engine/Scripts/Player.cs
index 057531d..365b9bd 100644
--- a/unity/Assets/2.5D Engine/Scripts/Player.cs
+++ b/unity/Assets/2.5D Engine/Scripts/Player.cs
@@ -4,7 +4,7 @@ using System.Collections;
namespace IndianOceanAssets.Engine2_5D
{
// Ensures the GameObject has these essential components
- [RequireComponent(typeof(HealthSystem), typeof(SwordAttack), typeof(ProjectileShooter))]
+ [RequireComponent(typeof(HealthSystem), typeof(SwordAttack), typeof(ProjectileShooter), typeof(SpiritLanternSystem))]
public class PlayerController : MonoBehaviour
{
[Header("Movement")]
diff --git a/unity/Assets/Editor/SpiritLanternPrefabCreator.cs b/unity/Assets/Editor/SpiritLanternPrefabCreator.cs
new file mode 100644
index 0000000..6e1cbff
--- /dev/null
+++ b/unity/Assets/Editor/SpiritLanternPrefabCreator.cs
@@ -0,0 +1,60 @@
+using UnityEngine;
+using UnityEditor;
+using IndianOceanAssets.Engine2_5D;
+
+///
+/// 编辑器工具:一键创建灵灯 Prefab。
+/// 菜单路径:Assets > 创建灵灯 Prefab
+///
+public static class SpiritLanternPrefabCreator
+{
+ [MenuItem("Assets/创建灵灯 Prefab")]
+ public static void Create()
+ {
+ // 创建根物体
+ GameObject root = new GameObject("SpiritLantern");
+
+ // SpriteRenderer — 灵灯外观(使用默认 Sprite,后续可替换为美术资源)
+ SpriteRenderer sr = root.AddComponent();
+ sr.color = new Color(1f, 0.85f, 0.3f, 0.9f); // 暖黄色灯光
+ sr.sortingOrder = 100; // 在遮罩之上渲染
+
+ // LightSource — 光照组件(复用现有光照系统,与主角遮罩效果一致)
+ LightSource lightSource = root.AddComponent();
+ SerializedObject so = new SerializedObject(lightSource);
+ so.FindProperty("radius").floatValue = 4f;
+ so.FindProperty("intensity").floatValue = 1f;
+ so.FindProperty("registerOnStart").boolValue = true;
+ so.ApplyModifiedProperties();
+
+ // SpiritLantern — 灵灯行为脚本
+ root.AddComponent();
+
+ // CircleCollider2D — 触发器,检测怪物接触
+ // 【碰撞检测说明】
+ // 灵灯是主动检测方:灵灯的 Trigger 检测进入的怪物 Collider2D
+ // 需确保 Edit > Project Settings > Physics 2D 碰撞矩阵中,
+ // 灵灯所在 Layer 与怪物 Layer 的交叉项已勾选
+ CircleCollider2D collider = root.AddComponent();
+ collider.isTrigger = true;
+ collider.radius = 0.5f;
+
+ // Rigidbody2D — 物理碰撞需要(Trigger 需要至少一方有 Rigidbody)
+ Rigidbody2D rb = root.AddComponent();
+ rb.isKinematic = true;
+ rb.bodyType = RigidbodyType2D.Kinematic;
+
+ // 设置 Layer 为 Default
+ root.layer = LayerMask.NameToLayer("Default");
+
+ // 保存为 Prefab
+ string prefabPath = "Assets/eco/Prefabs/SpiritLantern.prefab";
+ PrefabUtility.SaveAsPrefabAsset(root, prefabPath);
+
+ // 清理场景中的临时物体
+ Object.DestroyImmediate(root);
+
+ Debug.Log($"[SpiritLantern] Prefab 已创建: {prefabPath}");
+ EditorUtility.PingAsset(AssetDatabase.LoadMainAssetAtPath(prefabPath));
+ }
+}
diff --git a/unity/Assets/eco/SpiritLantern.cs b/unity/Assets/eco/SpiritLantern.cs
new file mode 100644
index 0000000..45e0abd
--- /dev/null
+++ b/unity/Assets/eco/SpiritLantern.cs
@@ -0,0 +1,116 @@
+using System;
+using UnityEngine;
+
+namespace IndianOceanAssets.Engine2_5D
+{
+ ///
+ /// 单个灵灯的行为组件 —— 挂在灵灯 Prefab 上。
+ /// 放置后照亮周围区域(通过 LightSource),
+ /// 怪物碰触时扣 1 点血并自毁回收,
+ /// 存在时间到期后自动回收。
+ ///
+ /// 【碰撞检测说明】
+ /// 灵灯自身持有 CircleCollider2D (isTrigger=true),是主动检测方。
+ /// 当怪物的 Collider2D 进入灵灯的触发区域时,OnTriggerEnter2D 被调用。
+ /// 需确保 Physics2D 碰撞矩阵中,灵灯所在 Layer 与怪物 Layer 可互相触发。
+ /// 灵灯不依赖 whatIsEnemy LayerMask,而是直接用 Tag/Component 判定目标。
+ ///
+ public class SpiritLantern : MonoBehaviour
+ {
+ [Header("存在时间")]
+ [Tooltip("灵灯放置后的存在时间(秒),到期自动回收")]
+ [SerializeField] private float lifetime = 10f;
+
+ [Header("伤害")]
+ [Tooltip("怪物碰触灵灯时受到的伤害")]
+ [SerializeField] private int damageOnContact = 1;
+
+ /// 灵灯被回收时触发(被 SpiritLanternSystem 订阅)
+ public event Action OnLanternRecalled;
+
+ private bool _recalled;
+ private SpiritLanternSystem _ownerSystem;
+
+ ///
+ /// 设置灵灯的所有者系统引用,用于回收时显式取消事件订阅。
+ /// 由 SpiritLanternSystem.PlaceLantern() 调用。
+ ///
+ public void SetOwner(SpiritLanternSystem system)
+ {
+ _ownerSystem = system;
+ }
+
+ private void Update()
+ {
+ // 存在时间倒计时
+ lifetime -= Time.deltaTime;
+ if (lifetime <= 0f)
+ {
+ Recall();
+ }
+ }
+
+ ///
+ /// 怪物碰触灵灯时触发:扣血 + 回收灵灯。
+ /// 灵灯的 Trigger 主动检测进入的碰撞体,排除玩家后对怪物造成伤害。
+ ///
+ private void OnTriggerEnter2D(Collider2D other)
+ {
+ // 排除玩家(玩家 Tag 为 "Player"),避免放置时与玩家碰撞体误触发
+ if (other.CompareTag("Player"))
+ return;
+
+ // 尝试获取怪物的 HealthSystem
+ HealthSystem health = other.GetComponent();
+ if (health != null)
+ {
+ health.Damage(damageOnContact);
+ Recall();
+ }
+ }
+
+ ///
+ /// 回收灵灯:显式取消事件订阅 → 触发通知 → 销毁 GameObject。
+ /// 使用 _recalled 标志防止重复触发。
+ ///
+ private void Recall()
+ {
+ if (_recalled) return;
+ _recalled = true;
+
+ // 1. 先通过 System 显式取消事件订阅,防止内存泄漏
+ if (_ownerSystem != null)
+ {
+ _ownerSystem.UnregisterLantern(this);
+ }
+
+ // 2. 通知其他可能的订阅者(如 UI 等)
+ OnLanternRecalled?.Invoke();
+ OnLanternRecalled = null;
+
+ // 3. 销毁灵灯 GameObject
+ Destroy(gameObject);
+ }
+
+ ///
+ /// 安全网:如果灵灯被非正常销毁(场景卸载、外部 Destroy 等),
+ /// 确保事件仍然触发,避免 SpiritLanternSystem 的灵灯计数永久丢失。
+ ///
+ private void OnDestroy()
+ {
+ if (!_recalled)
+ {
+ _recalled = true;
+
+ // 先取消订阅,再触发通知
+ if (_ownerSystem != null)
+ {
+ _ownerSystem.UnregisterLantern(this);
+ }
+
+ OnLanternRecalled?.Invoke();
+ OnLanternRecalled = null;
+ }
+ }
+ }
+}
diff --git a/unity/Assets/eco/SpiritLanternSystem.cs b/unity/Assets/eco/SpiritLanternSystem.cs
new file mode 100644
index 0000000..fb42919
--- /dev/null
+++ b/unity/Assets/eco/SpiritLanternSystem.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Collections.Generic;
+using UnityEngine;
+
+namespace IndianOceanAssets.Engine2_5D
+{
+ ///
+ /// 灵灯系统 —— 挂在玩家物体上。
+ /// 按 U 键在玩家当前位置放置灵灯,灵灯照亮周围区域,
+ /// 怪物碰触灵灯会被扣 1 点血,灵灯消失并被回收。
+ /// 灵灯有存在时间,超时后自动回收。
+ ///
+ public class SpiritLanternSystem : MonoBehaviour
+ {
+ [Header("灵灯数量")]
+ [Tooltip("最大可持有灵灯数量")]
+ [SerializeField] private int maxLanterns = 5;
+
+ [Header("按键")]
+ [SerializeField] private KeyCode lanternKey = KeyCode.U;
+
+ [Header("冷却")]
+ [Tooltip("放置灵灯的冷却时间(秒)")]
+ [SerializeField] private float cooldown = 3f;
+
+ [Header("灵灯预制体")]
+ [SerializeField] private GameObject lanternPrefab;
+
+ private int _remainingLanterns;
+ private float _lastPlaceTime = -999f;
+
+ /// 跟踪所有已放置的灵灯,用于回收时取消事件订阅,防止内存泄漏
+ private readonly List _activeLanterns = new List();
+
+ /// 当前可释放的灵灯数量(供 UI 使用)
+ public int RemainingLanterns => _remainingLanterns;
+
+ /// 冷却是否就绪
+ public bool IsCooldownReady => Time.time > _lastPlaceTime + cooldown;
+
+ /// 冷却剩余时间(供 UI 使用)
+ public float CooldownRemaining => Mathf.Max(0f, (_lastPlaceTime + cooldown) - Time.time);
+
+ private void Start()
+ {
+ _remainingLanterns = maxLanterns;
+ }
+
+ private void Update()
+ {
+ // 按 U 且 CD 就绪 且 还有灵灯 → 放置
+ if (Input.GetKeyDown(lanternKey)
+ && _remainingLanterns > 0
+ && Time.time > _lastPlaceTime + cooldown
+ && lanternPrefab != null)
+ {
+ PlaceLantern();
+ }
+ }
+
+ ///
+ /// 在玩家当前位置放置一个灵灯。
+ ///
+ private void PlaceLantern()
+ {
+ _remainingLanterns--;
+ _lastPlaceTime = Time.time;
+
+ // 在玩家位置实例化灵灯
+ GameObject lanternObj = Instantiate(lanternPrefab, transform.position, Quaternion.identity);
+ SpiritLantern lantern = lanternObj.GetComponent();
+
+ if (lantern != null)
+ {
+ // 设置所有者引用,用于回收时取消事件订阅
+ lantern.SetOwner(this);
+ // 订阅灵灯回收事件,并跟踪该灵灯
+ lantern.OnLanternRecalled += OnLanternRecalled;
+ _activeLanterns.Add(lantern);
+ }
+ }
+
+ ///
+ /// 灵灯被回收时调用(怪物碰触或超时)。
+ /// 清理已销毁的灵灯引用,防止内存泄漏。
+ ///
+ private void OnLanternRecalled()
+ {
+ // 清理列表中已销毁的灵灯引用
+ _activeLanterns.RemoveAll(l => l == null);
+ _remainingLanterns = Mathf.Min(_remainingLanterns + 1, maxLanterns);
+ }
+
+ ///
+ /// 显式取消订阅指定灵灯的事件并从跟踪列表移除。
+ /// 由 SpiritLantern 在销毁前调用,确保事件订阅被正确释放。
+ ///
+ public void UnregisterLantern(SpiritLantern lantern)
+ {
+ if (lantern != null)
+ {
+ lantern.OnLanternRecalled -= OnLanternRecalled;
+ _activeLanterns.Remove(lantern);
+ }
+ }
+
+ ///
+ /// 安全网:玩家销毁或场景卸载时,清理所有灵灯的事件订阅。
+ ///
+ private void OnDestroy()
+ {
+ foreach (var lantern in _activeLanterns)
+ {
+ if (lantern != null)
+ lantern.OnLanternRecalled -= OnLanternRecalled;
+ }
+ _activeLanterns.Clear();
+ }
+
+ // ===== 编辑器可视化 =====
+ private void OnDrawGizmosSelected()
+ {
+ // 绘制冷却指示
+ if (Time.time < _lastPlaceTime + cooldown)
+ {
+ float remaining = (_lastPlaceTime + cooldown) - Time.time;
+ float ratio = remaining / cooldown;
+ Gizmos.color = new Color(1f, 0.6f, 0f, 0.3f);
+ Gizmos.DrawWireSphere(transform.position, 1f * ratio);
+ }
+ }
+ }
+}