Files
5.24BuYu/Assets/MyGame/Scripts/Characters/Monster/MonsterContral.cs
T
2026-07-31 15:58:50 +08:00

619 lines
22 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================
// 文件: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;
/// <summary>
/// 鱼实体基类:封装状态机(移动/待机)、贝塞尔曲线路径跟随、动画播放、死亡逻辑。
/// 具体鱼类型(Boss等)通过继承重写虚方法扩展行为。
/// </summary>
public class MonsterContral : MonoBehaviour
{
// ----------------------------------------------------------------
// 常量
// ----------------------------------------------------------------
/// <summary>鱼阵路线 key 前缀</summary>
private const string YzMoveLineName = "鱼阵";
/// <summary>普通鱼路线 key 前缀</summary>
private const string NormalMoveLineName = "曲线";
// ----------------------------------------------------------------
// 公开字段(Inspector 可见 / 外部赋值)
// ----------------------------------------------------------------
/// <summary>
/// 基础倍率
/// </summary>
public int beilv;
/// <summary>本鱼唯一标识(服务器下发的 GUID)</summary>
public string guid;
/// <summary>当前鱼使用的路线 ID(Init 时保存,用于房主把活鱼广播给新加入玩家)</summary>
public string lastMoveLineId;
/// <summary>运行时数据(血量、路径、配置等)</summary>
public MonsterDataBase monsterData = new MonsterDataBase(new MonsterHealthBase());
/// <summary>Spine 骨骼动画组件(非 Spine 鱼可为 null)</summary>
public SkeletonAnimation skeleton;
/// <summary>是否使用 Spine 骨骼动画</summary>
public bool isSpine = true;
/// <summary>鱼类型 ID(对应 FishResourcesData 索引)</summary>
public int fishTypeID;
/// <summary>动画状态 → 动画名称 映射</summary>
public Dictionary<CharacterAnimationType, string> dic_AnimKeyAnimName
= new Dictionary<CharacterAnimationType, string>();
/// <summary>当前状态</summary>
public CharacterStateType currState;
/// <summary>血量(目前未使用扣血逻辑,保留供扩展)</summary>
public int HP = 5;
/// <summary>死亡粒子特效 Prefab(为 null 时使用 Manager 默认值)</summary>
public GameObject effect_Die;
/// <summary>死亡金币奖励特效 Prefab</summary>
public GameObject effect_DieAward;
public GameObject currEffect_DieAward;
public Action<GameObject> action_effect_DieAward;
/// <summary>是否处于移动状态(false 时 Update 跳过移动计算)</summary>
public bool isMove = true;
/// <summary>设置冰冻特效激活状态</summary>
public void SetFreezeEffect(bool active)
{
if (bingDongEffect != null)
bingDongEffect.SetActive(active);
}
/// <summary>是否为 3D 鱼(影响朝向轴选取)</summary>
public bool is3D = false;
/// <summary>
/// 前进方向轴,可选 "X" / "-X" / "Y" / "-Y" / "Z" / "-Z"。
/// 为空时默认使用 up(2D)或 forward(3D)。
/// </summary>
public string forwordXYZ;
/// <summary>鱼类型枚举(缓存,避免反复类型转换)</summary>
public FishTypeEnum fishenum;
/// <summary>路径已行进的时间(秒),用于贝塞尔曲线插值;可由外部预设偏移实现同步追帧。</summary>
public float moveTime = 0f;
/// <summary>是否为鱼阵成员</summary>
public bool isyz = false;
/// <summary>所属鱼阵 ID(空字符串表示普通鱼)</summary>
public string yzID = "";
// ----------------------------------------------------------------
// 私有字段
// ----------------------------------------------------------------
/// <summary>受击闪烁特效对象(在子物体中查找 "HitEffect")</summary>
private GameObject hurt;
/// <summary>UI 根节点 RectTransform(坐标转换用)</summary>
protected RectTransform uirootRect;
/// <summary>主相机缓存</summary>
protected Camera mainCamera;
public List<Transform> colliderList;
/// <summary>锁定点(colliderList 为空时使用,null 则用自身坐标)</summary>
public Transform lockPoint;
/// <summary>手动锁定时记录被点击的碰撞器,用于锁定锚点优先级(运行时赋值,不序列化)</summary>
[System.NonSerialized]
public Transform hitCollider;
/// <summary>冰冻特效(预制体内子物体,null 则跳过)</summary>
public GameObject bingDongEffect;
[Header("创建视频(可选)")]
[Tooltip("鱼创建时播放的入场视频片段,播放结束后才开始移动。为空则无入场视频。播放控件由 GameMainPanel_FishBase 提供")]
public UnityEngine.Video.VideoClip createVideoClip;
/// <summary>createVideoClip 实际播放时长(秒),用于计算 moveTime 偏移。<=0 表示未播放/无视频</summary>
protected float _createVideoDuration = 0f;
/// <summary>当前是否正在播放创建视频(由外部播放器 isPlaying 判断)</summary>
protected bool _isPlayingCreateVideo = false;
/// <summary>是否存活(活跃且未被回收)</summary>
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<RectTransform>();
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);
}
/// <summary>
/// 销毁时自动从 CreateFishManager 的三个字典中移除自身,避免野指针。
/// </summary>
public virtual void OnDisable()
{
if (CreateFishManager.Instance == null) return;
}
// ================================================================
// 初始化
// ================================================================
/// <summary>注册动画名称(子类可 override 扩展)。</summary>
public void InitAnimList()
{
dic_AnimKeyAnimName[CharacterAnimationType.move] = "stand2";
}
/// <summary>
/// 初始化路径并启动自动销毁计时。
/// </summary>
/// <param name="pathId">路线编号(与路线 key 前缀拼接)</param>
/// <param name="time">路径持续时间(秒)</param>
/// <param name="isyuzhen">是否为鱼阵成员</param>
/// <param name="yzID">鱼阵 ID(isyuzhen==true 时有效)</param>
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);
}
// ================================================================
// 状态机
// ================================================================
/// <summary>切换到新状态(先触发旧状态 Exit,再触发新状态 Enter)。</summary>
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() { }
/// <summary>
/// 每帧更新鱼的位置(贝塞尔曲线插值)和朝向。
/// 鱼阵(isyz==true)按阵型 ID 前缀分支,普通鱼走统一逻辑。
/// </summary>
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);
}
}
/// <summary>鱼阵移动:按 yzID 前缀选择运动方程。</summary>
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;
}
}
/// <summary>普通鱼移动:贝塞尔曲线路径 + 自动朝向。</summary>
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;
/// <summary>
/// 根据 forwordXYZ 字段将物体朝向目标点方向。
/// 为空时 2D 鱼用 up,3D 鱼用 forward。
/// </summary>
/// <param name="nextPos">下一帧目标位置</param>
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;
// ================================================================
// 动画
// ================================================================
/// <summary>播放指定类型动画(需要先在 dic_AnimKeyAnimName 中注册)。</summary>
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);
}
// ================================================================
// 受击 & 死亡
// ================================================================
/// <summary>触发受击闪烁(0.2s 后自动关闭)。</summary>
public void OnHurt()
{
if (hurt != null)
StartCoroutine(HurtCoroutine());
}
private IEnumerator HurtCoroutine()
{
hurt.SetActive(true);
yield return new WaitForSeconds(0.2f);
hurt.SetActive(false);
}
/// <summary>
/// 执行死亡:播放音效并销毁 GameObject。
/// 子类可 override 添加额外逻辑(如 Boss 爆炸动画)。
/// </summary>
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);
}
// ================================================================
// 工具方法
// ================================================================
/// <summary>递归深度查找子物体(广度优先)。</summary>
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;
}
}