// ============================================================ // 文件:MonsterContral.cs // 职责:鱼(怪物)实体基类 —— 状态机、路径移动、动画、死亡处理 // 优化点: // 1. 方向朝向逻辑提取为独立方法 ApplyFacingDirection,消除大量重复 switch // 2. OnMoveUpdata 按 isyz 分支,各分支内复用 ApplyFacingDirection // 3. OnDestroy 统一清理三个字典,去除重复判断 // 4. 常量命名使用 PascalCase,符合 C# 规范 // 5. 补充 XML 注释,明确方法职责 // 6. forwordXYZ 空检查改用 string.IsNullOrEmpty // ============================================================ using Assets.MyGame.Scripts.Net.API.GameConfig_SubClass; using Assets.MyGame.Scripts.Net.APIResponses.Enum; using Assets.MyGame.Scripts.Net.WebResponses; using Spine.Unity; using System; using System.Collections; using System.Collections.Generic; using TMPro; using UnityEngine; using static MonsterMoveLine; /// /// 鱼实体基类:封装状态机(移动/待机)、贝塞尔曲线路径跟随、动画播放、死亡逻辑。 /// 具体鱼类型(Boss等)通过继承重写虚方法扩展行为。 /// public class MonsterContral : MonoBehaviour { // ---------------------------------------------------------------- // 常量 // ---------------------------------------------------------------- /// 鱼阵路线 key 前缀 private const string YzMoveLineName = "鱼阵"; /// 普通鱼路线 key 前缀 private const string NormalMoveLineName = "曲线"; // ---------------------------------------------------------------- // 公开字段(Inspector 可见 / 外部赋值) // ---------------------------------------------------------------- /// /// 基础倍率 /// public int beilv; /// 本鱼唯一标识(服务器下发的 GUID) public string guid; /// 运行时数据(血量、路径、配置等) public MonsterDataBase monsterData = new MonsterDataBase(new MonsterHealthBase()); /// Spine 骨骼动画组件(非 Spine 鱼可为 null) public SkeletonAnimation skeleton; /// 是否使用 Spine 骨骼动画 public bool isSpine = true; /// 鱼类型 ID(对应 FishResourcesData 索引) public int fishTypeID; /// 动画状态 → 动画名称 映射 public Dictionary dic_AnimKeyAnimName = new Dictionary(); /// 当前状态 public CharacterStateType currState; /// 血量(目前未使用扣血逻辑,保留供扩展) public int HP = 5; /// 死亡粒子特效 Prefab(为 null 时使用 Manager 默认值) public GameObject effect_Die; /// 死亡金币奖励特效 Prefab public GameObject effect_DieAward; public GameObject currEffect_DieAward; public Action action_effect_DieAward; /// 是否处于移动状态(false 时 Update 跳过移动计算) public bool isMove = true; /// 是否为 3D 鱼(影响朝向轴选取) public bool is3D = false; /// /// 前进方向轴,可选 "X" / "-X" / "Y" / "-Y" / "Z" / "-Z"。 /// 为空时默认使用 up(2D)或 forward(3D)。 /// public string forwordXYZ; /// 鱼类型枚举(缓存,避免反复类型转换) public FishTypeEnum fishenum; /// 路径已行进的时间(秒),用于贝塞尔曲线插值;可由外部预设偏移实现同步追帧。 public float moveTime = 0f; /// 是否为鱼阵成员 public bool isyz = false; /// 所属鱼阵 ID(空字符串表示普通鱼) public string yzID = ""; // ---------------------------------------------------------------- // 私有字段 // ---------------------------------------------------------------- /// 受击闪烁特效对象(在子物体中查找 "HitEffect") private GameObject hurt; /// UI 根节点 RectTransform(坐标转换用) protected RectTransform uirootRect; /// 主相机缓存 protected Camera mainCamera; public List colliderList; /// 是否存活(活跃且未被回收) public bool IsAlive => gameObject.activeInHierarchy; // ================================================================ // Unity 生命周期 // ================================================================ public virtual void Awake() { LoadHitEffect(); } public async void LoadHitEffect() { // 查找受击特效子物体并默认隐藏 var hitTra = FindDeepChild(transform, "HitEffect"); if (hitTra != null) { hurt = hitTra.gameObject; hurt.SetActive(false); } } public virtual void OnEnable() { fishenum = (FishTypeEnum)monsterData.fishTypeConfig.fishType; uirootRect = UIManager.Instance.uiRoot.GetComponent(); mainCamera = Camera.main; moveTime=0f; InitAnimList(); ChangeState(CharacterStateType.move); } private void Update() { OnUpdate(currState); } /// /// 销毁时自动从 CreateFishManager 的三个字典中移除自身,避免野指针。 /// public virtual void OnDisable() { if (CreateFishManager.Instance == null) return; } // ================================================================ // 初始化 // ================================================================ /// 注册动画名称(子类可 override 扩展)。 public void InitAnimList() { dic_AnimKeyAnimName[CharacterAnimationType.move] = "stand2"; } /// /// 初始化路径并启动自动销毁计时。 /// /// 路线编号(与路线 key 前缀拼接) /// 路径持续时间(秒) /// 是否为鱼阵成员 /// 鱼阵 ID(isyuzhen==true 时有效) public virtual void Init(string pathId, float time, bool isyuzhen = false, string yzID = "") { // 根据是否为鱼阵选择路线 key string lineKey = isyuzhen ? YzMoveLineName + pathId : NormalMoveLineName + pathId; if (!MonsterMoveLine.dic_BezierCurveDatas.TryGetValue(lineKey, out var bezierData)) { Debug.LogError($"[MonsterContral] 找不到路线: {lineKey}"); return; } monsterData.moveLine.Init(bezierData, time); isyz = isyuzhen; this.yzID = yzID; // 鱼阵特殊处理:700 系扇形阵根据起点 y 值翻转朝向 if (isyuzhen && yzID.Length >= 3 && yzID.Substring(0, 3) == "700") { if (bezierData.start.y < 0) { var scale = transform.localScale; scale.y *= -1f; transform.localScale = scale; } } fishenum = (FishTypeEnum)monsterData.fishTypeConfig.fishType; // 路径结束后自动销毁 StartCoroutine(yanchi(gameObject,time, isyuzhen)); } public IEnumerator yanchi(GameObject obj,float time, bool isyuzhen) { yield return new WaitForSeconds(time); CreateFishManager.Instance.Id_DieFish_NotFX(guid); } // ================================================================ // 状态机 // ================================================================ /// 切换到新状态(先触发旧状态 Exit,再触发新状态 Enter)。 public void ChangeState(CharacterStateType stateType) { OnExit(currState); currState = stateType; OnEnter(currState); } public void OnEnter(CharacterStateType stateType) { switch (stateType) { case CharacterStateType.move: OnMoveEnter(); break; case CharacterStateType.idle: break; } } public void OnUpdate(CharacterStateType stateType) { if (!isMove) return; switch (stateType) { case CharacterStateType.move: OnMoveUpdata(); break; case CharacterStateType.idle: break; } } public void OnExit(CharacterStateType stateType) { // 当前无需处理,保留供子类 override } // ================================================================ // 移动逻辑 // ================================================================ public virtual void OnMoveEnter() { PlayAnim(CharacterAnimationType.move); } public virtual void OnMoveExit() { } /// /// 每帧更新鱼的位置(贝塞尔曲线插值)和朝向。 /// 鱼阵(isyz==true)按阵型 ID 前缀分支,普通鱼走统一逻辑。 /// public virtual void OnMoveUpdata() { moveTime += Time.deltaTime; if (isyz) { UpdateYZMovement(); } else { UpdateNormalMovement(); } } /// 鱼阵移动:按 yzID 前缀选择运动方程。 private void UpdateYZMovement() { if (yzID.Length < 3) return; string prefix = yzID.Substring(0, 3); switch (prefix) { // 700 / 701:直线向前移动(沿 up 方向) case "700": case "701": { var startWorld = CoordinateTransformation.CanvasToWorld( monsterData.moveLine.bezierCurveData.start, uirootRect, mainCamera); // 700:根据起点 y 值决定正方向;701:固定向上 bool goUp = (prefix == "701") || (monsterData.moveLine.bezierCurveData.start.y > 0); float dir = goUp ? -1f : 1f; Vector2 pos = startWorld + (Vector3)(dir * transform.up * moveTime * monsterData.moveLine.moveTime); transform.position = pos; break; } // 702 / 710:贝塞尔曲线路径 case "702": case "7007": case "710": // 顺序阵:每条路径逐一延迟生成 case "704": { Vector2 next = monsterData.moveLine.GetBezierPointJob(moveTime); if (fishenum != FishTypeEnum.Boss鱼) ApplyFacingDirection(next); transform.position = next; break; } default: break; } } /// 普通鱼移动:贝塞尔曲线路径 + 自动朝向。 private void UpdateNormalMovement() { Vector2 next = monsterData.moveLine.GetBezierPointJob(moveTime); if (fishenum != FishTypeEnum.Boss鱼) ApplyFacingDirection(next); transform.position = next; } public bool isRotate=true; /// /// 根据 forwordXYZ 字段将物体朝向目标点方向。 /// 为空时 2D 鱼用 up,3D 鱼用 forward。 /// /// 下一帧目标位置 private void ApplyFacingDirection(Vector2 nextPos) { if (!isRotate) return; Vector2 toNext = nextPos - (Vector2)transform.position; if (toNext.sqrMagnitude < 0.0001f) return; // 避免零向量 if (string.IsNullOrEmpty(forwordXYZ)) { if (!is3D) { // 2D 用 up,推荐用 Quaternion.AngleAxis float angle = Mathf.Atan2(toNext.y, toNext.x) * Mathf.Rad2Deg; transform.rotation = Quaternion.AngleAxis(angle - 90f, Vector3.forward); } else { // 3D 用 forward,推荐用 LookRotation transform.rotation = Quaternion.LookRotation(toNext.normalized, Vector3.up); } return; } switch (forwordXYZ) { case "X": transform.right = toNext.normalized; break; case "-X": transform.right = -toNext.normalized; break; case "Y": transform.up = toNext.normalized; break; case "-Y": transform.up = -toNext.normalized; break; case "Z": transform.forward = toNext.normalized; break; case "-Z": transform.forward = -toNext.normalized; break; } } // ================================================================ // 动画 // ================================================================ /// 播放指定类型动画(需要先在 dic_AnimKeyAnimName 中注册)。 public void PlayAnim(CharacterAnimationType animType, bool loop = true) { if (skeleton == null) return; if (!dic_AnimKeyAnimName.TryGetValue(animType, out var animName)) return; skeleton.AnimationState.SetAnimation(0, animName, loop); } // ================================================================ // 受击 & 死亡 // ================================================================ /// 触发受击闪烁(0.2s 后自动关闭)。 public void OnHurt() { if (hurt != null) StartCoroutine(HurtCoroutine()); } private IEnumerator HurtCoroutine() { hurt.SetActive(true); yield return new WaitForSeconds(0.2f); hurt.SetActive(false); } /// /// 执行死亡:播放音效并销毁 GameObject。 /// 子类可 override 添加额外逻辑(如 Boss 爆炸动画)。 /// public virtual void DieFish(RoomKillNotice obj = null) { if (FishResourcesData.fishResourcesDatasDic.TryGetValue(fishTypeID, out var res) && res.dieAudio != null && res.dieAudio.Count > 0) { AudioManager.Instance.PlayerFX(res.dieAudio[0]); } CreateFishManager.Instance.Id_DieFish_NotFX(guid); //Destroy(gameObject); } // ================================================================ // 工具方法 // ================================================================ /// 递归深度查找子物体(广度优先)。 private Transform FindDeepChild(Transform parent, string name) { foreach (Transform child in parent) { if (child.name == name) return child; var found = FindDeepChild(child, name); if (found != null) return found; } return null; } }