主题
GameUI 序列帧选型指南
WasiCore 中常见的 2D 序列帧有两条路线:
GameUI.Control.Primitive.Sprites:原生 GameUI 控件,自己推进帧并参与 UI 布局树。GameCore.Animation.AnimatedImageSource+CanvasAnimated:脚本层动画数据源,在 Canvas 渲染循环中手动绘制。
两者都能播放 sprite sheet,但抽象层级不同。选择时先判断它是“一个 UI 控件”还是“游戏画面里由脚本驱动的绘制对象”。
快速决策
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 加载光效、按钮高亮、头像边框、UI 面板上的循环小动画 | Sprites | 作为普通 UI 控件使用,能直接设置尺寸、对齐、透明度、层级和父子关系。 |
| 单张图片或文件夹序列,只需要按 FPS 循环播放 | Sprites | 不需要自建渲染循环,也不需要每帧执行 C# 绘制代码。 |
| 2D 角色、敌人、飞行物、平台游戏对象 | AnimatedImageSource + CanvasAnimated | 需要和游戏循环、物理、相机、批量对象位置一起绘制。 |
| 一张图集里有 idle/walk/attack/death 等多个命名动画 | AnimatedImageSource + CanvasAnimated | GameDataAnimatedImage.AnimationTags 能表达命名动画、循环方向、结束行为。 |
| 需要图层、命中框、挂点、逐帧偏移、调试框 | AnimatedImageSource + CanvasAnimated | AnimatedImageFrameLayer / LayerDefines / GetLayerBounds 更适合这类数据。 |
| 需要 UI inspector 中看到独立控件并用布局工具排查 | Sprites | Canvas 直接绘制的内容通常不会出现在 GameUI 控件树里。 |
| 一屏有很多由相同循环驱动的 2D 对象 | 通常用 CanvasAnimated | 可以在一个渲染循环中统一更新和绘制,避免创建大量 UI 控件。 |
Sprites:UI 控件型序列帧
Sprites 适合“它本质上是一个 UI 元素”的动画。它继承自 Control,因此可以使用 GameUI 的布局、事件、可见性、透明度、缩放、父子关系和生命周期规则。
普通单图 sprite sheet 需要告诉控件如何切帧:
csharp
#if CLIENT
using GameUI.Control.Extensions;
using GameUI.Control.Primitive;
var pulse = new Sprites()
.Image("image/effects/energy_pulse_sheet.png")
.SpriteSheet(frameWidth: 128, frameHeight: 128, frameCount: 8, framesPerRow: 8)
.SpriteFrames(startFrame: 0, endFrame: 7)
.Size(128, 128)
.Center()
.AddToVisualTree();
pulse.FramePerSecond = 8;
pulse.IsLooping = true;
pulse.Play();
#endif文件夹序列资源由 native 侧从子图列表推导帧区域,通常不需要 SpriteSheet(...):
csharp
#if CLIENT
var sparkle = new Sprites()
.Image("image/effects/sparkle_sequence")
.Size(96, 96)
.AddToVisualTree();
sparkle.FramePerSecond = 12;
sparkle.IsLooping = true;
sparkle.Play();
#endifSprites 参数语义
| 属性 / 方法 | 普通图片文件 | 文件夹序列资源 |
|---|---|---|
SpriteSize / FrameSize | 单帧像素尺寸,用来计算裁剪区域 | 不参与切帧,使用子图自身区域 |
FrameCount | 总帧数 | 播放上限,native 侧不会超过真实子图数量 |
RowFrameCount / FramesPerRow | 每行帧数;未配置时普通图片模式不会绘制 | 忽略 |
StartFrame / EndFrame | 播放范围,0 起始 | 同样表示子图下标范围 |
IntervalMillisecond / FramePerSecond | 控制播放速度 | 控制播放速度 |
Play() / Stop() | 开始或停止推进帧 | 开始或停止推进帧 |
如果传入的是单张 .png sprite sheet,只设置 .Image(...) 和 .Size(...) 不足以切帧,必须设置 FrameCount 和 RowFrameCount,推荐直接使用 .SpriteSheet(...)。
AnimatedImageSource:Canvas 绘制型序列帧
AnimatedImageSource 适合“它是游戏画面中的一个动画对象”。动画数据放在 GameDataAnimatedImage 中,代码在 CanvasAnimated.OnAnimatedRender 里按当前时间绘制。
csharp
#if CLIENT
using GameCore.Animation;
using GameUI.Control.Primitive;
using GameUI.Control.Primitive.Struct;
private static CanvasAnimated? canvas;
private static AnimatedImageSource? hero;
private static void CreateScene()
{
hero = new AnimatedImageSource(MyGameData.HeroAnimatedImage);
hero.PlayAnimation("idle", canvasElapsedMilliseconds: 0);
canvas = new CanvasAnimated();
canvas.FullScreen();
canvas.StartTiming();
canvas.OnAnimatedRender += DrawFrame;
canvas.AddToVisualTree();
}
private static void DrawFrame(object? sender, CanvasAnimatedEventArgs e)
{
if (canvas == null || hero == null)
{
return;
}
canvas.ResetState();
canvas.DrawAnimatedImage(hero, x: 320f, y: 480f);
}
#endifAnimatedImageSource 的优势来自数据结构,而不是控件树:
AnimationTags:一张图集上定义多个命名动画。LoopDirection/EndBehavior:控制往返、倒放、结束后隐藏或回退。LayerDefines/FrameLayer:表达图层、命中框、武器层、特效层等。GetLayerBounds(...):把动画数据用于碰撞、拾取、受击判断或调试绘制。CanvasAnimatedEventArgs:用DeltaTime和总时间把动画、物理和相机逻辑放到同一个渲染循环里。
常见误用
| 误用 | 问题 | 推荐修正 |
|---|---|---|
为一个按钮呼吸光效创建 CanvasAnimated | 需要额外渲染循环和事件清理,UI inspector 也看不到动画内容 | 用 Sprites 作为按钮子控件或背景控件 |
用 Sprites 做角色 idle/walk/attack 状态机 | 缺少命名动画、图层、命中框和游戏循环上下文 | 用 AnimatedImageSource + CanvasAnimated |
单图 sprite sheet 只写 .Image(...).Size(...) | 控件不知道每帧尺寸和每行帧数,可能只显示首帧或不绘制 | 加 .SpriteSheet(width, height, frameCount, framesPerRow) |
把图片路径写成 ui/image/foo.png | GameUI/Canvas 资源路径从 ui/ 下一级开始 | 写 image/foo.png |
不再使用的 CanvasAnimated 只 RemoveFromParent() | 渲染事件和底层资源可能继续占用 | 取消事件订阅后调用 Destroy() |
调试与验证
Sprites是 GameUI 控件,可以用ui.snapshot、ui.find、ui.get_rect检查控件树和布局。AnimatedImageSource绘制在 Canvas 里,通常不出现在控件树中;验证画面时优先用截图。- Canvas 动画需要调用
StartTiming()才会触发OnAnimatedRender。 - 普通 UI 控件关闭后永久不用时调用
Destroy();CanvasAnimated若手动订阅事件,先取消订阅再Destroy()。