TH1/Unity/Assets/Scripts/TH1_Renderer/GridVFXRenderer.cs
2026-06-11 00:34:50 +08:00

796 lines
28 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.

using System;
using System.Collections.Generic;
using UnityEngine;
using RuntimeData;
using Animancer;
using Logic;
using Logic.Audio;
using Logic.Skill;
using TH1Resource;
using TMPro;
using Unity.IO.LowLevel.Unsafe;
using Unity.VisualScripting;
using Object = UnityEngine.Object;
using PropertyAttribute = UnityEngine.PropertyAttribute;
using Random = UnityEngine.Random;
namespace TH1Renderer
{
public enum GridVFXType {
Heal = 0,
Die = 1,
DieHint = 2,
CounterDieHint = 3,
Flag = 4,
Damage = 5,
Hurt = 6,
Treasure = 7,
Fog = 8,
Coin = 9,
Fire =10,
ROYALFLAMES = 11,
//TewiFrenchBuff=12,
//KaguyaFrenchBuff=13,
CityConnect=14,
RedMistCreate=15,
//SakuyaGuard=16,
Lucky=17,
BigLucky=18,
Unlucky =19,
BigUnlucky=20,
LuckyText=21,
BigLuckyText=22,
UnluckyText=23,
BigUnluckyText=24,
Luxury=25,
KomeijiFear=26,
SkillIcon=27,
Dancing=28,
};
public class GridVFXManager
{
public Dictionary<GridVFXType, GridVFXRenderer> GridVFXDict;
public GridVFXManager(GameObject _effect)
{
GridVFXDict = new Dictionary<GridVFXType, GridVFXRenderer>();
if (!GridVFXRendererFactory.NewGridVFXDict(_effect, GridVFXDict))
Debug.LogError("Failed to create GridVFXDict");
}
public bool SetStop(GridVFXType type)
{
if (!GridVFXDict.TryGetValue(type, out var renderer)) return false;
renderer.SetStop();
return true;
}
public void Clear()
{
foreach (var obj in GridVFXDict)
{
obj.Value?.Clear();
}
}
public void Update()
{
foreach (var vfxPair in GridVFXDict)
vfxPair.Value?.Update();
}
public bool PlayVFX(GridVFXParams param,GridVFXPlayType playType)
{
if (!GridVFXDict.TryGetValue(param.Type, out var vfxRenderer))
{
//Debug.LogWarning($"[GridVFX][PlayVFX] ❌ NO RENDERER for type={param.Type}, playType={playType}. Dict has {GridVFXDict.Count} entries.");
return false;
}
if (vfxRenderer == null)
{
//Debug.LogWarning($"[GridVFX][PlayVFX] ❌ RENDERER NULL for type={param.Type}, playType={playType}");
return false;
}
if (playType is GridVFXPlayType.Play or GridVFXPlayType.PlayOnce)
{
//Debug.Log($"[GridVFX][PlayVFX] ▶️ PLAY type={param.Type}, playType={playType}, rendererClass={vfxRenderer.GetType().Name}, VFXObject={(vfxRenderer.VFXObject != null ? vfxRenderer.VFXObject.name : "NULL")}, VFXAnim={(vfxRenderer.VFXAnim != null ? vfxRenderer.VFXAnim.name : "NULL")}, Sprite={(vfxRenderer.Sprite != null ? vfxRenderer.Sprite.name : "NULL")}, IsPlaying(before)={vfxRenderer.IsPlaying}");
vfxRenderer.UpdateSpecialParam(param);
vfxRenderer.SetPlay(playType);
//Debug.Log($"[GridVFX][PlayVFX] ✅ AFTER SetPlay type={param.Type}, IsPlaying={vfxRenderer.IsPlaying}, VFXObject.activeSelf={(vfxRenderer.VFXObject != null ? vfxRenderer.VFXObject.activeSelf.ToString() : "NULL")}");
}
else
{
//Debug.Log($"[GridVFX][PlayVFX] ⏹️ STOP type={param.Type}");
vfxRenderer.SetStop();
}
return true;
}
}
public static class GridVFXRendererFactory
{
private static readonly Dictionary<GridVFXType, Func<GridVFXParams, GridVFXInfoDataAssets.GridVFXInfo, GridVFXRenderer>> _rendererFactory
= new Dictionary<GridVFXType, Func<GridVFXParams, GridVFXInfoDataAssets.GridVFXInfo, GridVFXRenderer>>();
private static bool _factoryInitialized;
public static void RegisterRenderer(GridVFXType type, Func<GridVFXParams, GridVFXInfoDataAssets.GridVFXInfo, GridVFXRenderer> creator)
{
if (creator == null) return;
_rendererFactory[type] = creator;
}
private static void EnsureFactoryInitialized()
{
if (_factoryInitialized) return;
RegisterRenderer(GridVFXType.Flag, (param, config) => new FlagGridVFXRenderer(param, config));
RegisterRenderer(GridVFXType.Damage, (param, config) => new DamageGridVFXRenderer(param, config));
RegisterRenderer(GridVFXType.ROYALFLAMES, (param, config) => new ROYALFLAMESGridVFXRenderer(param, config));
//RegisterRenderer(GridVFXType.TewiFrenchBuff, (param, config) => new TewiFrenchBuffGridVFXRenderer(param, config));
RegisterRenderer(GridVFXType.Coin, (param, config) => new CoinGridVFXRenderer(param, config));
RegisterRenderer(GridVFXType.SkillIcon, (param, config) => new SkillIconGridVFXRenderer(param, config));
RegisterRenderer(GridVFXType.Fire, (param, config) => new FireGridVFXRenderer(param));
RegisterRenderer(GridVFXType.Dancing, (param, config) => new DancingGridVFXRenderer(param, config));
_factoryInitialized = true;
}
/// <summary>
/// 从配置创建 GridVFXRenderer
/// </summary>
public static GridVFXRenderer Create(GridVFXParams param)
{
EnsureFactoryInitialized();
// 尝试获取配置
if (Table.Instance?.GridVFXInfoDataAssets == null)
{
Debug.LogError("[GridVFXRendererFactory] GridVFXInfoDataAssets not loaded!");
return null;
}
if (!Table.Instance.GridVFXInfoDataAssets.GetGridVFXInfo(param.Type, out var vfxInfo))
{
Debug.LogWarning($"[GridVFXRendererFactory] No config found for GridVFXType: {param.Type}");
return null;
}
if (_rendererFactory.TryGetValue(param.Type, out var creator))
return creator(param, vfxInfo);
return new GenericGridVFXRenderer(param, vfxInfo);
}
public static bool NewGridVFXDict(GameObject _effect, Dictionary<GridVFXType, GridVFXRenderer> dict)
{
EnsureFactoryInitialized();
if (_effect == null || dict == null) return false;
var dataAssets = Table.Instance?.GridVFXInfoDataAssets;
if (dataAssets == null)
{
Debug.LogError("[GridVFXRendererFactory] GridVFXInfoDataAssets not loaded!");
return false;
}
// 遍历配置表中所有已配置的VFX类型
foreach (var vfxType in dataAssets.GetAllConfiguredTypes())
{
if (!dataAssets.GetGridVFXInfo(vfxType, out var vfxInfo))
{
//Debug.LogWarning($"[GridVFX][Factory] ⚠️ No GridVFXInfo for type={vfxType}");
continue;
}
// 查找 GameObject
GameObject vfxObject = null;
if (!string.IsNullOrEmpty(vfxInfo.GameObjectPath))
{
vfxObject = _effect.transform.Find(vfxInfo.GameObjectPath)?.gameObject;
}
if (vfxType == GridVFXType.Coin)
{
//Debug.Log($"[GridVFX][Factory] 🪙 Coin registration: GameObjectPath='{vfxInfo.GameObjectPath}', vfxObject={(vfxObject != null ? vfxObject.name : "NULL")}, AnimClip={(vfxInfo.AnimClip != null ? vfxInfo.AnimClip.name : "NULL")}, Sprite={(vfxInfo.Sprite != null ? vfxInfo.Sprite.name : "NULL")}");
}
// 创建参数(使用配置中的资源)
var param = new GridVFXParams(
vfxInfo.Type,
vfxInfo.AnimClip,
vfxInfo.Sprite,
vfxObject
);
var renderer = Create(param);
if (renderer != null)
{
dict[vfxType] = renderer;
}
else if (vfxType == GridVFXType.Coin)
{
//Debug.LogError("[GridVFX][Factory] ❌ Coin renderer creation returned null!");
}
}
return true;
}
}
public enum GridVFXPlayType {Play,Stop,PlayOnce}
public class GridVFXParams
{
// 通用
public GridVFXType Type;
public GameObject VFXObject;
public AnimationClip VFXAnim;
public Sprite Sprite;
// 特殊子类用
public uint CivId;
public int Damage;
public SkillType SkillType;
public UnitFullType UnitFullType;
//从gridData持有的GridVFXRenderMark结构转化为GridVFXRenderer所需要的param结构
public GridVFXParams(GridVFXType type)
{
Type = type;
}
public GridVFXParams(GridVFXType type, int dmg)
{
if (type is GridVFXType.Damage)
{
Type = type;
Damage = dmg;
}
else
Debug.LogError("Wrong GridVFX Type");
}
public GridVFXParams(GridVFXType gridVFXType, AnimationClip vfxAnim, Sprite sprite, GameObject vfxObject,GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
Type = gridVFXType;
VFXObject = vfxObject;
Sprite = sprite;
VFXAnim = vfxAnim;
Damage = 0;
CivId = 0;
SkillType = SkillType.NONE;
UnitFullType = new UnitFullType();
}
public GridVFXParams(GridVFXType type, SkillType skillType, UnitFullType unitFullType)
{
Type = type;
SkillType = skillType;
UnitFullType = unitFullType;
Damage = 0;
CivId = 0;
Sprite = null;
VFXAnim = null;
VFXObject = null;
}
public GridVFXParams(GridVFXType type, SkillType skillType, UnitFullType unitFullType, AnimationClip vfxAnim, Sprite sprite, GameObject vfxObject)
{
Type = type;
SkillType = skillType;
UnitFullType = unitFullType;
VFXAnim = vfxAnim;
Sprite = sprite;
VFXObject = vfxObject;
Damage = 0;
CivId = 0;
}
}
public abstract class GridVFXRenderer
{
public GridVFXType Type;
public AnimationClip VFXAnim;
public Sprite Sprite;
public GameObject VFXObject;
protected SpriteRenderer _renderer;
protected Transform _transfrom;
public bool IsPlaying;
// 配置引用(可选)
protected GridVFXInfoDataAssets.GridVFXInfo _config;
public GridVFXRenderer(GridVFXParams param)
{
Type = param.Type;
VFXAnim = param.VFXAnim;
Sprite = param.Sprite;
VFXObject = param.VFXObject;
IsPlaying = false;
_renderer = VFXObject != null ? VFXObject.GetComponent<SpriteRenderer>() : null;
_transfrom = VFXObject != null ? VFXObject.GetComponent<Transform>() : null;
}
/// <summary>
/// 应用配置(仅 GenericGridVFXRenderer 会实际改变外观,特殊 Renderer 可覆盖此方法)
/// </summary>
protected virtual void ApplyVisualConfig()
{
if (_config == null || VFXObject == null) return;
if (_renderer != null)
{
_renderer.color = _config.TintColor;
}
// 只有明确设置了非 (1,1,1) 的 Scale 时才应用(保持 prefab 兼容性)
if (_transfrom != null && _config.Scale != Vector3.one)
{
#if UNITY_EDITOR
//Debug.Log($"[GridVFX] Applying scale {_config.Scale} to {Type}, current: {_transfrom.localScale}");
#endif
_transfrom.localScale = _config.Scale;
}
}
public virtual void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
if (IsPlaying)
{
//Debug.LogWarning($"[GridVFX][SetPlay-Base] ⚠️ SKIP (already playing) type={Type}");
return;
}
if (VFXObject == null)
{
//Debug.LogWarning($"[GridVFX][SetPlay-Base] ❌ VFXObject NULL type={Type}");
return;
}
if (Sprite != null && _renderer != null)
_renderer.sprite = Sprite;
Play(playType);
}
public virtual void Play(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
if (VFXObject == null)
{
//Debug.LogWarning($"[GridVFX][Play] ❌ VFXObject NULL for type={Type}");
return;
}
var vfxObject = VFXObject;
vfxObject.SetActive(true);
IsPlaying = true;
var animancer = vfxObject.GetComponent<AnimancerComponent>();
//Debug.Log($"[GridVFX][Play] 🎬 type={Type}, playType={playType}, VFXObject={VFXObject.name}, animancer={(animancer != null ? "OK" : "NULL")}, VFXAnim={(VFXAnim != null ? VFXAnim.name : "NULL")}");
if (VFXAnim != null && animancer != null)
{
animancer.Play(VFXAnim);
//Debug.Log($"[GridVFX][Play] 🎞️ animancer.Play called for type={Type} clip={VFXAnim.name} length={VFXAnim.length}");
//这里还要处理音频 TODO 将来配表化
if (Type == GridVFXType.Die)
{
AudioManager.Instance?.PlayAudio("SFX/UNIT_die");
}
if (Type == GridVFXType.Heal)
{
AudioManager.Instance?.PlayAudio("SFX/UNIT_heal");
}
if(playType is GridVFXPlayType.PlayOnce)
{
var timer = Timer.Instance;
if (timer == null)
{
if (vfxObject != null)
vfxObject.SetActive(false);
IsPlaying = false;
}
else
{
var animLength = VFXAnim.length;
timer.TimerRegister(vfxObject, () =>
{
if (vfxObject != null)
vfxObject.SetActive(false);
IsPlaying = false;
}, animLength,"GridVFXRenderer_" + Type);
}
}
}
}
public virtual void Clear()
{
IsPlaying = false;
}
public virtual void SetStop()
{
if (IsPlaying) Stop();
}
public virtual void Stop()
{
IsPlaying = false;
VFXObject?.SetActive(false);
}
public virtual void Update()
{
}
public virtual void UpdateSpecialParam(GridVFXParams param){}
public virtual void SetIntParam(int param)
{
}
};
public class FlagGridVFXRenderer : GridVFXRenderer
{
public uint CivId;
public FlagGridVFXRenderer(GridVFXParams param) : base(param)
{
CivId = param.CivId;
}
public FlagGridVFXRenderer(GridVFXParams param, GridVFXInfoDataAssets.GridVFXInfo config) : base(param)
{
CivId = param.CivId;
_config = config;
}
public override void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
if (IsPlaying) return;
if (VFXObject == null) return;
if (Table.Instance.PlayerDataAssets.GetPlayerInfoByCivId(CivId, CivId, out var playerInfo))
{
Sprite = playerInfo.FlagIcon;
var flagBgRenderer = VFXObject.transform.Find("FlagBG2")?.GetComponent<SpriteRenderer>();
if (flagBgRenderer != null)
flagBgRenderer.color = playerInfo.Color;
}
var t = VFXObject.transform.Find("FlagIcon");
var flagIconRenderer = t != null ? t.GetComponent<SpriteRenderer>() : null;
if (flagIconRenderer == null) return;
flagIconRenderer.sprite = Sprite;
Play(playType);
}
public override void UpdateSpecialParam(GridVFXParams param)
{
CivId = param.CivId;
}
}
public class DamageGridVFXRenderer : GridVFXRenderer
{
public int Damage;
public DamageGridVFXRenderer(GridVFXParams param) : base(param)
{
Damage = param.Damage;
}
public DamageGridVFXRenderer(GridVFXParams param, GridVFXInfoDataAssets.GridVFXInfo config) : base(param)
{
Damage = param.Damage;
_config = config;
}
public override void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
if (IsPlaying) return;
if (VFXObject == null) return;
var t = VFXObject.GetComponent<TextMeshPro>();
if (t == null) return;
t.text = Damage.ToString();
Play(playType);
}
public override void UpdateSpecialParam(GridVFXParams param)
{
Damage = param.Damage;
}
public override void SetIntParam(int param)
{
Damage = param;
}
}
public class ROYALFLAMESGridVFXRenderer : GridVFXRenderer
{
public int Damage;
public ROYALFLAMESGridVFXRenderer(GridVFXParams param) : base(param)
{
}
public ROYALFLAMESGridVFXRenderer(GridVFXParams param, GridVFXInfoDataAssets.GridVFXInfo config) : base(param)
{
_config = config;
}
public override void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
if (IsPlaying) return;
if (VFXObject == null) return;
if (Sprite != null && _renderer != null)
_renderer.sprite = Sprite;
// 视觉参数改为从配置读取
ApplyVisualConfig();
Play(playType);
}
}
public class FogGridVFXRenderer : GridVFXRenderer
{
public FogGridVFXRenderer(GridVFXParams param):base(param)
{
}
}
public class TewiFrenchBuffGridVFXRenderer : GridVFXRenderer
{
public TewiFrenchBuffGridVFXRenderer(GridVFXParams param) : base(param)
{
}
public TewiFrenchBuffGridVFXRenderer(GridVFXParams param, GridVFXInfoDataAssets.GridVFXInfo config) : base(param)
{
_config = config;
}
public override void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
if (IsPlaying) return;
if (VFXObject == null) return;
if (Sprite != null && _renderer != null)
_renderer.sprite = Sprite;
ApplyVisualConfig(); // _config 为 null 时会忽略
Play(playType);
}
}
public class CoinGridVFXRenderer : GridVFXRenderer
{
public CoinGridVFXRenderer(GridVFXParams param) : base(param)
{
}
public CoinGridVFXRenderer(GridVFXParams param, GridVFXInfoDataAssets.GridVFXInfo config) : base(param)
{
_config = config;
//Debug.Log($"[GridVFX][Coin] 🪙 CTOR VFXObject={(VFXObject != null ? VFXObject.name : "NULL")}, VFXAnim={(VFXAnim != null ? VFXAnim.name : "NULL")}, Sprite={(Sprite != null ? Sprite.name : "NULL")}, _renderer={(_renderer != null ? "OK" : "NULL")}, config.GameObjectPath={(config != null ? config.GameObjectPath : "NULL-CONFIG")}");
}
public override void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
//Debug.Log($"[GridVFX][Coin][SetPlay] 🪙 ENTER playType={playType}, IsPlaying={IsPlaying}, VFXObject={(VFXObject != null ? VFXObject.name : "NULL")}, Sprite={(Sprite != null ? Sprite.name : "NULL")}");
if (IsPlaying)
{
//Debug.LogWarning("[GridVFX][Coin][SetPlay] ⚠️ SKIP (already playing)");
return;
}
if (VFXObject == null)
{
//Debug.LogWarning("[GridVFX][Coin][SetPlay] ❌ VFXObject NULL — check GridVFXInfo.GameObjectPath for Coin");
return;
}
if (Sprite != null && _renderer != null)
_renderer.sprite = Sprite;
ApplyVisualConfig(); // _config 为 null 时会忽略
Play(playType);
}
}
public class SkillIconGridVFXRenderer : GridVFXRenderer
{
public SkillType SkillType;
public UnitFullType UnitFullType;
private SpriteRenderer _skillBGRenderer;
public SkillIconGridVFXRenderer(GridVFXParams param) : base(param)
{
SkillType = param.SkillType;
UnitFullType = param.UnitFullType;
CacheSkillBGRenderer();
}
public SkillIconGridVFXRenderer(GridVFXParams param, GridVFXInfoDataAssets.GridVFXInfo config) : base(param)
{
SkillType = param.SkillType;
UnitFullType = param.UnitFullType;
_config = config;
CacheSkillBGRenderer();
}
private void CacheSkillBGRenderer()
{
if (VFXObject == null) return;
var bgTransform = VFXObject.transform.Find("SkillBG");
if (bgTransform != null)
_skillBGRenderer = bgTransform.GetComponent<SpriteRenderer>();
}
public override void UpdateSpecialParam(GridVFXParams param)
{
SkillType = param.SkillType;
UnitFullType = param.UnitFullType;
}
public override void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
if (IsPlaying) return;
if (VFXObject == null) return;
if (Table.Instance?.SkillDataAssets == null) return;
if (!Table.Instance.SkillDataAssets.GetSkillIcon(SkillType, UnitFullType, out var skillIcon)) return;
if (_renderer == null) return;
_renderer.sprite = skillIcon;
// 根据 SkillViewType 对 SkillBG 染色(对齐 UIInfoCommonBaseSkillCircleMono.SetContent
if (_skillBGRenderer != null &&
Table.Instance.SkillDataAssets.GetSkillInfo(SkillType, out var skillInfo))
{
_skillBGRenderer.color = Table.Instance.SkillDataAssets.GetBGColor(skillInfo.SkillViewType, false);
}
ApplyVisualConfig();
Play(playType);
}
}
public class FireGridVFXRenderer : GridVFXRenderer
{
public FireGridVFXRenderer(GridVFXParams param):base(param) { }
public override void Play(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
IsPlaying = true;
VFXObject?.SetActive(true);
}
}
/// <summary>
/// 通用 VFX Renderer - 使用 GridVFXInfoDataAssets 配置
/// </summary>
public class GenericGridVFXRenderer : GridVFXRenderer
{
public GenericGridVFXRenderer(GridVFXParams param, GridVFXInfoDataAssets.GridVFXInfo config) : base(param)
{
_config = config;
}
public override void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
if (IsPlaying) return;
if (VFXObject == null) return;
if (Sprite != null && _renderer != null)
_renderer.sprite = Sprite;
//应用配置中的视觉参数
ApplyVisualConfig();
Play(playType);
}
protected override void ApplyVisualConfig()
{
if (_config == null || VFXObject == null) return;
if (_renderer != null)
{
_renderer.color = _config.TintColor;
}
if (_transfrom != null)
{
_transfrom.localScale = _config.Scale;
}
}
}
/// <summary>
/// Dancing VFX从中点出发沿 X 轴 中→左→右→左→右→中 来回跳舞2 个完整正弦周期,总时长 1.6s)。
/// 跨过中点向右移动时贴图镜像翻转,向左移动时恢复正常方向。
/// 不使用 AnimationClip由 Update 程序化驱动。
/// </summary>
public class DancingGridVFXRenderer : GridVFXRenderer
{
// 配置常量(如需可调整或挪到 GridVFXInfo 扩展字段)
private const float Duration = 2.4f; // 总时长:中→左→右→左→右→中 = 2 个完整周期
private const float Frequency = 0.8333f; // 2.4s 内 2 个完整周期 = 2 / 2.4
private const float Amplitude = 2.4f; // 位移幅度(世界单位)
private Vector3 _centerPos; // 起始 localPosition视为中点
private Vector3 _baseScale; // 起始 localScale保留正负号
private float _absScaleX; // |baseScale.x|,用于翻转
private float _startTime;
private bool _wasMirrored;
public DancingGridVFXRenderer(GridVFXParams param, GridVFXInfoDataAssets.GridVFXInfo config) : base(param)
{
_config = config;
}
public override void SetPlay(GridVFXPlayType playType = GridVFXPlayType.PlayOnce)
{
if (IsPlaying) return;
if (VFXObject == null) return;
if (Sprite != null && _renderer != null) _renderer.sprite = Sprite;
ApplyVisualConfig();
// 记录起始变换作为中点基准
_centerPos = _transfrom.localPosition;
_baseScale = _transfrom.localScale;
_absScaleX = Mathf.Abs(_baseScale.x);
_wasMirrored = false;
_startTime = Time.time;
VFXObject.SetActive(true);
IsPlaying = true;
// 到时回收:复位变换后隐藏
Timer.Instance.TimerRegister(VFXObject, () =>
{
if (_transfrom != null)
{
_transfrom.localPosition = _centerPos;
_transfrom.localScale = _baseScale;
}
VFXObject?.SetActive(false);
IsPlaying = false;
}, Duration, "GridVFXRenderer_Dancing");
}
public override void Update()
{
if (!IsPlaying || _transfrom == null) return;
float t = Time.time - _startTime;
if (t > Duration) return;
// -sin 让第一个半周期往左走,符合"中→左→右→左→右→中"的顺序
float dx = -Mathf.Sin(2f * Mathf.PI * Frequency * t) * Amplitude;
_transfrom.localPosition = _centerPos + new Vector3(dx, 0f, 0f);
// 在中点右侧时镜像scale.x 取负),左侧或正中时恢复
bool shouldMirror = dx > 0f;
if (shouldMirror != _wasMirrored)
{
var s = _transfrom.localScale;
s.x = shouldMirror ? -_absScaleX : _absScaleX;
_transfrom.localScale = s;
_wasMirrored = shouldMirror;
}
}
public override void Stop()
{
IsPlaying = false;
if (_transfrom != null)
{
_transfrom.localPosition = _centerPos;
_transfrom.localScale = _baseScale;
}
VFXObject?.SetActive(false);
}
}
}