Skip to content

2D 动画与序列帧选型 ​

WasiCore 有三种常用 2D 动画路线:

  • Sprites:原生 GameUI 控件,适合 UI 元素上的简单序列帧。
  • AnimatedImageSource + CanvasAnimated:脚本层 Canvas 绘制,适合 2D 游戏对象和复杂动画数据。
  • AnimatedSprite2D + AnimationSet2D:GameGraph 场景对象,适合把 Spriter 或 Spine 放进 3D SceneGraph。

先判断需求是“UI 控件动画”“Canvas 绘制对象”还是“3D 场景中的 2D 动画对象”。不要因为它们都能显示 2D 图片就混用。

选型规则 ​

需求选择
加载动画、按钮光效、头像框、面板装饰、一次性 UI 特效Sprites
单张图或文件夹序列按 FPS 循环播放,无命名状态Sprites
需要参与 GameUI 布局、点击、透明度、ZIndex、UI inspectorSprites
2D 角色、敌人、飞行物、平台游戏对象AnimatedImageSource + CanvasAnimated
一张图集有 idle/walk/attack/death 等命名动画AnimatedImageSource + CanvasAnimated
需要图层、命中框、挂点、逐帧偏移、播放进度或调试框AnimatedImageSource + CanvasAnimated
很多对象共享同一个渲染循环和相机/物理坐标转换CanvasAnimated
Spriter / SCML 动画要放进 3D 场景AnimatedSprite2D
Spine 角色或特效要参与 3D 场景相机、透视或深度遮挡AnimatedSprite2D
UI 面板里的 Spine 展示GameUI Spine 控件,不用 AnimatedSprite2D

Sprites 用法 ​

普通单图 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)
    .AddToVisualTree();

pulse.FramePerSecond = 8;
pulse.IsLooping = true;
pulse.Play();
#endif

文件夹序列通常只需要设置图片路径、速度和播放状态:

csharp
#if CLIENT
var sparkle = new Sprites()
    .Image("image/effects/sparkle_sequence")
    .Size(96, 96)
    .AddToVisualTree();

sparkle.FramePerSecond = 12;
sparkle.IsLooping = true;
sparkle.Play();
#endif

Sprites 语义要点:

  • SpriteSize / FrameSize:普通单图模式下的单帧像素尺寸;文件夹序列模式忽略。
  • FrameCount:普通单图模式下的总帧数;文件夹序列模式下是播放上限,实际不超过子图数量。
  • RowFrameCount / FramesPerRow:普通单图模式下每行帧数;文件夹序列模式忽略。
  • StartFrame / EndFrame:播放范围,0 起始。
  • .Image("image/foo.png") 使用 UI 图片路径,不写 ui/ 前缀,不放 user_files。

AnimatedImageSource 用法 ​

需要游戏循环、命名动画、图层或命中框时,用 GameDataAnimatedImage + AnimatedImageSource,在 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);
}
#endif

AnimatedSprite2D 用法 ​

需要让 2D 动画成为 3D 场景中的对象时,用 GameGraph AnimatedSprite2D。新项目优先按 UI 资源惯例放资源:Spriter/序列帧和图片放 ui/image/...,Spine 放 ui/spine/...。代码路径从 ui/ 下一级开始,不写 ui/ 前缀,并且保留 .scml、.skel 或 .json 扩展名。旧项目的当前项目 res/... 路径仍会作为 fallback 兼容。

csharp
#if CLIENT
using GameGraph.NodeSystem;
using GameGraph.NodeSystem.Component.Graphics;
using GameGraph.ResourceSystem;
using System.Numerics;

var sceneGraph = new SceneGraph(createOctree: true);
var spineSet = AnimationSet2D.Load("spine/spineboy/spineboy.skel");
if (spineSet != null)
{
    var node = sceneGraph.CreateChild("SpineBoy");
    node!.Position = new Vector3(0f, -1.2f, 0f);
    node.LocalScale = new Vector3(0.35f);

    var sprite = node.CreateComponent<AnimatedSprite2D>();
    sprite!.AnimationSet = spineSet;
    sprite.Skin = "default";
    sprite.Layer = 10;
    sprite.OrderInLayer = 0;
    sprite.SetAnimation("walk", LoopMode2D.ForceLooped);
}
#endif

AnimatedSprite2D 不是自动 billboard,不受 3D 灯光照亮;它按节点世界变换参与透视相机和深度关系,场景融合通常通过 Color / Alpha 手动调色。完整样例见 map_templates/code_sample/src/AnimatedSprite2DSample/。

界面 Spine 与 AnimatedSprite2D 的尺寸 ​

同一套 Spine 资源在两条路线里的大小算法不同。生成代码时按目标路线选尺寸,不要互相抄数值。

界面 Spine:

  • 用 Width / Height(数据里是 Layout 宽高)作为装入区域。
  • 用资源骨架 setup 包围盒对控件做等比缩放,取宽、高两个比例中较小的一个,不拉伸。
  • 剩余空间用 HorizontalContentAlignment / VerticalContentAlignment 对齐,不是父级对齐用的 HorizontalAlignment / VerticalAlignment。
  • 不要设置不存在的骨架缩放属性。布局刷新会覆盖手动写入 Skeleton 的缩放;翻转用 ImageFlipX / ImageFlipY。

