From 3c2f797f66509fcb096670d83b13833f5a15112e Mon Sep 17 00:00:00 2001 From: ksh999000 <740612340@qq.com> Date: Tue, 30 Jun 2026 22:21:55 +0800 Subject: [PATCH] =?UTF-8?q?=E7=81=B5=E7=81=AF=E7=B3=BB=E7=BB=9Fv0.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unity/Assets/2.5D Engine/Scripts/Player.cs | 2 +- .../Editor/SpiritLanternPrefabCreator.cs | 60 ++++++++ unity/Assets/eco/SpiritLantern.cs | 116 +++++++++++++++ unity/Assets/eco/SpiritLanternSystem.cs | 133 ++++++++++++++++++ 4 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 unity/Assets/Editor/SpiritLanternPrefabCreator.cs create mode 100644 unity/Assets/eco/SpiritLantern.cs create mode 100644 unity/Assets/eco/SpiritLanternSystem.cs 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); + } + } + } +}