// ============================================================
// 文件: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;
/// 当前鱼使用的路线 ID(Init 时保存,用于房主把活鱼广播给新加入玩家)
public string lastMoveLineId;
/// 运行时数据(血量、路径、配置等)
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;
/// 设置冰冻特效激活状态
public void SetFreezeEffect(bool active)
{
if (bingDongEffect != null)
bingDongEffect.SetActive(active);
}
/// 是否为 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;
/// 锁定点(colliderList 为空时使用,null 则用自身坐标)
public Transform lockPoint;
/// 手动锁定时记录被点击的碰撞器,用于锁定锚点优先级(运行时赋值,不序列化)
[System.NonSerialized]
public Transform hitCollider;
/// 冰冻特效(预制体内子物体,null 则跳过)
public GameObject bingDongEffect;
[Header("创建视频(可选)")]
[Tooltip("鱼创建时播放的入场视频片段,播放结束后才开始移动。为空则无入场视频。播放控件由 GameMainPanel_FishBase 提供")]
public UnityEngine.Video.VideoClip createVideoClip;
/// createVideoClip 实际播放时长(秒),用于计算 moveTime 偏移。<=0 表示未播放/无视频
protected float _createVideoDuration = 0f;
/// 当前是否正在播放创建视频(由外部播放器 isPlaying 判断)
protected bool _isPlayingCreateVideo = false;
/// 是否存活(活跃且未被回收)
public bool IsAlive => gameObject.activeInHierarchy;
public int rotationPY = 60;
// ================================================================
// Unity 生命周期
// ================================================================
public bool baseMmove;
public virtual void Awake()
{
LoadHitEffect();
baseMmove = isMove;
}
public async void LoadHitEffect()
{
// 查找受击特效子物体并默认隐藏
var hitTra = FindDeepChild(transform, "HitEffect");
if (hitTra != null)
{
hurt = hitTra.gameObject;
hurt.SetActive(false);
}
}
public virtual void OnEnable()
{
// ⭐ 用 baseMmove 恢复 isMove:Awake 时记录预制体原始 isMove 状态。
// - 普通鱼预制体 isMove=true → 池回收后恢复 true,正常游动
// (之前被注释导致:冰冻时 FreezeAllFish 设 isMove=false → 鱼回收 →
// 重新生成后残留 false → 鱼永远不动)
// - 爆奖 Boss 预制体 isMove=false → 恢复 false,固定在 (0,0,0) 播放动画
// (金龙/雷龙/玉龙等不走路径,直到爆奖结束销毁)
// - 冰冻中重新激活的鱼,末尾会覆盖为 false,安全
isMove = baseMmove;
fishenum = (FishTypeEnum)monsterData.fishTypeConfig.fishType;
uirootRect = UIManager.Instance.uiRoot.GetComponent();
mainCamera = Camera.main;
moveTime = 0f;
// ⭐ 重置创建视频状态:池回收的鱼 _isPlayingCreateVideo 可能残留 true,
// 重新生成时重置,由 Init 决定是否重新播放
_createVideoDuration = 0f;
_isPlayingCreateVideo = false;
InitAnimList();
ChangeState(CharacterStateType.move);
// ⭐ 同步冰冻特效状态:池回收的鱼 bingDongEffect 可能残留激活状态,
// 重新生成时会显示白色冰冻蒙皮。根据当前是否冰冻中决定显隐
bool frozen = CreateFishManager.Instance != null && CreateFishManager.Instance.isFrozen;
SetFreezeEffect(frozen);
if (frozen)
{
// 冰冻中重新生成的鱼也要被冻结
isMove = false;
ChangeState(CharacterStateType.idle);
}
}
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 = "")
{
// ⭐ 保存 pathId,用于房主把活鱼广播给新加入玩家
lastMoveLineId = pathId;
// 根据是否为鱼阵选择路线 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;
// isMove=false 的鱼不需要移动路线,直接归位到 (0,0,0)
if (!isMove)
transform.position = Vector3.zero;
// ⭐ 创建视频启动逻辑由子类负责:
// - MonsterControl_BossBase 在自己 Init 里启动 createVideo
// - 普通鱼走父类 Init,但普通鱼一般不配 createVideo
// OnMoveUpdata 会检测 createVideo.isPlaying,视频播放期间不移动
// 路径结束后自动销毁
//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()
{
if (!isMove) return; // 冰冻中不计时
// ⭐ 创建视频播放期间不移动、不累计 moveTime
// 视频结束后才开始走路线;视频时长不计入 moveTime(单独累计到 _createVideoDuration)
// 隐藏控件由 loopPointReached 回调负责(OnCreateVideoFinished)
if (_isPlayingCreateVideo)
{
_createVideoDuration += Time.deltaTime;
return;
}
moveTime += Time.deltaTime;
if (isyz)
{
UpdateYZMovement();
}
else
{
UpdateNormalMovement();
}
if (moveTime>=monsterData.moveLine.moveTime)
{
CreateFishManager.Instance.Id_DieFish_NotFX(guid);
}
}
/// 鱼阵移动:按 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;
public bool isNot1 = false;
///
/// 根据 forwordXYZ 字段将物体朝向目标点方向。
/// 为空时 2D 鱼用 up,3D 鱼用 forward。
///
/// 下一帧目标位置
private void ApplyFacingDirection(Vector2 nextPos)
{
Transform child = transform.GetChild(0);
Vector3 localPos = Vector3.zero;
localPos.z = depthOffset;
child.localPosition = localPos;
if (!isRotate) return;
Vector2 toNext = nextPos - (Vector2)transform.position;
if (toNext.sqrMagnitude < 0.0001f) return;
// 处理未指定朝向轴的情况(默认行为)
if (string.IsNullOrEmpty(forwordXYZ))
{
if (!is3D)
{
float angle = Mathf.Atan2(toNext.y, toNext.x) * Mathf.Rad2Deg;
transform.rotation = Quaternion.AngleAxis(angle - 90f, Vector3.forward);
}
else
{
transform.rotation = Quaternion.LookRotation(toNext.normalized, Vector3.up);
}
return;
}
// 是否为子物体单独控制朝向和深度
if (ischild0)
{
//// 父物体朝向
switch (forwordXYZ)
{
case "X":
{
transform.GetChild(0).right = toNext.normalized;
var vv = transform.GetChild(0).eulerAngles;
vv.x = rotationPY;
transform.GetChild(0).eulerAngles = vv;
}
break;
case "-X":
{
transform.GetChild(0).right = -toNext.normalized;
var vv = transform.GetChild(0).eulerAngles;
vv.x = -rotationPY;
transform.GetChild(0).eulerAngles = vv;
}
break;
case "Y":
{
transform.GetChild(0).up = toNext.normalized;
var vv = transform.GetChild(0).eulerAngles;
vv.y = rotationPY;
transform.GetChild(0).eulerAngles = vv;
}
break;
case "-Y":
{
transform.GetChild(0).up = -toNext.normalized;
var vv = transform.GetChild(0).eulerAngles;
vv.y = -rotationPY;
transform.GetChild(0).eulerAngles = vv;
}
break;
case "Z":
{
transform.GetChild(0).forward = toNext.normalized;
var vv = transform.GetChild(0).eulerAngles;
if (isNot1)
{
vv.z = rotationPY;
}
else
{
vv.z = rotationPY * (vv.y < 180 ? -1 : 1);
}
transform.GetChild(0).eulerAngles = vv;
}
break;
case "-Z":
{
transform.GetChild(0).forward = -toNext.normalized;
var vv = transform.GetChild(0).eulerAngles;
vv.z = -rotationPY * (vv.y < 180 ? -1 : 1);
transform.GetChild(0).eulerAngles = vv;
}
break;
}
//// 子物体深度偏移:方向正 → 偏移正,方向负 → 偏移负
//switch (forwordXYZ)
//{
// case "X": localPos.z = depthOffset; break;
// case "-X": localPos.z = -depthOffset; break;
// case "Y": localPos.x = depthOffset; break;
// case "-Y": localPos.x = -depthOffset; break;
// case "Z": localPos.x = depthOffset; break;
// case "-Z": localPos.x = -depthOffset; break;
//}
}
else
{
// 控制父物体自身朝向
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;
}
}
}
public bool ischild0 = false;
public int depthOffset;
// ================================================================
// 动画
// ================================================================
/// 播放指定类型动画(需要先在 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;
}
}