(client) feat:添加临时动画组件,添加逃跑逻辑

This commit is contained in:
m0_75251201
2025-09-06 12:25:55 +08:00
parent f43aeffebf
commit 15cdd2b244
73 changed files with 3420 additions and 6055 deletions

View File

@ -1,110 +1,251 @@
// Base/BaseAnimator.cs
using System;
using UnityEngine;
using Base; // 假设ITick接口在这个命名空间
namespace Base
{
// 抽象基类,封装通用动画逻辑
/// <summary>
/// 抽象基类,用于封装通用动画逻辑。
/// 所有的动画播放器都应继承自该类,并实现其抽象方法来控制具体的显示组件。
/// </summary>
public abstract class BaseAnimator : MonoBehaviour, ITick
{
// 通用公开字段(可在编辑器中设置)
[SerializeField] protected Sprite[] _sprites; // 动画精灵序列
[SerializeField] protected float _fps = 2; // 每秒帧数
[SerializeField] protected Sprite _staticSprite; // 暂停时的静态精灵
/// <summary>
/// 动画播放的精灵序列。
/// </summary>
[SerializeField] protected Sprite[] _sprites;
/// <summary>
/// 每秒帧数,控制动画播放速度。
/// </summary>
[SerializeField] protected float _fps = 2;
/// <summary>
/// 动画暂停时显示的静态精灵。
/// 如果未设置,则默认保持当前帧。
/// </summary>
[SerializeField] protected Sprite _staticSprite;
// 通用内部状态
protected bool _isPaused; // 暂停状态
protected float _frameTimer; // 帧计时器
protected int _currentFrameIndex; // 当前帧索引
// 抽象方法:子类必须实现以获取并验证其特有的显示组件
/// <summary>
/// 指示动画是否处于暂停状态。
/// </summary>
protected bool _isPaused;
/// <summary>
/// 用于跟踪当前帧播放时间的计时器。
/// </summary>
protected float _frameTimer;
/// <summary>
/// 当前正在显示的精灵帧索引。
/// </summary>
protected int _currentFrameIndex;
/// <summary>
/// 指示动画是否为一次性播放模式。
/// </summary>
protected bool _isOneShot;
/// <summary>
/// 抽象方法:子类必须实现此方法以获取并验证其特有的显示组件。
/// 例如SpriteRenderer、Image等。
/// </summary>
protected abstract void ValidateComponent();
// 抽象方法子类必须实现以设置实际显示组件的Sprite
/// <summary>
/// 抽象方法:子类必须实现此方法以将给定的精灵设置到实际显示组件上。
/// </summary>
/// <param name="sprite">要显示的精灵。</param>
protected abstract void SetDisplaySprite(Sprite sprite);
/// <summary>
/// Unity生命周期方法在脚本实例被加载时调用。
/// 主要用于初始化组件和动画的第一帧。
/// </summary>
protected virtual void Awake()
{
ValidateComponent(); // 子类获取组件
ValidateStartFrame(); // 初始化第一帧
// 子类获取并验证其显示组件
ValidateComponent();
// 初始化动画的第一帧
ValidateStartFrame();
}
// ITick接口实现
/// <summary>
/// Unity生命周期方法在第一次帧更新之前调用。
/// 用于将当前动画器添加到全局计时器。
/// </summary>
private void Start()
{
Clock.AddTick(this);
}
/// <summary>
/// Unity生命周期方法在MonoBehaviour被销毁时调用。
/// 用于将当前动画器从全局计时器中移除。
/// </summary>
private void OnDestroy()
{
Clock.RemoveTick(this);
}
/// <summary>
/// ITick接口实现每帧更新动画状态。
/// </summary>
public void Tick()
{
// 如果游戏对象不活跃,则不进行更新
if (!gameObject.activeSelf)
return;
var deltaTime = Time.deltaTime;
// 如果动画处于暂停状态,则处理暂停逻辑
if (_isPaused)
{
HandlePausedState();
return;
}
// 否则,播放动画
PlayAnimation(deltaTime);
}
/// <summary>
/// 验证并设置动画的初始帧。
/// </summary>
protected void ValidateStartFrame()
{
// 确保有精灵时可显示有效帧
// 确保有精灵序列且不为空以显示有效帧
if (_sprites != null && _sprites.Length > 0)
{
// 确保当前帧索引在有效范围内
_currentFrameIndex = Mathf.Clamp(_currentFrameIndex, 0, _sprites.Length - 1);
SetDisplaySprite(_sprites[_currentFrameIndex]); // 调用抽象方法设置Sprite
// 调用抽象方法设置当前帧的精灵
SetDisplaySprite(_sprites[_currentFrameIndex]);
}
else
{
SetDisplaySprite(null); // 调用抽象方法清空Sprite
// 如果没有精灵,则调用抽象方法清空显示组件的精灵
SetDisplaySprite(null);
// 如果没有精灵可显示,默认隐藏游戏对象
gameObject.SetActive(false);
}
}
/// <summary>
/// 处理动画处于暂停状态时的显示逻辑。
/// </summary>
protected void HandlePausedState()
{
// 优先使用静态精灵,否则保持当前帧
// 如果设置了静态精灵,则优先显示静态精灵
if (_staticSprite)
{
SetDisplaySprite(_staticSprite); // 调用抽象方法设置Sprite
// 调用抽象方法显示静态精灵
SetDisplaySprite(_staticSprite);
}
// 否则,保持当前显示的Sprite不需要额外操作SetDisplaySprite已在NextFrame中设置)
// 否则,保持当前显示的精灵SetDisplaySprite已在NextFrame或ValidateStartFrame中设置无需额外操作。
}
/// <summary>
/// 根据时间增量播放动画。
/// </summary>
/// <param name="deltaTime">自上一帧以来的时间增量。</param>
protected void PlayAnimation(float deltaTime)
{
// 如果没有精灵序列或序列为空,则清空显示并隐藏游戏对象
if (_sprites == null || _sprites.Length == 0)
{
// 如果没有精灵,确保显示组件的Sprite被清除
// 确保显示组件的精灵被清除
SetDisplaySprite(null);
// 没有精灵可显示,隐藏游戏对象
gameObject.SetActive(false);
return;
}
// 更新帧计时器
_frameTimer += deltaTime;
// 计算每帧的持续时间
var frameDuration = 1f / _fps;
// 检查帧切换条件
// 检查是否满足帧切换条件,如果一帧时间过去,则切换到下一帧
while (_frameTimer >= frameDuration)
{
_frameTimer -= frameDuration;
NextFrame();
// 如果是一次性播放模式,并且动画已播放到最后一帧并暂停,则跳出循环,防止在同一帧内进行二次处理
if (_isOneShot && _currentFrameIndex == _sprites.Length - 1 && _isPaused)
{
return;
}
}
}
/// <summary>
/// 切换到动画的下一帧。
/// </summary>
protected void NextFrame()
{
// 如果没有精灵序列或序列为空,则直接返回
if (_sprites == null || _sprites.Length == 0) return;
// 循环播放动画
_currentFrameIndex = (_currentFrameIndex + 1) % _sprites.Length;
SetDisplaySprite(_sprites[_currentFrameIndex]); // 调用抽象方法更新Sprite
// 增加帧索引
_currentFrameIndex++;
// 检查是否播放到精灵序列的末尾
if (_currentFrameIndex >= _sprites.Length)
{
// 如果是一次性播放模式
if (_isOneShot)
{
// 播放到最后一帧后停止,并保持在最后一帧
_currentFrameIndex = _sprites.Length - 1;
// 确保显示最后一帧
SetDisplaySprite(_sprites[_currentFrameIndex]);
// 调用一次性动画结束处理
FinishOneShotAnimation();
return; // 停止进一步处理
}
else
{
// 循环播放模式下,重置到第一帧
_currentFrameIndex = 0;
}
}
// 调用抽象方法更新显示组件的精灵
SetDisplaySprite(_sprites[_currentFrameIndex]);
}
// 外部控制方法
/// <summary>
/// 处理一次性动画播放结束时的逻辑。
/// </summary>
protected void FinishOneShotAnimation()
{
_isOneShot = false; // 结束一次性播放模式
_isPaused = true; // 动画暂停
gameObject.SetActive(false); // 隐藏当前游戏对象
// TODO: 如果有动画结束回调,可以在这里触发
// _onOneShotFinished?.Invoke();
}
/// <summary>
/// 设置动画的暂停状态。
/// </summary>
/// <param name="paused">如果为true动画将暂停如果为false动画将继续播放。</param>
public void SetPaused(bool paused) => _isPaused = paused;
/// <summary>
/// 设置新的精灵序列用于动画播放。
/// 设置后动画将从第一帧开始播放,并重置暂停和一次性播放模式。
/// </summary>
/// <param name="newSprites">新的精灵数组。</param>
public void SetSprites(Sprite[] newSprites)
{
_sprites = newSprites;
// 如果有新的精灵数组,则立即显示第一帧
// 如果有新的精灵数组且不为空,则立即显示第一帧
if (_sprites != null && _sprites.Length > 0)
{
_currentFrameIndex = 0; // 重置当前帧索引为第一帧
@ -112,27 +253,85 @@ namespace Base
}
else
{
SetDisplaySprite(null); // 如果没有精灵,则清空渲染器
SetDisplaySprite(null); // 如果没有精灵,则清空显示组件的精灵
gameObject.SetActive(false); // 如果没有精灵可显示,隐藏游戏对象
}
// 重置帧计时器,以确保从头开始播放
// 重置帧计时器,以确保从新精灵序列的开头开始播放
_frameTimer = 0f;
_isOneShot = false; // 设置新精灵时,取消一次性播放模式
_isPaused = false; // 默认开始播放
}
/// <summary>
/// 将动画状态恢复到初始设置。
/// 通常恢复到第一帧并暂停。
/// </summary>
public void Restore()
{
_currentFrameIndex = 0;
_isOneShot = false; // 重置时取消一次性播放模式
_isPaused = true; // 默认恢复后暂停
_frameTimer = 0f;
// 如果有精灵序列且不为空,则恢复到第一帧并尝试显示
if (_sprites != null && _sprites.Length > 0)
{
SetDisplaySprite(_sprites[_currentFrameIndex]); // 恢复到第一帧
// 如果游戏对象当前不活跃,则激活它使其可见
if (!gameObject.activeSelf)
{
gameObject.SetActive(true);
}
}
else
{
SetDisplaySprite(null);
SetDisplaySprite(null); // 如果没有精灵,则清空显示
gameObject.SetActive(false); // 隐藏游戏对象
}
}
/// <summary>
/// 以一次性播放模式播放动画。
/// 动画将从第一帧播放到最后一帧,然后停止并隐藏游戏对象。
/// </summary>
public void PlayOneShot()
{
// 如果没有精灵序列或序列为空,则发出警告并隐藏游戏对象
if (_sprites == null || _sprites.Length == 0)
{
Debug.LogWarning("无法播放一次性动画:未分配精灵。", this);
gameObject.SetActive(false); // 没有精灵可显示,隐藏游戏对象
return;
}
// 重置动画状态为一次性播放模式
_isOneShot = true;
_isPaused = false;
_currentFrameIndex = 0;
_frameTimer = 0f;
// 确保游戏对象处于激活状态才能播放
if (!gameObject.activeSelf)
{
gameObject.SetActive(true);
}
// 立即显示第一帧
SetDisplaySprite(_sprites[_currentFrameIndex]);
}
/// <summary>
/// 设置动画的每秒帧数FPS
/// FPS值将被限制在最小0.1,以防止除零或过小的帧率。
/// </summary>
/// <param name="newFPS">新的每秒帧数。</param>
public void SetFPS(float newFPS) => _fps = Mathf.Max(0.1f, newFPS);
/// <summary>
/// 设置动画暂停时显示的静态精灵。
/// </summary>
/// <param name="sprite">新的静态精灵。</param>
public void SetStaticSprite(Sprite sprite) => _staticSprite = sprite;
}
}

