1428 lines
65 KiB
C#
1428 lines
65 KiB
C#
// ============================================================
|
||
// 文件: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>
|
||
/// 鱼实体基类:封装状态机(移动/待机)、B样曲线路径跟随(原贝塞尔已切B样条)、动画播放、死亡逻辑。
|
||
/// 具体鱼类型(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>
|
||
/// ⭐ 是否已完成 Init(路线/guid/moveTime 已赋值)。
|
||
/// false=池回收后未初始化,OnEnable 不应执行 OnMoveUpdata,避免旧 moveTime/moveLine 误触发销毁
|
||
/// true=Init 已调用,可以正常 Update
|
||
/// </summary>
|
||
protected bool isInitialized = false;
|
||
|
||
/// <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)
|
||
return;
|
||
for (int i = 0; i < bingDongEffect.Count; i++)
|
||
{
|
||
if (bingDongEffect[i] != null)
|
||
bingDongEffect[i].SetActive(active);
|
||
}
|
||
}
|
||
|
||
/// <summary>是否为 3D 鱼(影响朝向轴选取)</summary>
|
||
public bool is3D = false;
|
||
|
||
/// <summary>
|
||
/// 前进方向轴,可选 "X" / "-X" / "Y" / "-Y" / "Z" / "-Z"。
|
||
/// 为空时默认使用 up(2D)或 forward(3D)。
|
||
/// ⭐ 运行时不应修改;如需修改请调用 RefreshFacingAxis() 重新解析。
|
||
/// </summary>
|
||
public string forwordXYZ;
|
||
|
||
/// <summary>
|
||
/// forwordXYZ 预解析后的朝向轴枚举(OnEnable 时解析)。
|
||
/// 避免 ApplyFacingDirection 每帧每鱼做 string.IsNullOrEmpty + switch(string)。
|
||
/// </summary>
|
||
private FacingAxis _facingAxis = FacingAxis.None;
|
||
|
||
/// <summary>朝向轴枚举:对应 forwordXYZ 的 7 种取值</summary>
|
||
private enum FacingAxis
|
||
{
|
||
None, // "" / null
|
||
X,
|
||
NegX, // "-X"
|
||
Y,
|
||
NegY, // "-Y"
|
||
Z,
|
||
NegZ // "-Z"
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将 forwordXYZ 字符串解析为枚举。OnEnable 调用一次,运行时直接用枚举 switch。
|
||
/// </summary>
|
||
private void RefreshFacingAxis()
|
||
{
|
||
if (string.IsNullOrEmpty(forwordXYZ))
|
||
{
|
||
_facingAxis = FacingAxis.None;
|
||
return;
|
||
}
|
||
switch (forwordXYZ)
|
||
{
|
||
case "X": _facingAxis = FacingAxis.X; break;
|
||
case "-X": _facingAxis = FacingAxis.NegX; break;
|
||
case "Y": _facingAxis = FacingAxis.Y; break;
|
||
case "-Y": _facingAxis = FacingAxis.NegY; break;
|
||
case "Z": _facingAxis = FacingAxis.Z; break;
|
||
case "-Z": _facingAxis = FacingAxis.NegZ; break;
|
||
default: _facingAxis = FacingAxis.None; break;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 缓存 yzID 前3字符前缀。Init 时调用一次(普通鱼在 base.Init,Boss 在自己 Init)。
|
||
/// 避免 UpdateYZMovement 每帧每鱼 yzID.Substring(0,3) 产生字符串 GC 分配。
|
||
/// </summary>
|
||
protected void SetYzPrefix(string id)
|
||
{
|
||
_yzPrefix = (!string.IsNullOrEmpty(id) && id.Length >= 3) ? id.Substring(0, 3) : "";
|
||
}
|
||
|
||
/// <summary>鱼类型枚举(缓存,避免反复类型转换)</summary>
|
||
public FishTypeEnum fishenum;
|
||
|
||
/// <summary>
|
||
/// 是否为"按路线奇偶翻转X轴朝向"的Boss(Boss本体或BossBase子类,且未配置forwordXYZ自定义朝向轴)。
|
||
/// 这类Boss不随路线切线旋转,朝向只由路线方向决定:
|
||
/// 路线单数=左→右(X保持预制体正向),路线双数=右→左(X取负镜像)。
|
||
/// </summary>
|
||
protected bool IsRouteFlipBoss =>
|
||
(fishenum == FishTypeEnum.Boss鱼 || this is MonsterControl_BossBase)
|
||
&& string.IsNullOrEmpty(forwordXYZ);
|
||
|
||
/// <summary>
|
||
/// ⭐ Boss路线朝向:按路线编号奇偶翻转X轴。
|
||
/// 单数路线=左→右 → X保持预制体原始值不变
|
||
/// 双数路线=右→左 → X取预制体原始值的负数(镜像)
|
||
/// useRouteFlipX=false 时不镜像,X始终保持正向(特殊怪物用)。
|
||
/// 以预制体原始X缩放为基准绝对赋值,对象池复用后重新Init不会叠加翻转。
|
||
/// 路线编号非数字时不翻转(保持原朝向)。
|
||
/// </summary>
|
||
protected void ApplyBossRouteFlip(string pathId)
|
||
{
|
||
if (!int.TryParse(pathId, out int routeNum)) return;
|
||
|
||
float dirX = (useRouteFlipX && routeNum % 2 == 0) ? -1f : 1f;
|
||
var scale = transform.localScale;
|
||
scale.x = _prefabScaleX * dirX;
|
||
transform.localScale = scale;
|
||
}
|
||
|
||
/// <summary>路径已行进的时间(秒),用于B样曲线插值;可由外部预设偏移实现同步追帧。</summary>
|
||
public float moveTime = 0f;
|
||
|
||
/// <summary>
|
||
/// 阵鱼(700/701)创建时刻偏移(秒):由生成端填入"该鱼创建时刻 - 本次鱼阵开始时刻"。
|
||
/// 位移公式里用 (moveTime + yzGroupTimeOffset) 取代 moveTime,
|
||
/// 使先创建的那一组已经沿分布方向游出去、后创建的从出生点出发,
|
||
/// 避免多个 组/鱼 全部重合在同一个出生点上。
|
||
/// ⭐ 与 moveTime 分开:偏移只影响初始位置,不影响存活时长(moveTime 仍从 0 计时)。
|
||
/// </summary>
|
||
public float yzGroupTimeOffset = 0f;
|
||
|
||
/// <summary>是否为鱼阵成员</summary>
|
||
public bool isyz = false;
|
||
|
||
/// <summary>所属鱼阵 ID(空字符串表示普通鱼)</summary>
|
||
public string yzID = "";
|
||
|
||
// ----------------------------------------------------------------
|
||
// ⭐ 缓存优化字段
|
||
// ----------------------------------------------------------------
|
||
|
||
/// <summary>缓存的子物体(朝向控制用),避免每帧 transform.GetChild(0)</summary>
|
||
protected Transform _cachedChild;
|
||
|
||
// ----------------------------------------------------------------
|
||
// ⭐ 鱼阵 700/701 直线移动缓存
|
||
// 同一鱼阵起点相同,Init 时一次性算好世界坐标 + 方向系数,
|
||
// UpdateYZMovement 每帧直接用,避免每帧每鱼 CanvasToWorld + 条件判断。
|
||
// ----------------------------------------------------------------
|
||
private bool _isYzLineMove; // 是否为 700/701 直线阵
|
||
private Vector3 _yzStartWorld; // 直线阵起点世界坐标
|
||
private float _yzLineDir; // 直线阵方向系数 (-1 向上 / +1 向下)
|
||
private bool _yzLineInited; // 直线阵缓存是否已初始化
|
||
/// <summary>扇形(700)/圈(701)分布角方向:Init时按轴读生成姿态前方的XY投影,每条鱼各自不同</summary>
|
||
private Vector2 _yzMoveDir = Vector2.zero;
|
||
|
||
/// <summary>阵鱼定位数值是否已打日志(一次性诊断用,池复用重置)</summary>
|
||
private bool _yzPosLogged;
|
||
|
||
/// <summary>
|
||
/// yzID 前3字符前缀缓存(Init 时 Substring 一次)。
|
||
/// 避免 UpdateYZMovement 每帧每鱼 yzID.Substring(0,3) 产生字符串 GC 分配。
|
||
/// </summary>
|
||
private string _yzPrefix = "";
|
||
|
||
// ----------------------------------------------------------------
|
||
// 私有字段
|
||
// ----------------------------------------------------------------
|
||
|
||
/// <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 List<GameObject> bingDongEffect;
|
||
|
||
[Header("锁定控制(可选)")]
|
||
[Tooltip("是否需要锁定事件控制。默认false=一直可锁定;true=出场/切换状态期间不可锁定,需调用 NotifyLockable() 后才可锁定")]
|
||
public bool needLockEvent = false;
|
||
/// <summary>当前是否可锁定。
|
||
/// - needLockEvent=false → 一直 true
|
||
/// - needLockEvent=true → 初始 false,等 NotifyLockable() 调用后变 true
|
||
/// ⭐ 用于嫦娥(出场→移动两步)和小白龙(多动画片段切换)等特殊鱼
|
||
/// </summary>
|
||
[System.NonSerialized]
|
||
public bool isLockable = true;
|
||
|
||
/// <summary>
|
||
/// ⭐ 鱼是否已死亡(收到服务端击杀通知/离场销毁后为 true)。
|
||
/// 死亡后到回收前有特效播放窗口期,此期间:
|
||
/// - 子弹(BullitMove)不再命中它(碰撞/锁定追踪都跳过)
|
||
/// - 不再被锁定(GameMainPanel_FishBase.IsMonsterLockable 返回 false)
|
||
/// OnEnable 池复用时重置为 false。
|
||
/// </summary>
|
||
[System.NonSerialized]
|
||
public bool isDead = false;
|
||
/// <summary>可锁定事件:needLockEvent=true 的鱼首次可锁定时触发,GameMainPanel 订阅用于自动锁定</summary>
|
||
public event Action<MonsterContral> OnLockable;
|
||
|
||
/// <summary>
|
||
/// 通知此鱼现在可以锁定了。needLockEvent=true 的鱼在出场结束/状态切换完成时调用。
|
||
/// </summary>
|
||
public void NotifyLockable()
|
||
{
|
||
if (!isLockable)
|
||
{
|
||
isLockable = true;
|
||
OnLockable?.Invoke(this);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// ⭐ 本次服务端击杀通知是否需要清除各座位对该鱼的锁定引用(默认 true:死了就清锁)。
|
||
/// 多次死亡的 Boss(星际巡航/烈焰风暴)重写:非最终死亡(爆奖不死)时返回 false,
|
||
/// 此时鱼既没死也不回收,保持锁定,玩家不用重新点一次锁定。
|
||
/// </summary>
|
||
/// <param name="killNotice">本次击杀通知(子类据此判断是否最终死亡)</param>
|
||
public virtual bool ShouldClearLockOnKill(RoomKillNotice killNotice) => true;
|
||
|
||
/// <summary>
|
||
/// ⭐ 是否延迟注册入鱼列表(子类控制,默认false=OnlyFish 创建时立即注册,现有鱼零影响)。
|
||
/// true=CreateFishManager.OnlyFish 跳过 RegisterFishInstance,
|
||
/// 由子类的激活方法(如 StealthBossControl.ActivateBoss)显式补注册。
|
||
/// 潜行Boss用:激活前不入鱼列表 → 自动锁定枚举/技能选鱼/服务端击杀通知全部找不到它。
|
||
/// </summary>
|
||
public virtual bool DeferFishRegister => false;
|
||
|
||
/// <summary>
|
||
/// 切换锁定点(不切换锁定对象)。
|
||
/// ⭐ 用于小白龙等切换模型/动画片段后锁定点也随之变化的情况:
|
||
/// 重新从 colliderList 找第一个激活的碰撞器,更新为新的锁定锚点。
|
||
/// 如果当前没有被锁定则不做任何事。
|
||
/// 调用方(GameMainPanel_FishBase)负责更新 monCotl_Lock_Position。
|
||
/// </summary>
|
||
public Transform SwitchLockPoint()
|
||
{
|
||
// 优先用 lockPoint(若激活)
|
||
if (lockPoint != null && lockPoint.gameObject.activeInHierarchy)
|
||
return lockPoint;
|
||
// 其次 colliderList 第一个激活的
|
||
if (colliderList != null)
|
||
{
|
||
for (int i = 0; i < colliderList.Count; i++)
|
||
{
|
||
var col = colliderList[i];
|
||
if (col != null && col.gameObject.activeInHierarchy)
|
||
return col;
|
||
}
|
||
}
|
||
// ⭐ 完全没配锚点的鱼(如 XJMH/FLZY 多数 Boss 预制体) → 回退用自身 transform,
|
||
// 与 GameMainPanel_FishBase.ResolveLockTransform ⑤ 的回退规则一致
|
||
if (lockPoint == null && (colliderList == null || colliderList.Count == 0))
|
||
return transform;
|
||
return null;
|
||
}
|
||
|
||
[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;
|
||
|
||
[Tooltip("反面补偿:掉头(跨左右中线)时绕鱼头轴z翻180-z,保持透视方向/背朝向一致。往返路线开;环形路线绕圈会跨中线两次导致跳变闪,环形路线的鱼关掉(全程连续转不翻面)")]
|
||
public bool useMirrorCompensate = true;
|
||
|
||
[Tooltip("Z轴侧倾按左右符号翻转(原始语义):向左→+rotationPY/向右→-rotationPY,纯符号切换无180°翻转。true=启用此模式(忽略反面补偿);false=走useMirrorCompensate的180°反面补偿")]
|
||
public bool useRollSignFlip = false;
|
||
|
||
[Tooltip("Boss路线奇偶翻转X轴:单数路线(左→右)保持正向,双数路线(右→左)X取-1镜像。默认开;特殊怪物不需要镜像时关闭")]
|
||
public bool useRouteFlipX = true;
|
||
|
||
|
||
// ================================================================
|
||
// Unity 生命周期
|
||
// ================================================================
|
||
public bool baseMmove;
|
||
|
||
/// <summary>预制体原始X缩放(Awake缓存):Boss按路线奇偶翻转X轴的基准,池复用后重新Init不会叠加翻转</summary>
|
||
private float _prefabScaleX = 1f;
|
||
|
||
public virtual void Awake()
|
||
{
|
||
LoadHitEffect();
|
||
baseMmove = isMove;
|
||
_prefabScaleX = transform.localScale.x;
|
||
}
|
||
/// <summary>
|
||
/// 查找受击特效子物体并强制隐藏。
|
||
/// 原为 async void(无 await 实际同步执行),现改为普通方法;
|
||
/// Awake 和每次 OnEnable 都会调用,保证池回收后不残留激活状态。
|
||
/// </summary>
|
||
public void LoadHitEffect()
|
||
{
|
||
// 查找受击特效子物体并默认隐藏
|
||
var hitTra = FindDeepChild(transform, "HitEffect");
|
||
if (hitTra != null)
|
||
{
|
||
hurt = hitTra.gameObject;
|
||
hurt.SetActive(false);
|
||
}
|
||
}
|
||
public virtual void OnEnable()
|
||
{
|
||
// ⭐ 每次 OnEnable 重新查找并强制隐藏受击特效。
|
||
// 根因:HurtCoroutine 激活 hurt 后等 0.2s 再关闭,若鱼在这期间被
|
||
// 击杀回收(GO 停用 → 协程中断),hurt 会残留激活状态;
|
||
// 而 LoadHitEffect 原来只在 Awake 调用一次(池化对象不重跑 Awake),
|
||
// 重新生成时不会恢复 → 鱼带着受击特效出场。
|
||
// 另外池回收的鱼 transform 可能更换子层级,重新查找更稳妥。
|
||
LoadHitEffect();
|
||
|
||
// ⭐ 用 baseMmove 恢复 isMove:Awake 时记录预制体原始 isMove 状态。
|
||
// - 普通鱼预制体 isMove=true → 池回收后恢复 true,正常游动
|
||
// (之前被注释导致:冰冻时 FreezeAllFish 设 isMove=false → 鱼回收 →
|
||
// 重新生成后残留 false → 鱼永远不动)
|
||
// - 爆奖 Boss 预制体 isMove=false → 恢复 false,固定在 (0,0,0) 播放动画
|
||
// (金龙/雷龙/玉龙等不走路径,直到爆奖结束销毁)
|
||
// - 冰冻中重新激活的鱼,末尾会覆盖为 false,安全
|
||
isMove = baseMmove;
|
||
// ⭐ 重置可锁定状态:needLockEvent=true 的鱼每次重新生成都需等 NotifyLockable()
|
||
// needLockEvent=false 的鱼一直可锁定
|
||
isLockable = !needLockEvent;
|
||
// ⭐ 重置死亡标记:池回收复用的鱼重新激活时是活鱼
|
||
isDead = false;
|
||
// ⭐ 重置路线方向缓存:池复用会换路线,需重新由起终点判定
|
||
_routeDirXCached = false;
|
||
_routeDirX = 0;
|
||
fishenum = (FishTypeEnum)monsterData.fishTypeConfig.fishType;
|
||
uirootRect = UIManager.Instance.uiRoot.GetComponent<RectTransform>();
|
||
mainCamera = Camera.main;
|
||
moveTime = 0f;
|
||
yzGroupTimeOffset = 0f; // 池复用清掉上次的阵鱼创建偏移
|
||
_yzPosLogged = false; // 池复用重置一次性定位日志
|
||
// ⭐ 重置创建视频状态:池回收的鱼 _isPlayingCreateVideo 可能残留 true,
|
||
// 重新生成时重置,由 Init 决定是否重新播放
|
||
_createVideoDuration = 0f;
|
||
_isPlayingCreateVideo = false;
|
||
InitAnimList();
|
||
|
||
// ⭐ 预解析 forwordXYZ 为枚举,避免 ApplyFacingDirection 每帧每鱼 string switch
|
||
RefreshFacingAxis();
|
||
|
||
// ⭐ 同步缓存到 moveLine,避免 GetBezierPointJob 内每帧每鱼重复查询
|
||
// UIManager.Instance.uiRoot.GetComponent<RectTransform>() 和 Camera.main
|
||
monsterData.moveLine.cachedCamera = mainCamera;
|
||
monsterData.moveLine.cachedUirootRect = uirootRect;
|
||
|
||
// ⭐ 缓存子物体引用(朝向控制用),避免 ApplyFacingDirection 每帧 transform.GetChild(0)
|
||
// 池回收的鱼 transform 可能更换子层级,每次 OnEnable 重新缓存
|
||
if (transform.childCount > 0)
|
||
{
|
||
_cachedChild = transform.GetChild(0);
|
||
}
|
||
else
|
||
{
|
||
_cachedChild = null;
|
||
}
|
||
|
||
// ⭐ 2D鱼:重置渲染器缓存(池回收后子层级可能变化)并重新应用sortingOrder层级
|
||
_cached2DRenderers = null;
|
||
_appliedSortOrder = int.MinValue;
|
||
Apply2DOrder();
|
||
|
||
// ⭐ 池回收的鱼(isInitialized=false)不立即 ChangeState(move),
|
||
// 避免 OnMoveUpdata 用旧 moveTime/moveLine 误触发 Id_DieFish_NotFX
|
||
// Init 调用后会设置 isInitialized=true 并 ChangeState(move)
|
||
if (isInitialized)
|
||
{
|
||
ChangeState(CharacterStateType.move);
|
||
}
|
||
|
||
// ⭐ 同步冰冻特效状态:池回收的鱼 bingDongEffect 可能残留激活状态,
|
||
// 重新生成时会显示白色冰冻蒙皮。根据当前是否冰冻中决定显隐
|
||
bool frozen = CreateFishManager.Instance != null && CreateFishManager.Instance.isFrozen;
|
||
SetFreezeEffect(frozen);
|
||
if (frozen)
|
||
{
|
||
// 冰冻中重新生成的鱼也要被冻结
|
||
isMove = false;
|
||
ChangeState(CharacterStateType.idle);
|
||
}
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
// ⭐ 调试:绘制当前鱼移动路线(Game视图Debug线,每帧重画;详见 DrawMoveLine)
|
||
if (drawMoveLine)
|
||
DrawMoveLine();
|
||
|
||
OnUpdate(currState);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 销毁时自动从 CreateFishManager 的三个字典中移除自身,避免野指针。
|
||
/// </summary>
|
||
public virtual void OnDisable()
|
||
{
|
||
// ⭐ 清掉调试折线缓存:池回收后路线失效,重新 Init 换新路线时会按引用变化重建
|
||
_drawLinePoints = null;
|
||
_drawLineCurve = null;
|
||
|
||
if (CreateFishManager.Instance == null) return;
|
||
|
||
// ⭐ 重置初始化标志:鱼被回收后需要重新 Init 才能正常 Update
|
||
isInitialized = false;
|
||
}
|
||
|
||
// ================================================================
|
||
// 初始化
|
||
// ================================================================
|
||
|
||
/// <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;
|
||
|
||
// ⭐ 缓存 yzID 前3字符前缀,避免 UpdateYZMovement 每帧每鱼 Substring 产生 GC
|
||
SetYzPrefix(yzID);
|
||
|
||
// ⭐ 预计算 700/701 直线阵的世界起点 + 方向系数
|
||
// 原逻辑每帧每鱼 CanvasToWorld(start) + 判断 goUp,现 Init 一次性算好
|
||
CacheYzLineMoveData(bezierData);
|
||
|
||
fishenum = (FishTypeEnum)monsterData.fishTypeConfig.fishType;
|
||
|
||
// isMove=false 的鱼不需要移动路线,x/y 归位到 (0,0);z 不带——用预制体自带的深度
|
||
if (!isMove)
|
||
transform.position = new Vector3(0, 0, transform.position.z);
|
||
|
||
// ⭐ Boss朝向:不随路线切线旋转,按路线编号奇偶翻转X轴(单数左→右保持正向/双数右→左翻转)
|
||
if (isMove && IsRouteFlipBoss)
|
||
ApplyBossRouteFlip(pathId);
|
||
|
||
// ⭐ 创建视频启动逻辑由子类负责:
|
||
// - MonsterControl_BossBase 在自己 Init 里启动 createVideo
|
||
// - 普通鱼走父类 Init,但普通鱼一般不配 createVideo
|
||
// OnMoveUpdata 会检测 createVideo.isPlaying,视频播放期间不移动
|
||
|
||
// ⭐ 标记已初始化,并启动 move 状态(OnEnable 里 isInitialized=false 时不会 ChangeState)
|
||
isInitialized = true;
|
||
ChangeState(CharacterStateType.move);
|
||
|
||
// 路径结束后自动销毁
|
||
//StartCoroutine(yanchi(gameObject, time, isyuzhen));
|
||
// ⭐ 使用 OnEnable 缓存的 _cachedChild,避免每帧 transform.GetChild(0) 调用开销
|
||
// 兜底:缓存丢失时重新获取一次(理论上不会发生)
|
||
Transform child = _cachedChild;
|
||
if (child == null)
|
||
{
|
||
if (transform.childCount == 0) return;
|
||
child = transform.GetChild(0);
|
||
_cachedChild = child;
|
||
}
|
||
|
||
Vector3 localPos = child.localPosition;
|
||
// ⭐ x/y 归零摆正模型;z 深度只在显式配置 depthOffset(≠0)时才覆盖,
|
||
// 否则保留预制体上设置的 3D 深度(3D Boss 用,之前被强制归零导致深度无效)
|
||
localPos.x = 0f;
|
||
if (depthOffset != 0)
|
||
localPos.z = depthOffset;
|
||
child.localPosition = localPos;
|
||
|
||
}
|
||
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;
|
||
// ⭐ 池回收的鱼(isInitialized=false)不执行移动逻辑,
|
||
// 避免 currState 残留为 move 时用旧 guid/moveLine 误触发 Id_DieFish_NotFX
|
||
// Init 调用后会设置 isInitialized=true
|
||
if (!isInitialized) return;
|
||
|
||
switch (stateType)
|
||
{
|
||
case CharacterStateType.move: OnMoveUpdata(); break;
|
||
case CharacterStateType.idle: break;
|
||
}
|
||
}
|
||
|
||
public void OnExit(CharacterStateType stateType)
|
||
{
|
||
// 当前无需处理,保留供子类 override
|
||
}
|
||
|
||
/// <summary>召唤出场:路线指定进度处的"正确朝向"结果</summary>
|
||
public struct RouteFacingResult
|
||
{
|
||
/// <summary>朝向逻辑实际改写的节点(根节点 或 child0)——出场动画只转这个节点</summary>
|
||
public Transform node;
|
||
/// <summary>该节点在目标位置的正确世界朝向</summary>
|
||
public Quaternion rotation;
|
||
/// <summary>是否取到了朝向(鱼阵 700/701、无路线、Boss 走路线的奇偶翻转分支等取不到)</summary>
|
||
public bool valid;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取"路线指定进度处的正确朝向"(召唤出场用:出场结束时朝向 = 该点在路线上的正确朝向)。
|
||
/// ⭐ 直接复用鱼自身的朝向逻辑 ApplyFacingDirection:forwordXYZ 正方向轴 / ischild0 子物体控制 /
|
||
/// rotationPY 侧倾 / useRollSignFlip / useMirrorCompensate 镜像补偿 / _routeDirX 路线左右符号
|
||
/// 全部与正常游动时一致,避免在面板里重算一套对不上的朝向。
|
||
/// ⭐ 内部会把朝向写成目标朝向(用于读取),随后自动还原调用前的姿态,调用方直接拿去插值即可。
|
||
/// ⭐ 方向取"中点稍后一点"的切线:鱼站在中点时 toNext≈0,朝向逻辑会直接 return 算不出朝向。
|
||
/// </summary>
|
||
/// <param name="moveTimeRatio">路线进度(0.5=中点/屏幕中央)</param>
|
||
/// <param name="aheadSeconds">前瞻秒数(取中点之后这么多秒的点做方向)</param>
|
||
public RouteFacingResult GetRouteFacingAt(float moveTimeRatio, float aheadSeconds = 0.2f)
|
||
{
|
||
var result = new RouteFacingResult();
|
||
var line = monsterData != null ? monsterData.moveLine : null;
|
||
if (line == null || line.moveTime <= 0f) return result; // 无路线(原地出场的 Boss)取不到
|
||
if (_isYzLineMove) return result; // 700/701 鱼阵姿态由生成端决定,不参与朝向逻辑
|
||
|
||
Transform child = _cachedChild;
|
||
if (child == null && transform.childCount > 0)
|
||
{
|
||
child = transform.GetChild(0);
|
||
_cachedChild = child;
|
||
}
|
||
if (child == null) return result;
|
||
|
||
float tMid = line.moveTime * Mathf.Clamp01(moveTimeRatio);
|
||
Vector2 aheadPos = line.GetBezierPointJob(Mathf.Min(tMid + Mathf.Max(0f, aheadSeconds), line.moveTime));
|
||
|
||
Quaternion rootBefore = transform.rotation;
|
||
Quaternion childBefore = child.rotation;
|
||
|
||
ApplyFacingDirection(aheadPos); // 鱼自身的朝向逻辑算一遍(会写朝向)
|
||
|
||
// 判定被改写的节点:根节点或 child0(不同分支改的节点不同)
|
||
if (Quaternion.Angle(rootBefore, transform.rotation) > 0.01f)
|
||
{
|
||
result.node = transform;
|
||
result.rotation = transform.rotation;
|
||
}
|
||
else if (Quaternion.Angle(childBefore, child.rotation) > 0.01f)
|
||
{
|
||
result.node = child;
|
||
result.rotation = child.rotation;
|
||
}
|
||
result.valid = result.node != null;
|
||
|
||
if (result.valid)
|
||
{
|
||
// 还原出场前姿态:出场动画以目标朝向为基准倒退若干圈开始旋转
|
||
transform.rotation = rootBefore;
|
||
child.rotation = childBefore;
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 召唤卡出场动画结束:重新置位初始化标记,并从路线指定比例处继续游动。
|
||
/// ⭐ 出场期间这条鱼被 SetActive(false) 再重新激活,且 MonsterContral 被临时禁用:
|
||
/// - OnDisable 会把 isInitialized 复位成 false(池复用的保护逻辑) → 不重新置位的话
|
||
/// OnUpdate 第一行就 return,鱼到位后不会继续走路线
|
||
/// - 组件禁用期间 OnEnable 不会执行,重新启用时 Unity 会补跑 OnEnable(复位 isMove/moveTime),
|
||
/// 所以本方法必须在"启用之后"调用(moveTime 会被 OnEnable 清零,顺序反了就丢)
|
||
/// </summary>
|
||
/// <param name="moveTimeRatio">从路线的百分之几处继续(0.5 = 路线中点/屏幕中央)</param>
|
||
public void OnSummonEntranceEnd(float moveTimeRatio = 0.5f)
|
||
{
|
||
isInitialized = true;
|
||
if (monsterData != null && monsterData.moveLine != null && monsterData.moveLine.moveTime > 0f)
|
||
moveTime = monsterData.moveLine.moveTime * Mathf.Clamp01(moveTimeRatio);
|
||
ChangeState(CharacterStateType.move);
|
||
}
|
||
|
||
// ================================================================
|
||
// 移动逻辑
|
||
// ================================================================
|
||
|
||
public virtual void OnMoveEnter()
|
||
{
|
||
PlayAnim(CharacterAnimationType.move);
|
||
}
|
||
|
||
public virtual void OnMoveExit() { }
|
||
|
||
/// <summary>
|
||
/// 每帧更新鱼的位置(B样曲线插值)和朝向。
|
||
/// 鱼阵(isyz==true)按阵型 ID 前缀分支,普通鱼走统一逻辑。
|
||
/// ⭐ 性能优化(零 GC + 缓存):
|
||
/// - 曲线求值:静态 Vector2[]/节点向量缓存 + ThreadStatic 临时缓冲,零 GC 分配
|
||
/// - 朝向:forwordXYZ 预解析为枚举,避免每帧 string switch
|
||
/// - 子物体:OnEnable 缓存 _cachedChild,避免每帧 transform.GetChild(0)
|
||
/// - 鱼阵 700/701:Init 预计算起点世界坐标 + 方向系数
|
||
/// - yzID 前缀:Init 缓存 _yzPrefix,避免每帧 Substring
|
||
/// </summary>
|
||
public virtual void OnMoveUpdata()
|
||
{
|
||
if (!isMove) return; // 冰冻中不计时
|
||
|
||
// ⭐ 创建视频播放期间不移动、不累计 moveTime
|
||
// 视频结束后才开始走路线;视频时长不计入 moveTime(单独累计到 _createVideoDuration)
|
||
// 隐藏控件由 loopPointReached 回调负责(OnCreateVideoFinished)
|
||
if (_isPlayingCreateVideo)
|
||
{
|
||
_createVideoDuration += Time.deltaTime;
|
||
return;
|
||
}
|
||
|
||
moveTime += Time.deltaTime;
|
||
|
||
// ⭐ 终点检测:每帧执行(轻量)
|
||
if (moveTime >= monsterData.moveLine.moveTime)
|
||
{
|
||
// ⭐ 路径走完后立即停止移动,防止 Id_DieFish_NotFX 每帧重复触发
|
||
// (野鱼 guid 已被覆盖时,Id_DieFish_NotFX 会 NOT_FOUND,但 moveTime 一直 >= moveTime,
|
||
// 不停止的话每帧都会调用,产生海量 NOT_FOUND 日志)
|
||
isMove = false;
|
||
CreateFishManager.Instance.Id_DieFish_NotFX(guid);
|
||
return;
|
||
}
|
||
|
||
// ⭐ 每帧执行重计算(已无 GC 压力,分帧会导致鱼体一卡一卡)
|
||
if (isyz)
|
||
{
|
||
UpdateYZMovement();
|
||
}
|
||
else
|
||
{
|
||
UpdateNormalMovement();
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 预计算 700/701 直线阵的世界起点 + 方向系数。
|
||
/// ⭐ 优化:原 UpdateYZMovement 每帧每鱼 CanvasToWorld(start) + 判断 goUp,
|
||
/// 现在 Init 一次性算好,运行时直接用缓存值。
|
||
/// 非直线阵(702/704/710 等)标记 _isYzLineMove=false 跳过。
|
||
/// </summary>
|
||
protected void CacheYzLineMoveData(MonsterMoveLine.BezierCurveData bezierData)
|
||
{
|
||
_isYzLineMove = false;
|
||
_yzLineInited = false;
|
||
|
||
if (!isyz || string.IsNullOrEmpty(_yzPrefix)) return;
|
||
|
||
if (_yzPrefix != "700" && _yzPrefix != "701") return;
|
||
|
||
// 世界起点(CanvasToWorld 已在 OnEnable 缓存 uirootRect/mainCamera)
|
||
_yzStartWorld = CoordinateTransformation.CanvasToWorld(
|
||
bezierData.start, uirootRect, mainCamera);
|
||
|
||
// 700:根据起点 y 值决定正方向;701:固定向上(dir=-1)
|
||
// ⭐ 座位镜像:701 写死的"向上"随镜像取反(起点/朝向都已翻转,固定方向必须跟着翻);
|
||
// 700 按 start.y 判断——数据已翻转,自动适应无需处理
|
||
bool goUp = (_yzPrefix == "701") ? !SeatMirror.IsMirrored : (bezierData.start.y > 0);
|
||
_yzLineDir = goUp ? -1f : 1f;
|
||
|
||
// ⭐ 分布角方向缓存:按 forwordXYZ 读根当前(=生成端刚设置的)前方向量,取XY平面投影。
|
||
// 扇形(700)/圈(701)阵型靠生成端分布角实现:每条鱼朝各自方向辐射展开,
|
||
// 移动必须沿各自分布方向;此处姿态尚未被任何朝向逻辑修改,读到的就是分布方向。
|
||
// 投影过短(如配X的鱼被生成端Y=-90摆平转出屏幕平面)时回退固定垂直方向(0,±1)
|
||
Vector3 fwd;
|
||
switch (_facingAxis)
|
||
{
|
||
case FacingAxis.X: fwd = transform.right; break;
|
||
case FacingAxis.NegX: fwd = -transform.right; break;
|
||
case FacingAxis.Y: fwd = transform.up; break;
|
||
case FacingAxis.NegY: fwd = -transform.up; break;
|
||
case FacingAxis.Z: fwd = transform.forward; break;
|
||
case FacingAxis.NegZ: fwd = -transform.forward; break;
|
||
default: fwd = transform.up; break;
|
||
}
|
||
Vector2 dir2 = new Vector2(fwd.x, fwd.y);
|
||
_yzMoveDir = dir2.sqrMagnitude > 0.25f
|
||
? dir2.normalized
|
||
: new Vector2(0f, _yzLineDir);
|
||
|
||
_isYzLineMove = true;
|
||
_yzLineInited = true;
|
||
}
|
||
|
||
/// <summary>鱼阵移动:按 yzID 前缀选择运动方程。</summary>
|
||
private void UpdateYZMovement()
|
||
{
|
||
// ⭐ 700/701 直线阵(扇形/圈):使用预计算缓存,零 CanvasToWorld 调用
|
||
if (_isYzLineMove && _yzLineInited)
|
||
{
|
||
// ⭐ 沿各自生成分布角方向辐射移动(Init时缓存,每条鱼各自不同):
|
||
// 扇形/圈的阵型靠生成端分布角实现,移动方向必须与之一致;
|
||
// _yzMoveDir 在 CacheYzLineMoveData 里按轴读生成姿态前方,投影过短回退(0,±1)
|
||
Vector2 lineDir = _yzMoveDir;
|
||
// ⭐ 加上创建时刻偏移:先创建的组已经游出去,后创建的从出生点出发,避免重合在同一点
|
||
Vector2 pos = (Vector2)_yzStartWorld + lineDir * (moveTime + yzGroupTimeOffset) * monsterData.moveLine.moveTime;
|
||
|
||
// ⭐ 一次性诊断日志:输出阵鱼定位的各项数值(排查"偏移距离太小")
|
||
if (!_yzPosLogged)
|
||
{
|
||
_yzPosLogged = true;
|
||
var cam = mainCamera != null ? mainCamera : Camera.main;
|
||
float worldW = cam != null ? cam.orthographicSize * 2f * cam.aspect : 0f;
|
||
//Debug.Log($"[阵鱼定位] guid={guid} yzID={yzID} prefix={_yzPrefix} " +
|
||
// $"start={_yzStartWorld.ToString("F2")} dir={_yzMoveDir.ToString("F3")} " +
|
||
// $"moveTime={moveTime:F3} offset={yzGroupTimeOffset:F3} speed={monsterData.moveLine.moveTime:F2} " +
|
||
// $"pos={pos.ToString("F2")} 屏幕世界宽={worldW:F2} isMove={isMove} isyz={isyz}");
|
||
}
|
||
|
||
// ⭐ 朝向保持生成姿态(分布角)不转——不调 ApplyFacingDirection:
|
||
// 它会覆盖根的分布角,把扇形/圈抹成一列竖队
|
||
// ⭐ 保留根节点当前 z(2D 小鱼 z 一直为 0,行为不变)
|
||
transform.position = new Vector3(pos.x, pos.y, transform.position.z);
|
||
return;
|
||
}
|
||
|
||
// ⭐ 用 Init 时缓存的 _yzPrefix,避免每帧每鱼 yzID.Substring(0,3) GC 分配
|
||
if (string.IsNullOrEmpty(_yzPrefix)) return;
|
||
|
||
switch (_yzPrefix)
|
||
{
|
||
// 700 / 701:沿各自分布方向辐射移动—— 兜底分支,正常走缓存路径
|
||
case "700":
|
||
case "701":
|
||
{
|
||
var startWorld = CoordinateTransformation.CanvasToWorld(
|
||
monsterData.moveLine.bezierCurveData.start, uirootRect, mainCamera);
|
||
|
||
// 700:根据起点 y 值决定正方向;701:固定
|
||
// ⭐ 座位镜像:701 写死的"向上"随镜像取反(与 CacheYzLineMoveData 同规则)
|
||
bool goUp = (_yzPrefix == "701") ? !SeatMirror.IsMirrored : (monsterData.moveLine.bezierCurveData.start.y > 0);
|
||
float dir = goUp ? -1f : 1f;
|
||
|
||
// ⭐ 同缓存分支:沿分布方向移动(缓存值优先,未初始化回退固定垂直),朝向不转
|
||
Vector2 lineDir = _yzMoveDir != Vector2.zero ? _yzMoveDir : new Vector2(0f, dir);
|
||
// ⭐ 兜底分支同样加上创建时刻偏移(与缓存分支一致)
|
||
Vector2 pos = (Vector2)startWorld + lineDir * (moveTime + yzGroupTimeOffset) * monsterData.moveLine.moveTime;
|
||
|
||
// ⭐ 保留根节点当前 z(2D 小鱼 z 一直为 0,行为不变)
|
||
transform.position = new Vector3(pos.x, pos.y, transform.position.z);
|
||
break;
|
||
}
|
||
|
||
// 702 / 710:B样曲线路径
|
||
case "702":
|
||
case "7007":
|
||
case "710": // 顺序阵:每条路径逐一延迟生成
|
||
case "704":
|
||
{
|
||
Vector2 next = monsterData.moveLine.GetBezierPointJob(moveTime);
|
||
if (fishenum != FishTypeEnum.Boss鱼)
|
||
ApplyFacingDirection(next);
|
||
// ⭐ 保留根节点当前 z(2D 小鱼 z 一直为 0,行为不变)
|
||
transform.position = new Vector3(next.x, next.y, transform.position.z);
|
||
break;
|
||
}
|
||
|
||
default:
|
||
break;
|
||
}
|
||
}
|
||
|
||
/// <summary>普通鱼移动:B样曲线路径 + 自动朝向。</summary>
|
||
private void UpdateNormalMovement()
|
||
{
|
||
Vector2 next = monsterData.moveLine.GetBezierPointJob(moveTime);
|
||
|
||
//if (fishenum != FishTypeEnum.Boss鱼)
|
||
ApplyFacingDirection(next);
|
||
|
||
// ⭐ 保留根节点当前 z:3D Boss 在预制体上配置的深度不被擦掉
|
||
// (普通鱼 z 一直是 0,行为不变;next 是 Vector2 直接赋值会把 z 强制归 0)
|
||
transform.position = new Vector3(next.x, next.y, transform.position.z);
|
||
}
|
||
public bool isRotate = true;
|
||
public bool isNot1 = false;
|
||
|
||
/// <summary>
|
||
/// 路线水平方向缓存:+1=左→右,-1=右→左,0=纯垂直。
|
||
/// 由曲线起终点一次判定(终点在起点右边=左→右),路线终身不变。
|
||
/// ⭐ 侧倾符号判断统一用它,替代每帧 dirNorm 分量:曲线中段波动会让 dirNorm 分量
|
||
/// 频繁穿0,符号每帧翻转导致侧倾 ±rotationPY 来回闪(±90跳)。
|
||
/// </summary>
|
||
private int _routeDirX = 0;
|
||
/// <summary>_routeDirX 是否已计算(ApplyFacingDirection 首次调用时懒算,OnEnable 重置供池复用)</summary>
|
||
private bool _routeDirXCached = false;
|
||
/// <summary>
|
||
/// 根据 forwordXYZ 字段将物体朝向目标点方向。
|
||
/// 为空时 2D 鱼用 up,3D 鱼用 forward。
|
||
/// </summary>
|
||
/// <param name="nextPos">下一帧目标位置</param>
|
||
private void ApplyFacingDirection(Vector2 nextPos)
|
||
{
|
||
// ⭐ 700/701扇形/圈鱼阵特殊处理:出生时生成端已设好分布角度,Init时已缓存移动方向(_yzMoveDir),
|
||
// 之后全程跳过一切旋转/摆位/镜像逻辑——任何路径调用都直接返回,保证阵型姿态绝不被触碰。
|
||
// (_isYzLineMove 由 CacheYzLineMoveData 在 Init 内设置,仅 700/701 为 true,出生后立即生效)
|
||
if (_isYzLineMove)
|
||
return;
|
||
|
||
// ⭐ 使用 OnEnable 缓存的 _cachedChild,避免每帧 transform.GetChild(0) 调用开销
|
||
// 兜底:缓存丢失时重新获取一次(理论上不会发生)
|
||
Transform child = _cachedChild;
|
||
if (child == null)
|
||
{
|
||
if (transform.childCount == 0) return;
|
||
child = transform.GetChild(0);
|
||
_cachedChild = child;
|
||
}
|
||
|
||
Vector3 localPos = child.localPosition;
|
||
// ⭐ x/y 归零摆正模型;z 深度只在显式配置 depthOffset(≠0)时才覆盖,
|
||
// 否则保留预制体上设置的 3D 深度(3D Boss 用,之前被强制归零导致深度无效)
|
||
localPos.x = 0f;
|
||
localPos.y = 0f;
|
||
if (depthOffset != 0)
|
||
localPos.z = depthOffset;
|
||
child.localPosition = localPos;
|
||
if (!isRotate) return;
|
||
|
||
Vector2 toNext = nextPos - (Vector2)transform.position;
|
||
if (toNext.sqrMagnitude < 0.0001f) return;
|
||
|
||
Vector2 dirNorm = toNext.normalized; // ⭐ 缓存归一化向量,避免分支内重复计算
|
||
|
||
// ⭐ 路线左右方向懒缓存:终点在起点右边=左→右(+1)/左边=右→左(-1)/相同=纯垂直(0)
|
||
// 由起终点一次判定,路线终身不变——镜像/侧倾符号判断不再受曲线中段波动影响
|
||
if (!_routeDirXCached)
|
||
{
|
||
_routeDirXCached = true;
|
||
var bd = monsterData.moveLine.bezierCurveData;
|
||
if (bd.end.x > bd.start.x) _routeDirX = 1;
|
||
else if (bd.end.x < bd.start.x) _routeDirX = -1;
|
||
else _routeDirX = 0;
|
||
}
|
||
|
||
// ⭐ 用预解析的枚举 switch,避免每帧每鱼 string 比较
|
||
// 处理未指定朝向轴的情况(默认行为)
|
||
if (_facingAxis == FacingAxis.None)
|
||
{
|
||
// ⭐ Boss不随路线切线旋转(重构前旧逻辑Boss鱼本来就不参与朝向):
|
||
// 未配置正方向(forwordXYZ)的Boss,朝向由 ApplyBossRouteFlip 按路线奇偶翻转X轴决定;
|
||
// 配置了正方向的Boss走下方轴对齐逻辑,保持原有机制不变
|
||
if (IsRouteFlipBoss) return;
|
||
|
||
// ⭐ 镜像由路线方向一次判定(_routeDirX)+useMirrorCompensate开关(与轴分支统一):
|
||
// 横向向左路线才镜像;环形路线起终点相同(_routeDirX==0)天然不镜像——
|
||
// 原每帧dirNorm.x滞回(_defaultMirror)在环形/绕圈路线上每圈跨中线两次,
|
||
// 镜像来回翻转导致模型闪(±90姿态跳,XJMH飞机类高发)
|
||
bool mirrorLeft = useMirrorCompensate && _routeDirX < 0;
|
||
|
||
if (!is3D)
|
||
{
|
||
float angle = Mathf.Atan2(toNext.y, toNext.x) * Mathf.Rad2Deg;
|
||
transform.rotation = Quaternion.AngleAxis(angle - 90f, Vector3.forward);
|
||
|
||
// ⭐ 左右镜像(修复"出鱼有的正有的反",不对称鱼往左显示反):
|
||
// 贴图头朝上的鱼纯旋转对齐时,往左游=整体转180°=肚皮朝上(反)。
|
||
// 往左时叠加X轴镜像:旋转角不变(镜像不改变鱼头方向),
|
||
// 只翻回背腹朝向,左右游姿态一致。以预制体基准绝对赋值,池复用不叠加
|
||
float mirrorX = mirrorLeft ? -_prefabScaleX : _prefabScaleX;
|
||
var s = transform.localScale;
|
||
if (!Mathf.Approximately(s.x, mirrorX))
|
||
{
|
||
s.x = mirrorX;
|
||
transform.localScale = s;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// ⭐ 连续构造替代 LookRotation(dirNorm, up):朝向接近正上/正下时
|
||
// LookRotation 参考系退化(cross(up,forward)≈0),左右轴随dx符号
|
||
// 瞬间翻转180°导致鱼抖。此写法与 LookRotation 稳态等价且全程连续
|
||
float heading = Mathf.Atan2(dirNorm.y, dirNorm.x) * Mathf.Rad2Deg;
|
||
var rot = Quaternion.AngleAxis(heading, Vector3.forward)
|
||
* Quaternion.AngleAxis(90f, Vector3.up);
|
||
// ⭐ 往左时绕头尾轴再转180°:3D鱼左右近似对称,等效镜像,
|
||
// 背始终朝上(与往右一致),避免负缩放的法线翻转副作用
|
||
if (mirrorLeft)
|
||
rot *= Quaternion.AngleAxis(180f, Vector3.forward);
|
||
transform.rotation = rot;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 是否为子物体单独控制朝向和深度
|
||
if (ischild0)
|
||
{
|
||
// ⭐ 全部使用缓存 child,不再 transform.GetChild(0)
|
||
switch (_facingAxis)
|
||
{
|
||
case FacingAxis.X:
|
||
{
|
||
child.right = dirNorm;
|
||
var vv = child.eulerAngles;
|
||
// ⭐ 侧倾符号由路线方向一次判定(_routeDirX,起终点计算):
|
||
// 横向路线 向右→负/向左→正;曲线中段 dy 波动穿0不再翻号(±90闪已除)。
|
||
// 纯垂直路线(_routeDirX==0)回退用 dy(垂直游动恒号,稳定)
|
||
bool neg = _routeDirX != 0 ? _routeDirX > 0 : dirNorm.y >= 0f;
|
||
vv.x = rotationPY * (neg ? -1f : 1f);
|
||
child.eulerAngles = vv;
|
||
}
|
||
break;
|
||
case FacingAxis.NegX:
|
||
{
|
||
child.right = -dirNorm;
|
||
var vv = child.eulerAngles;
|
||
// ⭐ 同上:符号由路线方向一次判定,垂直路线回退用 dy
|
||
bool neg = _routeDirX != 0 ? _routeDirX > 0 : dirNorm.y <= 0f;
|
||
vv.x = -rotationPY * (neg ? -1f : 1f);
|
||
child.eulerAngles = vv;
|
||
}
|
||
break;
|
||
case FacingAxis.Y:
|
||
{
|
||
child.up = dirNorm;
|
||
var vv = child.eulerAngles;
|
||
// ⭐ 同上:符号由路线方向一次判定。
|
||
// 横向路线 向左→负/向右→正(与原 dx 判断语义一致且不再受波动影响);
|
||
// 纯垂直路线回退用 dy(上游→负/下游→正)
|
||
bool neg = _routeDirX != 0 ? _routeDirX < 0 : dirNorm.y >= 0f;
|
||
vv.y = rotationPY * (neg ? -1f : 1f);
|
||
child.eulerAngles = vv;
|
||
}
|
||
break;
|
||
case FacingAxis.NegY:
|
||
{
|
||
child.up = -dirNorm;
|
||
var vv = child.eulerAngles;
|
||
// ⭐ 同上:符号由路线方向一次判定,垂直路线回退用 dy
|
||
bool neg = _routeDirX != 0 ? _routeDirX > 0 : dirNorm.y <= 0f;
|
||
vv.y = -rotationPY * (neg ? -1f : 1f);
|
||
child.eulerAngles = vv;
|
||
}
|
||
break;
|
||
case FacingAxis.Z:
|
||
{
|
||
// ⭐ 换成旋转角度(不指定forward方向):鱼头朝Z的模型,
|
||
// 转向只动X分量 = -heading(唯一随路径变的轴);
|
||
// Y=90 固定摆平(鱼头从Z轴转到屏幕平面,初始化锁死);
|
||
// Z=rotationPY 绕鱼头轴的姿态偏移(不改鱼头方向)
|
||
float heading = Mathf.Atan2(dirNorm.y, dirNorm.x) * Mathf.Rad2Deg;
|
||
float rollZ;
|
||
if (useRollSignFlip)
|
||
{
|
||
// ⭐ 原始语义:按路线左右纯符号翻转(向左→+rotationPY/向右→-rotationPY),
|
||
// 无180°翻转;_routeDirX 起终点一次判定,终身不变不闪
|
||
rollZ = _routeDirX < 0 ? rotationPY : -rotationPY;
|
||
}
|
||
else
|
||
{
|
||
// ⭐ 反面补偿模式:由路线方向一次判定(_routeDirX),向左路线才补偿。
|
||
// 原 _defaultMirror 每帧滞回,环形路线绕圈跨中线两次会来回翻转
|
||
// → 模型闪 + Y欧拉±90跳。环形路线起终点相同(_routeDirX==0)天然不补偿
|
||
rollZ = -rotationPY;
|
||
if (useMirrorCompensate && _routeDirX < 0) rollZ = 180f - rollZ;
|
||
//if (fishTypeID == 13018)
|
||
//{
|
||
// Debug.Log("rollZ" + rollZ + "---rotationPY" + rotationPY);
|
||
//}
|
||
}
|
||
child.rotation = Quaternion.Euler(-heading, 90f, rollZ);
|
||
}
|
||
break;
|
||
case FacingAxis.NegZ:
|
||
{
|
||
// ⭐ 鱼头=-Z:同样X分量转向,基准反向(+180);Z取-rotationPY
|
||
float heading = Mathf.Atan2(dirNorm.y, dirNorm.x) * Mathf.Rad2Deg;
|
||
float rollZ;
|
||
if (useRollSignFlip)
|
||
{
|
||
// ⭐ 原始语义(与Z case条件相反):向右→+rotationPY/向左→-rotationPY
|
||
rollZ = _routeDirX > 0 ? rotationPY : -rotationPY;
|
||
}
|
||
else
|
||
{
|
||
// ⭐ 反面补偿模式(与Z case条件相反):向右路线才补偿
|
||
rollZ = -rotationPY;
|
||
if (useMirrorCompensate && _routeDirX > 0) rollZ = 180f - rollZ;
|
||
|
||
}
|
||
child.rotation = Quaternion.Euler(-heading + 180f, 90f, rollZ);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// ⭐ 控制父物体自身朝向:与 ischild0 分支同款完整逻辑
|
||
// (侧倾符号由方向直接算 + Z/NegZ 连续构造 + 镜像补偿),
|
||
// 只是操作对象是 transform 自身——未勾 ischild0 的鱼(如鱼阵普通鱼)
|
||
// 同样获得防闪处理,不再只有一行式轴对齐
|
||
switch (_facingAxis)
|
||
{
|
||
case FacingAxis.X:
|
||
{
|
||
transform.right = dirNorm;
|
||
var vv = transform.eulerAngles;
|
||
// ⭐ 侧倾符号由路线方向一次判定(_routeDirX,起终点计算),垂直路线回退用 dy
|
||
bool neg = _routeDirX != 0 ? _routeDirX > 0 : dirNorm.y >= 0f;
|
||
vv.x = rotationPY * (neg ? -1f : 1f);
|
||
transform.eulerAngles = vv;
|
||
}
|
||
break;
|
||
case FacingAxis.NegX:
|
||
{
|
||
transform.right = -dirNorm;
|
||
var vv = transform.eulerAngles;
|
||
bool neg = _routeDirX != 0 ? _routeDirX > 0 : dirNorm.y <= 0f;
|
||
vv.x = -rotationPY * (neg ? -1f : 1f);
|
||
transform.eulerAngles = vv;
|
||
}
|
||
break;
|
||
case FacingAxis.Y:
|
||
{
|
||
transform.up = dirNorm;
|
||
var vv = transform.eulerAngles;
|
||
// ⭐ 同上:横向路线 向左→负/向右→正,垂直路线回退用 dy
|
||
bool neg = _routeDirX != 0 ? _routeDirX < 0 : dirNorm.y >= 0f;
|
||
vv.y = rotationPY * (neg ? -1f : 1f);
|
||
transform.eulerAngles = vv;
|
||
}
|
||
break;
|
||
case FacingAxis.NegY:
|
||
{
|
||
transform.up = -dirNorm;
|
||
var vv = transform.eulerAngles;
|
||
bool neg = _routeDirX != 0 ? _routeDirX > 0 : dirNorm.y <= 0f;
|
||
vv.y = -rotationPY * (neg ? -1f : 1f);
|
||
transform.eulerAngles = vv;
|
||
}
|
||
break;
|
||
case FacingAxis.Z:
|
||
{
|
||
// ⭐ 连续构造:转向只动X分量=-heading,Y=90固定摆平,Z=rotationPY
|
||
float heading = Mathf.Atan2(dirNorm.y, dirNorm.x) * Mathf.Rad2Deg;
|
||
float rollZ;
|
||
if (useRollSignFlip)
|
||
{
|
||
// ⭐ 原始语义:按路线左右纯符号翻转(向左→+rotationPY/向右→-rotationPY)
|
||
rollZ = _routeDirX < 0 ? rotationPY : -rotationPY;
|
||
}
|
||
else
|
||
{
|
||
// ⭐ 反面补偿模式:向左路线才补偿,环形(_routeDirX==0)不补偿
|
||
rollZ = -rotationPY;
|
||
if (useMirrorCompensate && _routeDirX < 0) rollZ = 180f - rollZ;
|
||
}
|
||
transform.rotation = Quaternion.Euler(-heading, 90f, rollZ);
|
||
}
|
||
break;
|
||
case FacingAxis.NegZ:
|
||
{
|
||
// ⭐ 鱼头=-Z:基准反向(+180);符号条件与Z case相反(向右路线)
|
||
float heading = Mathf.Atan2(dirNorm.y, dirNorm.x) * Mathf.Rad2Deg;
|
||
float rollZ;
|
||
if (useRollSignFlip)
|
||
{
|
||
rollZ = _routeDirX > 0 ? rotationPY : -rotationPY;
|
||
}
|
||
else
|
||
{
|
||
rollZ = -rotationPY;
|
||
if (useMirrorCompensate && _routeDirX > 0) rollZ = 180f - rollZ;
|
||
//if (fishTypeID==13018)
|
||
//{
|
||
// Debug.Log("rollZ"+ rollZ+ "---rotationPY"+rotationPY);
|
||
//}
|
||
}
|
||
transform.rotation = Quaternion.Euler(-heading + 180f, 90f, rollZ);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
public bool ischild0 = false;
|
||
public int depthOffset;
|
||
|
||
// ----------------------------------------------------------------
|
||
// ⭐ 2D层级(2D鱼用,与3D鱼的z轴depthOffset深度兼容并存)
|
||
// ----------------------------------------------------------------
|
||
|
||
[Header("2D层级(2D鱼)")]
|
||
[Tooltip("是否为2D渲染鱼:用sortingOrder控制层级(3D鱼继续用z轴depthOffset深度,互不影响)")]
|
||
public bool is2DFish = false;
|
||
[Tooltip("2D鱼的sortingOrder层级值,运行时可调 Set2DOrder(int) 修改")]
|
||
public int sortOrder2D = 0;
|
||
|
||
/// <summary>2D鱼渲染器缓存(设置sortingOrder用,OnEnable重建,避免重复GetComponentsInChildren)</summary>
|
||
private Renderer[] _cached2DRenderers;
|
||
/// <summary>已应用的sortingOrder值(值未变化时跳过重复设置)</summary>
|
||
private int _appliedSortOrder = int.MinValue;
|
||
|
||
/// <summary>
|
||
/// 设置2D层级(sortingOrder)。对鱼身上所有渲染器统一生效,
|
||
/// 多渲染器按层级顺序依次 +1 递增,保持鱼内部前后遮挡关系不变。
|
||
/// 仅 is2DFish=true 时生效;3D鱼仍走 z 轴 depthOffset,兼容现有逻辑。
|
||
/// </summary>
|
||
public void Set2DOrder(int order)
|
||
{
|
||
sortOrder2D = order;
|
||
Apply2DOrder();
|
||
}
|
||
|
||
/// <summary>应用sortingOrder到全部子渲染器(值相同则跳过)</summary>
|
||
private void Apply2DOrder()
|
||
{
|
||
if (!is2DFish) return;
|
||
if (_appliedSortOrder == sortOrder2D) return;
|
||
|
||
if (_cached2DRenderers == null)
|
||
_cached2DRenderers = GetComponentsInChildren<Renderer>(true);
|
||
|
||
_appliedSortOrder = sortOrder2D;
|
||
for (int i = 0; i < _cached2DRenderers.Length; i++)
|
||
{
|
||
var r = _cached2DRenderers[i];
|
||
if (r != null)
|
||
r.sortingOrder = sortOrder2D + i;
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// ⭐ 绘制路线(调试)
|
||
// 把 monsterData.moveLine 的移动曲线(B样条)按时间均分采样成世界坐标折线:
|
||
// - Scene 视图:OnDrawGizmos 画 Gizmos 折线(不选中也常显)
|
||
// - Game 视图 :Update 每帧画 Debug.DrawLine(需勾选 Game 视图 Gizmos 按钮)
|
||
// 折线只在首次绘制时采样一次并缓存(换路线/改段数自动重建),运行时零 GC。
|
||
// ================================================================
|
||
|
||
[Header("绘制路线(调试)")]
|
||
[Tooltip("绘制当前鱼的移动路线:Scene视图Gizmos折线 + Game视图Debug线。路线在Init后才有数据,运行时勾选即时生效")]
|
||
public bool drawMoveLine = false;
|
||
|
||
[Tooltip("路线采样段数,越大越平滑(运行时可调,改后自动重建折线)")]
|
||
[Range(8, 200)]
|
||
public int drawLineSegments = 60;
|
||
|
||
[Tooltip("路线折线颜色(另固定画绿色起点/红色终点标记)")]
|
||
public Color drawLineColor = Color.yellow;
|
||
|
||
/// <summary>采样后的世界坐标折线缓存(BuildDrawLinePoints 生成,Gizmos 与 Debug.DrawLine 共用)</summary>
|
||
private Vector3[] _drawLinePoints;
|
||
/// <summary>折线缓存对应的曲线引用(池复用换路线后引用变化→触发重建)</summary>
|
||
private MonsterMoveLine.BezierCurveData _drawLineCurve;
|
||
|
||
/// <summary>
|
||
/// 采样当前路线为世界坐标折线。
|
||
/// ⭐ 复用 monsterData.moveLine.GetBezierPointJob 按总时间均分采样,
|
||
/// 画出来的就是鱼实际游走的路径(含 Canvas→世界 坐标换算,和移动逻辑同一套取点)。
|
||
/// </summary>
|
||
private void BuildDrawLinePoints(int segments)
|
||
{
|
||
var line = monsterData.moveLine;
|
||
_drawLineCurve = line.bezierCurveData;
|
||
|
||
// 无路线 / 相机和UI根未缓存(如编辑态未 Init)时无法做 Canvas→世界 换算,跳过
|
||
if (line.bezierCurveData == null || line.cachedCamera == null || line.cachedUirootRect == null)
|
||
{
|
||
_drawLinePoints = null;
|
||
return;
|
||
}
|
||
|
||
_drawLinePoints = new Vector3[segments + 1];
|
||
for (int i = 0; i <= segments; i++)
|
||
{
|
||
// t = moveTime * i / segments,GetBezierPointJob 内部归一化后取曲线点
|
||
// ⭐ 不关心具体曲线算法(贝塞尔/B样):与鱼移动走同一方法,画啥鱼走啥
|
||
Vector2 p = line.GetBezierPointJob(line.moveTime * i / segments);
|
||
_drawLinePoints[i] = new Vector3(p.x, p.y, 0f);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确保折线缓存可用:换路线(池复用后 bezierCurveData 引用变化)或改采样段数时重建。
|
||
/// </summary>
|
||
private bool EnsureDrawLinePoints()
|
||
{
|
||
int segments = Mathf.Max(2, drawLineSegments);
|
||
if (_drawLinePoints == null
|
||
|| _drawLinePoints.Length != segments + 1
|
||
|| _drawLineCurve != monsterData.moveLine.bezierCurveData)
|
||
{
|
||
BuildDrawLinePoints(segments);
|
||
}
|
||
return _drawLinePoints != null;
|
||
}
|
||
|
||
/// <summary>
|
||
/// Game 视图绘制当前路线(Debug.DrawLine 只保留一帧,由 Update 每帧调用)。
|
||
/// </summary>
|
||
private void DrawMoveLine()
|
||
{
|
||
if (!drawMoveLine) return;
|
||
if (monsterData.moveLine.bezierCurveData == null) return;
|
||
if (!EnsureDrawLinePoints()) return;
|
||
|
||
// depthTest=false:线不被鱼体遮挡,始终可见
|
||
for (int i = 0; i < _drawLinePoints.Length - 1; i++)
|
||
Debug.DrawLine(_drawLinePoints[i], _drawLinePoints[i + 1], drawLineColor, 0f, false);
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
// ⭐ 用 OnDrawGizmos 常显(不用选中就能看,池化的运行时鱼在Hierarchy里难选中);
|
||
// drawMoveLine=false 时立即返回,平时零开销
|
||
private void OnDrawGizmos()
|
||
{
|
||
if (!drawMoveLine) return;
|
||
if (monsterData.moveLine.bezierCurveData == null) return;
|
||
if (!EnsureDrawLinePoints()) return;
|
||
|
||
Gizmos.color = drawLineColor;
|
||
for (int i = 0; i < _drawLinePoints.Length - 1; i++)
|
||
Gizmos.DrawLine(_drawLinePoints[i], _drawLinePoints[i + 1]);
|
||
|
||
// 起点绿 / 终点红,游动方向一目了然
|
||
Gizmos.color = Color.green;
|
||
Gizmos.DrawWireSphere(_drawLinePoints[0], 0.25f);
|
||
Gizmos.color = Color.red;
|
||
Gizmos.DrawWireSphere(_drawLinePoints[_drawLinePoints.Length - 1], 0.25f);
|
||
}
|
||
#endif
|
||
|
||
// ================================================================
|
||
// 动画
|
||
// ================================================================
|
||
|
||
/// <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 后自动关闭)。子类可 override 追加受击表现(如爆裂金鲨首炮切战损,属本地近似)。</summary>
|
||
public virtual 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 爆炸动画)。
|
||
/// ⭐ 统一设置 isDead=true:死亡后子弹不再命中、不再被锁定。
|
||
/// (不调 base 的子类由 CreateFishManager.Ws_DieFish 入口统一标记)
|
||
/// </summary>
|
||
public virtual void DieFish(RoomKillNotice obj = null)
|
||
{
|
||
isDead = true;
|
||
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;
|
||
}
|
||
}
|