// Copyright (c) 2025 EFramework Innovation. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
namespace EFramework.Unity.UIPlugin
{
///
/// UISpriteAnimation 实现了图片序列帧动画的效果,支持从图集或单独精灵加载动画帧,是一个 UI 精灵动画组件。
///
///
///
/// 功能特性
/// - 加载模式:支持图集和精灵图片两种加载模式
/// - 参数控制:支持自定义动画帧率和循环播放等设置
///
/// 使用手册
/// 1. 基本使用
///
/// 添加组件:
/// - 在 Hierarchey 面板中选择或创建一个游戏对象(必须有Image组件)
/// - 在 Inspector 窗口点击 Add Component
/// - 选择 UI/Sprite Animation 以添加组件
///
/// 2. 模式选择
///
/// 1. 图集模式(Atlas):
/// - 从 UIAtlas 组件加载精灵
/// - 确保游戏对象上有 UIAtlas 组件
/// - 设置 Prefix(前缀) 筛选图集中的精灵
/// - 匹配前缀的精灵将按名称排序组成动画序列
///
/// 2. 精灵模式(Sprite):
/// - 直接使用精灵数组
/// - 将精灵数组赋值给 Sprites 属性
/// - 精灵将按数组顺序播放
///
/// 3. 动画设置
///
/// 1. 基本设置:
///
/// // 设置动画帧率(每秒帧数)
/// spriteAnimation.FrameRate = 30;
///
/// // 设置是否循环播放
/// spriteAnimation.Loop = true;
///
/// // 设置精灵数组(精灵模式下)
/// spriteAnimation.Sprites = mySprites;
///
/// // 设置精灵名称前缀(图集模式下)
/// spriteAnimation.Prefix = "run_";
///
/// 2. 运行控制:
///
/// // 重置动画到第一帧并重新开始播放
/// spriteAnimation.Reset();
///
/// // 检查动画是否处于活动状态
/// if (spriteAnimation.Active) {
/// Debug.Log("动画正在播放");
/// }
///
/// // 获取动画总帧数
/// int totalFrames = spriteAnimation.Frames;
///
/// 更多信息请参考模块文档。
///
[ExecuteInEditMode]
[RequireComponent(typeof(Image))]
[AddComponentMenu("UI/Sprite Animation")]
public class UISpriteAnimation : MonoBehaviour
{
///
/// SpriteMode 是精灵加载模式枚举。
/// 定义动画帧的来源方式。
///
public enum SpriteMode
{
///
/// Atlas 为图集模式,从 UIAtlas 组件获取精灵序列。
///
Atlas,
///
/// Sprite 为精灵模式,直接使用设置的精灵数组。
///
Sprite
}
///
/// spriteMode 是当前使用的精灵模式。
/// 决定动画帧的来源是图集还是精灵数组。
///
[SerializeField] private SpriteMode spriteMode = SpriteMode.Atlas;
///
/// sprites 是动画使用的精灵数组。
/// 在精灵模式下,这些精灵将按顺序组成动画序列。
///
[SerializeField] private Sprite[] sprites = new Sprite[0];
///
/// prefix 是图集中精灵名称的前缀,用于过滤精灵。
/// 在图集模式下,只有名称以此前缀开头的精灵才会被加入动画序列。
///
[SerializeField] private string prefix = "";
///
/// frameRate 是动画帧率,每秒显示的帧数。
/// 值越大动画播放速度越快。
///
[SerializeField] private int frameRate = 30;
///
/// loop 表示是否循环播放动画。
/// 若为true,动画将在播放完毕后自动从头开始。
///
[SerializeField] private bool loop = true;
///
/// nativeSize 表示是否使用精灵的原始大小。
/// 若为true,将根据每一帧精灵的原始尺寸调整Image大小。
///
[SerializeField] private bool nativeSize = false;
///
/// spriteNames 存储图集中符合前缀条件的精灵名称列表。
/// 在图集模式下使用,按名称排序后按顺序播放。
///
private readonly List spriteNames = new();
///
/// image 引用当前游戏对象上的 Image 组件。
/// 用于显示动画帧。
///
private Image image;
///
/// atlas 引用当前游戏对象上的 UIAtlas 组件。
/// 在图集模式下用于获取精灵。
///
private UIAtlas atlas;
///
/// delta 是累积的时间增量,用于计算何时切换下一帧。
/// 根据帧率和经过的时间决定何时显示下一帧。
///
private float delta = 0f;
///
/// index 是当前显示的帧索引。
/// 指示当前正在显示的动画帧位置。
///
private int index = 0;
///
/// active 表示动画是否处于活动状态。
/// 若为false,动画将停止更新。
///
private bool active = true;
///
/// Sprites 获取或设置动画使用的精灵数组。
/// 更改后会自动重建动画序列。
///
public Sprite[] Sprites { get { return sprites; } set { if (sprites != value) { sprites = value; Rebuild(); } } }
///
/// Frames 获取动画的总帧数。
/// 根据当前模式返回可用的帧数量。
///
public int Frames { get { return Sprites.Length; } }
///
/// FrameRate 获取或设置动画的帧率。
/// 控制动画播放的速度,每秒显示的帧数。
///
public int FrameRate { get { return frameRate; } set { frameRate = value; } }
///
/// Prefix 获取或设置图集中精灵名称的前缀。
/// 更改后会自动重建动画序列。
///
public string Prefix { get { return prefix; } set { if (prefix != value) { prefix = value; Rebuild(); } } }
///
/// Loop 获取或设置动画是否循环播放。
/// 控制动画播放到最后一帧后的行为。
///
public bool Loop { get { return loop; } set { loop = value; } }
///
/// Active 获取动画是否处于活动状态。
/// 判断动画是否正在播放中。
///
public bool Active { get { return active; } }
///
/// Start 组件启动时初始化。
/// 调用 Rebuild 方法准备动画序列。
///
private void Start() { Rebuild(); }
///
/// Update 每帧更新动画播放状态。
/// 根据帧率和时间计算当前应显示的帧,并更新 Image 组件。
///
private void Update()
{
var count = spriteMode == SpriteMode.Atlas ? sprites.Length : spriteNames.Count;
if (active && count > 1 && Application.isPlaying && frameRate > 0f)
{
delta += Time.deltaTime;
float rate = 1f / frameRate;
if (rate < delta)
{
delta = (rate > 0f) ? delta - rate : 0f;
if (++index >= count)
{
index = 0;
active = Loop;
}
if (active)
{
if (spriteMode == SpriteMode.Atlas)
{
image.sprite = sprites[index];
}
else if (spriteMode == SpriteMode.Sprite)
{
if (atlas)
{
image.sprite = atlas.GetSprite(spriteNames[index]);
}
}
if (nativeSize) image.SetNativeSize();
}
}
}
}
///
/// Rebuild 重建动画帧数据,根据当前模式准备精灵列表。
/// 初始化组件引用并根据当前设置准备动画序列。
///
private void Rebuild()
{
if (image == null) image = GetComponent();
if (atlas == null) atlas = GetComponent();
if (spriteMode == SpriteMode.Sprite)
{
spriteNames.Clear();
if (image && atlas)
{
var sprites = atlas.Sprites;
for (int i = 0, imax = sprites.Length; i < imax; ++i)
{
Sprite sprite = sprites[i];
if (string.IsNullOrEmpty(prefix) || sprite.name.StartsWith(prefix))
{
spriteNames.Add(sprite.name);
}
}
spriteNames.Sort();
}
}
}
///
/// Reset 重置动画到初始状态,并显示第一帧。
/// 可用于手动重新开始动画播放。
///
public void Reset()
{
active = true;
index = 0;
if (image != null)
{
if (spriteMode == SpriteMode.Atlas)
{
if (sprites.Length > 0)
{
image.sprite = sprites[index];
}
}
else if (spriteMode == SpriteMode.Sprite)
{
if (atlas && spriteNames.Count > 0)
{
image.sprite = atlas.GetSprite(spriteNames[index]);
}
}
if (nativeSize) image.SetNativeSize();
}
}
}
}