View File

@ -44,7 +44,6 @@ namespace Prefab
{
orientationDict[orientation] = inf; // 所有值都指向同一个列表
}
entity.bodyAnimationNode[state] = orientationDict;
}
outline.Init();

View File

@ -11,26 +11,18 @@ namespace Prefab
protected override void ValidateComponent()
{
_renderer = GetComponent<SpriteRenderer>();
if (_renderer == null)
{
Debug.LogError("SpriteAnimator requires a SpriteRenderer component.", this);
enabled = false; // 禁用脚本如果组件缺失
}
if (_renderer != null) return;
Debug.LogError("SpriteAnimator requires a SpriteRenderer component.", this);
enabled = false; // 禁用脚本如果组件缺失
}
protected override void SetDisplaySprite(Sprite sprite)
{
if (_renderer != null)
if (_renderer)
{
_renderer.sprite = sprite;
}
}
// [可选] 如果SpriteAnimator需要任何Awake后特有的初始化逻辑可以在这里重写
// protected override void Awake()
// {
// base.Awake(); // 确保调用基类的Awake
// // 子类特有的初始化
// }
}
}

View File

@ -0,0 +1,74 @@
using System;
using Base;
using UnityEngine;
namespace Prefab
{
public class TemporaryAnimator : MonoBehaviour, ITick
{
[Tooltip("动画持续时间")]
public float lifeTime = 3;
private float _timer; // 每次启用时会重置为0
// 使用属性限制外部只能获取和一次性设置动画函数
[Tooltip("定义X轴偏移的函数 (时间 -> X偏移量)")]
public Func<float, float> GetXPosition { get; private set; }
[Tooltip("定义Y轴偏移的函数 (时间 -> Y偏移量)")]
public Func<float, float> GetYPosition { get; private set; }
private Vector3 _originalPosition;
private void Start()
{
_originalPosition = transform.position; // 初始位置只需在组件生命周期中获取一次
}
private void OnEnable()
{
_timer = 0; // 每次启用时重置计时器,确保动画从头开始
Clock.AddTick(this); // 在启用时注册到全局Tick系统
}
private void OnDisable()
{
// 在禁用或销毁时从全局Tick系统取消注册避免不必要的调用和资源泄露
Clock.RemoveTick(this);
}
public virtual void Tick()
{
_timer += Time.deltaTime;
// 钳制当前动画时间确保最后一帧使用精确的lifeTime值
// 避免动画函数接收到超出预期范围的时间参数
var currentAnimationTime = Mathf.Min(_timer, lifeTime);
var shouldDestroy = _timer >= lifeTime; // 判断是否达到销毁条件
// 调用委托获取X和Y轴的偏移量如果委托未设置则默认为0
var xOffset = GetXPosition?.Invoke(currentAnimationTime) ?? 0f;
var yOffset = GetYPosition?.Invoke(currentAnimationTime) ?? 0f;
// 应用偏移量到物体位置保持原始Z轴不变
transform.position = _originalPosition + new Vector3(xOffset, yOffset, 0);
// 如果达到销毁条件,在完成最后一帧动画更新后销毁物体
if (shouldDestroy)
{
Destroy(this.gameObject);
}
}
/// <summary>
/// 设置动画的X和Y轴位移函数。
/// 此方法通常在实例化TemporaryAnimator后立即调用以配置其动画行为。
/// </summary>
/// <param name="getXPosFunc">时间到X轴位移的函数。</param>
/// <param name="getYPosFunc">时间到Y轴位移的函数。</param>
public void SetAnimationFunctions(Func<float, float> getXPosFunc, Func<float, float> getYPosFunc)
{
GetXPosition = getXPosFunc;
GetYPosition = getYPosFunc;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 82401f6330c143b39063d7bd3401912f
timeCreated: 1757033312

View File

@ -0,0 +1,19 @@
using UnityEngine;
using UnityEngine.UI;
namespace Prefab
{
public class TemporaryAnimatorImageUI:TemporaryAnimator
{
public UIImageAnimator imageAnimator;
public void Init(Sprite[] sprite,float fps=3)
{
if (imageAnimator == null)
imageAnimator = GetComponentInChildren<UIImageAnimator>();
if (!imageAnimator) return;
imageAnimator.SetSprites(sprite);
imageAnimator.SetFPS(fps);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 41e1382eba7740a197fe36daececa4f1
timeCreated: 1757036250

View File

@ -0,0 +1,20 @@
using UnityEngine;
namespace Prefab
{
public class TemporaryAnimatorSprite:TemporaryAnimator
{
[SerializeField]
private SpriteAnimator spriteAnimator;
public void Init(Sprite[] textures,float fps=3)
{
if(spriteAnimator == null)
spriteAnimator = GetComponentInChildren<SpriteAnimator>();
if (!spriteAnimator) return;
spriteAnimator.SetSprites(textures);
spriteAnimator.SetFPS(fps);
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 200d5281b8d2443da52763a0f06ec0d8
timeCreated: 1757033388

View File

@ -0,0 +1,151 @@
using Base;
using TMPro;
using UnityEngine;
namespace Prefab
{
/// <summary>
/// 临时动画文本组件,用于在指定时间内逐帧显示一系列文本动画。
/// 该组件假设 TemporaryAnimator 实现了 ITickable 接口,因此可以在游戏循环中接收更新。
/// </summary>
public class TemporaryAnimatorText : TemporaryAnimator
{
/// <summary>
/// 用于显示动画文本的 TMP_Text 组件。
/// 允许在编辑器中赋值,以增加灵活性和鲁棒性。
/// </summary>
[SerializeField] public TMP_Text text;
/// <summary>
/// 当前播放的动画帧索引。
/// </summary>
private int currentFrameIndex = 0;
/// <summary>
/// 存储所有动画帧文本的字符串数组。每个元素代表一帧动画内容。
/// </summary>
private string[] animationsKey;
/// <summary>
/// 每帧动画的持续时间。根据帧率FPS计算得出。
/// </summary>
private float frameDuration;
/// <summary>
/// 用于跟踪当前帧已持续时间的计时器。
/// </summary>
private float timer;
/// <summary>
/// 初始化动画文本组件。
/// </summary>
/// <param name="s">包含所有动画帧文本的字符串,多个帧之间用逗号分隔。</param>
/// <param name="fps">动画播放的帧率Frames Per Second。</param>
public void Init(string s, float fps=3)
{
// 如果文本组件未赋值,尝试在子对象中查找。
// 如果仍未找到,则记录错误并提前退出。
if (!text)
{
text = GetComponentInChildren<TMP_Text>();
if (!text)
{
Debug.LogError("TemporaryAnimatorText: 未在子对象中找到或未指定 TMP_Text 组件。动画无法进行。", this);
return; // 提前退出 Init 方法,避免后续操作使用未初始化的 text
}
}
// 处理输入字符串为 null 或空字符串的情况。
// 在这种情况下,动画将显示空字符串或停止。
if (string.IsNullOrEmpty(s))
{
Debug.LogWarning("TemporaryAnimatorText: 输入的动画字符串为空或为 null。动画将显示空字符串或停止。", this);
animationsKey = new string[] { "" }; // 确保 animationsKey 不为 null 且至少包含一个空字符串元素
}
else
{
animationsKey = s.Split(',');
// 如果 s.Split(',') 结果为空数组(这种情况极少见,但在某些 Split 重载下可能发生),也应进行处理。
if (animationsKey.Length == 0)
{
Debug.LogWarning("TemporaryAnimatorText: 输入的动画字符串导致动画帧数组为空。动画将显示空字符串或停止。", this);
animationsKey = new string[] { "" };
}
}
// 检查帧率FPS的合法性。FPS 必须是大于 0 的正值。
// 如果帧率不合法,则设置为默认值,防止除以零或动画速度异常。
if (fps <= 0)
{
Debug.LogError("TemporaryAnimatorText: FPS 必须是大于 0 的正值。将动画帧持续时间设置为 1 秒。", this);
this.frameDuration = 1f; // 默认每帧1秒实际效果是动画暂停或非常慢
}
else
{
this.frameDuration = 1f / fps;
}
// 重新初始化动画时,将当前帧索引重置为第一帧。
currentFrameIndex = 0;
// 在 animationsKey 准备好后,设置初始文本为第一帧的内容。
if (text && animationsKey.Length > 0)
{
text.text = animationsKey[currentFrameIndex];
}
// 如果动画帧数组为空,但文本组件存在,则清空文本显示。
else if (text)
{
text.text = "";
}
// 重置计时器,从零开始计算新帧的持续时间。
timer = 0f;
}
/// <summary>
/// 每帧更新动画显示。该方法将在游戏循环中被 Clock 调用。
/// </summary>
public override void Tick()
{
base.Tick();
if (!text)
{
Debug.LogWarning("TemporaryAnimatorText: 在 Tick 方法执行期间 TMP_Text 组件为 null。正在从 Clock 中移除。", this);
return; // 无法执行动画,直接返回
}
// 检查动画帧数组是否已初始化或为空。
// 如果没有动画帧,则无需更新,直接返回。
if (animationsKey == null || animationsKey.Length == 0)
{
// Debug.LogWarning("TemporaryAnimatorText: 动画键未初始化或为空。跳过 Tick 更新。", this); // 这条日志已被注释掉,避免频繁输出不必要的警告
return; // 没有动画帧,直接返回
}
// 增加计时器。
timer += Time.deltaTime;
// 如果当前帧的持续时间已达到或超过预设的帧持续时间,则更新到下一帧。
if (!(timer >= frameDuration)) return;
// 减去一帧的持续时间,以便计算下一帧的剩余时间,或者处理帧率不精确导致的累计误差。
timer -= frameDuration;
// 移动到下一帧。
currentFrameIndex += 1;
// 如果当前帧索引超出动画帧数组的范围,则循环回到第一帧。
if (currentFrameIndex >= animationsKey.Length)
{
currentFrameIndex = 0;
}
// 更新文本组件显示为当前帧的文本内容。
text.text = animationsKey[currentFrameIndex];
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 28e36606550e40a895acad13a6aa1edf
timeCreated: 1757036151

View File

@ -1,9 +1,12 @@
// UI/UIImageAnimator.cs
using Base;
using UnityEngine;
using UnityEngine.UI;
using Base; // 引入Base命名空间以使用BaseAnimator
namespace UI
// 引入Base命名空间以使用BaseAnimator
namespace Prefab
{
[RequireComponent(typeof(Image))]
public class UIImageAnimator : BaseAnimator // 继承BaseAnimator