AnimatedSprite2D:

  • 加载时 Spine 坐标乘以 0.01,约 100 个 Spine 单位为 1 个世界单位。
  • 骨架缩放在动画更新时重置为翻转用的 ±1,不会按包围盒自动适配。
  • 用节点 LocalScale / 世界缩放改变大小,不要给精灵设置界面式的 Width / Height。

完整说明见 GameGraph 2D 动画组件。

场景精灵射线命中 ​

为 StaticSprite2D / AnimatedSprite2D 生成点击选取或命中逻辑前,先区分 Octree 绘制对象查询与物理碰撞查询:

  • OctreeComponent.Raycast / RaycastSingle 的常规 Aabb、Obb、Triangle、TriangleUv 级别均检测当前绘制顶点的世界 AABB。Spriter 序列帧与 Spine 都把当前帧或姿态的全部部件合并成一个包围盒;透明区域、部件间空隙及旋转后的 AABB 空白也可能命中。不要承诺 TriangleUv 能识别不透明像素。
  • 结果指向精灵所属 Node 与组件 DrawablePointer;Normal 是射线方向的反向,TextureUV 未计算,SubObject 为 uint.MaxValue,不能识别 Spine 骨骼、slot 或 attachment。
  • 使用默认 DrawableQueryFlags.Any,再按节点或组件筛选;2D 精灵经 Renderer2D 的 Geometry 入口进入 Octree,单独传 Geometry2D 无法选中该入口。不要把 Layer / OrderInLayer 当成 RaycastSingle 的最上层不透明像素保证。
  • PhysicsWorld / PhysicsWorld2D 只查询另外配置的碰撞形状。需要精确战斗命中时使用项目命中框、碰撞形状或更细的检测;渲染组件不会自动生成这些数据。

完整字段和范围见 GameGraph 2D 动画组件。

纹理发白排查 ​

当 GameGraph Spine 或 Spriter / SCML 动画比原始 PNG 明显发白时,先检查 sRGB 解码,不要直接生成用 Color 压暗的补偿代码。对于按 sRGB 创作的颜色贴图,为 atlas 页或 Spriter 散图的每张 PNG 添加同目录、同文件名的纹理参数 XML,例如 hero.png 对应 hero.xml:

xml
<texture>
    <srgb enable="true" />
</texture>
  • Spine 会按 .atlas 引用的每张页图片读取同名 XML;多页 atlas 每页都要配置。
  • Spriter / SCML 的多张图片应使用完全一致的纹理参数。参数一致时运行时仍可合并内部图集;只有部分图片存在 XML 或参数冲突时会分别加载并记录警告。
  • 法线、遮罩、查找表等线性数据纹理不要开启 sRGB。
  • 修改后重新启动 Game View 或重新进入游戏局,避免动画集和纹理缓存影响验证。
  • 只有颜色已经正确、项目仍希望适配场景氛围时,才使用 Color / Alpha 做艺术性调色。
  • 黑边、白边等透明边缘问题应另行检查 Spine atlas 的 pma: true 与贴图是否实际预乘;PMA 和 sRGB 是两个不同问题。

生成代码时避免的错误 ​

  • 不要为了简单 UI 光效创建 CanvasAnimated。
  • 不要用 Sprites 实现复杂角色状态机、命中框或多图层动画。
  • 不要把需要出现在 3D 场景里的 Spine 先画到 Canvas 再转贴图;优先用 AnimatedSprite2D。
  • 不要把 UI Spine.Resource = "Spine/hero/hero" 的路径照搬给 AnimationSet2D.Load(...);GameGraph 可以使用 AnimationSet2D.Load("spine/hero/hero.skel"),但必须写出 .skel 或 .json 后缀,且不写 ui/ 前缀。
  • 不要用界面 Spine 的 Width / Height 去设置 AnimatedSprite2D 的节点缩放,也不要给场景 Spine 查找控件式宽高。尺寸规则见“界面 Spine 与 AnimatedSprite2D 的尺寸”。
  • 不要在动画比原图发白时直接用 Color 压暗;先按“纹理发白排查”检查同名纹理 XML 和 sRGB 设置。
  • UI Spine 出现黑边时,检查 .atlas 页是否与贴图一致地声明 pma: true。PMA 读取不区分 4.x 和 3.8,UI Spine 也不会因 3.8 .skel 自动开启预乘;只有确认贴图本身已经预乘时才补该标记。完整说明见 docs/guides/ResourcePathGuide.md#ui-spine-动画。
  • 不要把单图 sprite sheet 只写成 .Image(...).Size(...);必须加 .SpriteSheet(...)。
  • CanvasAnimated 必须调用 StartTiming();动态绘制每帧开头通常调用 canvas.ResetState()。
  • 不再复用的 CanvasAnimated 要取消事件订阅并 Destroy();不要只 RemoveFromParent(),也不要只依赖父容器 ClearChildren()。
  • Canvas 绘制内容通常不出现在 ui.snapshot 中,视觉验证用截图;Sprites 可以用 UI inspector 查控件树。

继续阅读 ​

  • docs/systems/SpriteAnimationChoiceGuide.md
  • docs/systems/GameGraph2DAnimation.md
  • docs/systems/CanvasDrawingSystem.md
  • docs/systems/AnimatedImageSourceSystem.md
  • docs/guides/ResourcePathGuide.md