Files
youlegames/codes/games/client/Projects/Game_Surface_3/docs/Spine动画集成手册.md
2026-04-09 17:31:46 +08:00

26 KiB
Raw Blame History

Spine 动画集成手册

本手册针对 gameabc 引擎 的 Spine 项目,说明如何在 Canvas 2D 游戏中加载、 控制和管理 Spine 骨骼动画。
运行时版本:spine-canvas 4.2 | 引擎:gameabc


目录

  1. 项目结构总览
  2. 快速开始5 分钟跑通
  3. 从 Spine 编辑器导出资源
  4. SpineMgr 完整 API 参考
  5. 事件系统
  6. 常见用法示例
  7. 与 gameabc 精灵系统配合
  8. 性能优化建议
  9. 常见问题排查
  10. 附录:文件加载顺序

1. 项目结构总览

Projects/Spine/
├── index.html                  ← 入口 HTML
├── js/
│   ├── spine-canvas.js         ← Spine Canvas 2D 运行时 (第三方库)
│   ├── gameabc.min.js          ← gameabc 游戏引擎
│   ├── SpineMgr.js             ← ★ Spine 动画管理器(独立文件,自动挂钩渲染)
│   ├── gamemain.js             ← 游戏主逻辑(无需修改)
│   ├── Spine_Event.js          ← Spine 事件回调 (complete / event)
│   └── Project1_Event.js       ← 精灵事件单元
├── assets/
│   ├── bmp/                    ← gameabc 图片资源
│   └── spine/                  ← ★ Spine 资源目录
│       ├── xxx.json              ← 骨骼数据 (Spine 编辑器导出)
│       ├── xxx.atlas             ← 图集描述文件
│       └── xxx.png               ← 图集纹理图片
├── output/                     ← gameabc 编译输出的配置数据
├── save/                       ← gameabc 编辑器保存的 XML
└── docs/                       ← 本文档所在目录

关键文件说明

文件 作用 需要修改
js/spine-canvas.js Spine 4.2 Canvas 渲染运行时 ✗ 不要修改
js/SpineMgr.js Spine 动画管理器,自动初始化+自动渲染 ✗ 不需要修改
js/gamemain.js 游戏主逻辑(保持原样) ✗ 不需要修改
js/Spine_Event.js Spine 动画完成/自定义事件回调 ✓ 处理动画事件
assets/spine/* Spine 导出的资源文件 ✓ 放入你的资源

2. 快速开始5 分钟跑通

第一步:准备 Spine 资源

将 Spine 编辑器导出的 3 个文件复制到 assets/spine/ 目录:

assets/spine/
├── hero.json       ← 骨骼 JSON
├── hero.atlas      ← 图集描述
└── hero.png        ← 图集纹理

第二步:在任意位置调用 API 播放动画

无需手动 load,直接调用 setAnimation 即可。如果该 Spine 实例尚未加载,会自动根据 id 查找资源(约定文件名 id.json / id.atlas并创建实例。 渲染也已自动挂钩到 gameenddraw,开发者无需编写任何渲染代码。

在任何 gameabc 事件回调中直接调用即可:

// 设置位置 + 播放动画,无需先 load
gameabc_face.spineMgr.setPosition("hero", 640, 500);
gameabc_face.spineMgr.setAnimation("hero", "idle", true);

// 之后切换动画
gameabc_face.spineMgr.setAnimation("hero", "attack", false);

约定id 必须与资源文件名一致(不含扩展名)。例如 id 为 "hero" 则需要 assets/spine/hero.jsonhero.atlashero.png

可选:如果资源不在默认路径 assets/spine/,可手动调用一次 init()

gameabc_face.spineMgr.init("other/path/");

第三步:打开 index.html 即可运行

用浏览器打开 index.html(需要 HTTP 服务器环境,不能直接 file://

# 简易方法:在项目目录下启动 HTTP 服务
cd codes/games/client/Projects/Spine
npx http-server -p 8080
# 浏览器访问 http://localhost:8080

或者使用 gameabc 自带的预览环境。


3. 从 Spine 编辑器导出资源

导出设置Spine 编辑器 → Export

  1. 打开 Spine 编辑器,加载你的 .spine 项目
  2. 点击菜单 Spine → Export...
  3. 左侧选择 JSON 格式
  4. 配置项:
设置项 推荐值 说明
Output folder assets/spine/ 直接导出到项目目录
Extension .json 骨骼数据格式
Create atlas ✓ 勾选 同时生成图集
Atlas extension .atlas 图集描述格式
Images folder (默认) 纹理来源
Max width/height 2048 / 2048 图集纹理最大尺寸
Pack settings → Power of two ✓ 勾选 纹理尺寸为 2 的幂
Pack settings → Premultiply alpha ✗ 不勾选 Canvas 2D 不需要预乘
  1. 点击 Export 按钮

导出后得到的文件

hero.json      ← 骨骼数据,包含骨骼、插槽、动画等
hero.atlas     ← 图集描述,记录每个区域在纹理中的位置
hero.png       ← 图集纹理图片

注意:如果图集很大被拆分成多张(hero.pnghero2.png),所有 .png 都需要放到 assets/spine/ 目录中。.atlas 文件会自动引用它们。


4. SpineMgr 完整 API 参考

SpineMgr 挂载在 gameabc_face.spineMgr 上,所有调用形如:

gameabc_face.spineMgr.方法名(参数);

4.1 init(basePath) (可选)

手动设置资源根路径。通常无需调用——load() 会自动以默认路径 "assets/spine/" 初始化。 仅在资源放在其他目录时才需要调用:

gameabc_face.spineMgr.init("other/spine/path/");
参数 类型 说明
basePath string Spine 资源文件的根目录路径,需要以 / 结尾。省略则使用默认值 "assets/spine/"

4.2 load(id, jsonFile, atlasFile, option) (可选)

手动加载一组 Spine 资源并注册为一个实例。通常无需调用——所有 API 在实例不存在时会自动加载。

仅在以下情况需要手动调用 load()

  • id 与文件名不一致(如 id 为 "mj" 但文件为 mj_gangshangkaihua.json
  • 需要在加载时指定 skin、scale 等初始参数
gameabc_face.spineMgr.load("hero", "hero.json", "hero.atlas", {
    x: 640,
    y: 500,
    scale: 0.5,
    skin: "default",
    animation: "idle",
    loop: true,
    mixDuration: 0.2
});
参数 类型 说明
id string 唯一标识,后续所有操作通过此 id 引用
jsonFile string 骨骼 JSON 文件名(相对于 basePath
atlasFile string 图集 atlas 文件名(相对于 basePath
option object 可选配置对象

option 字段:

字段 类型 默认值 说明
x number 0 Canvas X 坐标
y number 0 Canvas Y 坐标
scale number 1 初始缩放比例
skin string "default" 初始皮肤名称
animation string null 加载完成后自动播放的动画名
loop boolean true 默认动画是否循环
mixDuration number 0.2 动画切换时的过渡混合时长(秒)

4.3 setAnimation(id, animName, loop, track)

切换动画。立即替换指定轨道上的当前动画。如果实例尚未加载,会自动根据 id 加载资源并在就绪后播放。

// 无需先 load直接调用即可
gameabc_face.spineMgr.setAnimation("hero", "attack", false);
gameabc_face.spineMgr.setAnimation("hero", "run", true, 0);
参数 类型 默认值 说明
id string - 实例标识
animName string - 动画名称(必须在 Spine 中存在)
loop boolean true 是否循环播放
track number 0 轨道号(多轨道可叠加动画)

返回值: TrackEntry 对象,可用于进一步控制;加载未完成时返回 null


4.4 addAnimation(id, animName, loop, delay, track)

将动画添加到播放队列,在当前动画结束后自动播放。

// 先播放 attackattack 完成后自动切换到 idle
gameabc_face.spineMgr.setAnimation("hero", "attack", false);
gameabc_face.spineMgr.addAnimation("hero", "idle", true, 0);
参数 类型 默认值 说明
id string - 实例标识
animName string - 队列中的下一个动画
loop boolean true 是否循环
delay number 0 延迟秒数0 = 上一动画结束时立即开始)
track number 0 轨道号

4.5 setPosition(id, x, y)

设置 Spine 实例在 Canvas 上的位置。

gameabc_face.spineMgr.setPosition("hero", 640, 500);

Spine 的坐标原点在骨骼的根骨骼处。Y 轴向上为正(与 Canvas 的 Y 轴方向相反), spine-canvas 运行时已做了内部转换。


4.6 setScale(id, sx, sy)

设置缩放。

gameabc_face.spineMgr.setScale("hero", 0.5);       // 等比缩放
gameabc_face.spineMgr.setScale("hero", 0.5, 0.8);  // 分别设置 X/Y
参数 类型 说明
sx number X 方向缩放
sy number Y 方向缩放(省略则等于 sx

4.7 setFlip(id, flipX, flipY)

水平/垂直翻转。

gameabc_face.spineMgr.setFlip("hero", true, false);  // 水平翻转

4.8 setVisible(id, visible)

显示或隐藏 Spine 实例。

gameabc_face.spineMgr.setVisible("hero", false);  // 隐藏
gameabc_face.spineMgr.setVisible("hero", true);   // 显示

4.9 setSkin(id, skinName)

切换皮肤。

gameabc_face.spineMgr.setSkin("hero", "warrior");

切换皮肤后会自动重置插槽到 Setup Pose。皮肤名必须在 Spine 编辑器中预定义。


4.10 getAnimations(id)

获取该实例所有可用动画名称列表。

var anims = gameabc_face.spineMgr.getAnimations("hero");
// 返回: ["idle", "walk", "run", "attack", "die"]
logmessage("动画列表: " + anims.join(", "));

4.11 getSkins(id)

获取该实例所有可用皮肤名称列表。

var skins = gameabc_face.spineMgr.getSkins("hero");
// 返回: ["default", "warrior", "mage"]

4.12 playOnce(id, animName, track)

播放一次动画后自动隐藏。 自动显示实例、播放指定动画(不循环),动画完成后自动设置 visible = false

// 播放一次攻击动画,播完自动隐藏
gameabc_face.spineMgr.playOnce("hero", "attack");

// 指定轨道
gameabc_face.spineMgr.playOnce("hero", "attack", 0);
参数 类型 默认值 说明
id string - 实例标识
animName string - 动画名称
track number 0 轨道号

4.13 playQueue(id, animList, hideOnComplete)

按顺序播放一组动画(队列),全部播完后可选择隐藏或保持显示。 自动显示实例,队列中每个动画均播放一次(不循环),依次播放。

// 播放 attack → die全部播完后自动隐藏默认
gameabc_face.spineMgr.playQueue("hero", ["attack", "die"]);

// 播放 intro → idle全部播完后保持显示
gameabc_face.spineMgr.playQueue("hero", ["intro", "idle"], false);
参数 类型 默认值 说明
id string - 实例标识
animList string[] - 动画名称数组,按顺序依次播放
hideOnComplete boolean true true = 队列全部播完后自动隐藏;false = 保持显示

4.14 remove(id)

销毁指定 Spine 实例,释放内存。

gameabc_face.spineMgr.remove("hero");

4.15 removeAll()

销毁所有 Spine 实例。

gameabc_face.spineMgr.removeAll();

4.16 spine_onComplete(spineId, animName, trackIndex) (事件回调)

动画完成回调。每次动画播放一轮结束时触发。js/Spine_Event.js 中定义。

// 在 Spine_Event.js 中定义
gameabc_face.spine_onComplete = function(spineId, animName, trackIndex) {
    // 示例:攻击播完后恢复 idle
    if (animName === "attack") {
        gameabc_face.spineMgr.setAnimation(spineId, "idle", true);
    }
};
参数 类型 说明
spineId string 实例标识(即 load / autoLoad 时的 id
animName string 刚完成的动画名称
trackIndex number 轨道号(通常为 0

注意:循环动画每播完一轮也会触发。playOnce / playQueue 的自动隐藏在此回调之前执行, 因此回调中可以检查 visible 状态或重新显示实例。


4.17 spine_onEvent(spineId, eventName, intValue, floatValue, stringValue) (事件回调)

自定义事件回调。当动画播放到 Spine 编辑器中定义的 Event 关键帧时触发。js/Spine_Event.js 中定义。

// 在 Spine_Event.js 中定义
gameabc_face.spine_onEvent = function(spineId, eventName, intValue, floatValue, stringValue) {
    if (eventName === "footstep") {
        // 播放脚步声
    }
    if (eventName === "hit") {
        // 产生伤害判定
    }
};
参数 类型 说明
spineId string 实例标识
eventName string Spine 编辑器中定义的事件名
intValue number 事件的整数参数
floatValue number 事件的浮点参数
stringValue string 事件的字符串参数

事件需要在 Spine 编辑器的时间线中预先添加 Event Key导出 JSON 后运行时自动解析。


5. 事件系统(详细说明)

Spine 动画在运行时会触发两类事件,回调定义在 js/Spine_Event.js 中。 API 签名见 4.164.17

5.1 动画完成事件 spine_onComplete

每次动画循环播放一轮结束时触发。

// 在 Spine_Event.js 中
gameabc_face.spine_onComplete = function(spineId, animName, trackIndex) {
    // spineId    : load 时的唯一标识, 如 "hero"
    // animName   : 完成的动画名, 如 "attack"
    // trackIndex : 轨道号 (通常为 0)

    // 示例:非循环攻击动画播完后恢复 idle
    if (animName === "attack") {
        gameabc_face.spineMgr.setAnimation(spineId, "idle", true);
    }
};

5.2 自定义事件 spine_onEvent

当动画播放到 Spine 编辑器中定义的 Event 关键帧时触发。

// 在 Spine_Event.js 中
gameabc_face.spine_onEvent = function(spineId, eventName, intValue, floatValue, stringValue) {
    // spineId     : 唯一标识
    // eventName   : Spine 编辑器中定义的事件名
    // intValue    : 整数参数
    // floatValue  : 浮点参数
    // stringValue : 字符串参数

    if (eventName === "footstep") {
        // 播放脚步声
    }
    if (eventName === "hit") {
        // 产生伤害判定
    }
};

如何在 Spine 编辑器中添加事件

  1. 打开 Spine 编辑器,选中动画
  2. 在时间线底部点击右键 → Add Event Key
  3. Tree 面板中创建并命名事件(如 hitfootstep
  4. 可为事件设置 int / float / string 参数
  5. 导出 JSON 后,运行时会自动解析并触发回调

6. 常见用法示例

6.1 加载多个角色

// 在 gamestart 或任意时机,直接设置位置并播放(自动加载)
gameabc_face.spineMgr.setPosition("hero", 640, 500);
gameabc_face.spineMgr.setAnimation("hero", "idle", true);

gameabc_face.spineMgr.setPosition("npc", 300, 500);
gameabc_face.spineMgr.setAnimation("npc", "idle", true);

gameabc_face.spineMgr.setPosition("monster", 900, 500);
gameabc_face.spineMgr.setAnimation("monster", "walk", true);

6.2 播放一次后隐藏 / 队列播放

// 播放一次攻击动画,完成后自动隐藏
gameabc_face.spineMgr.setPosition("effect", 640, 400);
gameabc_face.spineMgr.playOnce("effect", "explode");

// 队列播放:攻击 → 死亡,全部播完后自动隐藏
gameabc_face.spineMgr.playQueue("monster", ["hit", "die"]);

// 队列播放:入场 → 待机,全部播完后保持显示
gameabc_face.spineMgr.playQueue("hero", ["intro", "idle"], false);

6.3 点击切换动画(攻击→恢复)

gameabc_face.mousedown = function(gameid, spid, downx, downy) {
    // 点击播放攻击(非循环)
    gameabc_face.spineMgr.setAnimation("hero", "attack", false);
    // 攻击完自动切回 idle
    gameabc_face.spineMgr.addAnimation("hero", "idle", true, 0);
};

6.4 角色移动 + 动画联动

var heroState = "idle";

gameabc_face.mousedown = function(gameid, spid, downx, downy) {
    heroState = "run";
    gameabc_face.spineMgr.setAnimation("hero", "run", true);
};

gameabc_face.mouseup = function(gameid, spid_down, downx, downy, spid_up, upx, upy) {
    heroState = "idle";
    gameabc_face.spineMgr.setAnimation("hero", "idle", true);
};

gameabc_face.mousemove = function(gameid, spid, downx, downy, movex, movey, timelong, offmovex, offmovey) {
    // 通过拖拽移动角色
    var mgr = gameabc_face.spineMgr;
    var entry = mgr._entries["hero"];
    if (entry) {
        mgr.setPosition("hero", entry.x + offmovex, entry.y + offmovey);
        // 根据移动方向翻转
        mgr.setFlip("hero", offmovex < 0, false);
    }
};

6.5 多轨道叠加(走路 + 射击)

Spine 支持多轨道同时播放动画。低轨道track 0为基础动画高轨道覆盖部分骨骼

// track 0: 下半身走路
gameabc_face.spineMgr.setAnimation("hero", "walk", true, 0);

// track 1: 上半身射击(在 Spine 编辑器中只设置上半身骨骼的关键帧)
gameabc_face.spineMgr.setAnimation("hero", "shoot", false, 1);

6.6 动态切换皮肤(换装系统)

// 查看有哪些皮肤
var skins = gameabc_face.spineMgr.getSkins("hero");
logmessage("可用皮肤: " + skins.join(", "));

// 切换到战士皮肤
gameabc_face.spineMgr.setSkin("hero", "warrior");

// 切换到法师皮肤
gameabc_face.spineMgr.setSkin("hero", "mage");

6.7 运行时查询动画列表

gameabc_face.spine_onComplete = function(spineId, animName, trackIndex) {
    var anims = gameabc_face.spineMgr.getAnimations(spineId);
    logmessage(spineId + " 拥有的动画: " + anims.join(", "));
};

7. 与 gameabc 精灵系统配合

渲染时序

gameabc 引擎循环 (每帧)
    │
    ├── gamebegindraw()          ← 帧开始
    ├── 遍历 Layer → 每个精灵:
    │       ├── gamemydrawbegin()
    │       ├── 精灵自绘 (图片/文字)
    │       └── gamemydraw()
    ├── gameenddraw()            ← 用户自定义逻辑
    │       └── (自动) spineMgr.updateAndDraw(ctx)  ← ★ 通过 defineProperty 自动追加
    └── 帧结束

Spine 动画在 gameenddraw 末尾自动渲染(通过 Object.defineProperty 拦截实现, 无论开发者如何重新定义 gameenddrawSpine 渲染都不会丢失), 因此会覆盖在所有 gameabc 精灵之上。 开发者在 gameenddraw 中编写的自定义逻辑会先执行Spine 渲染在其后自动执行。

如果需要 Spine 在精灵之下渲染

可以通过手动控制渲染时机来实现。在 gamebegindraw 中手动调用渲染,并禁用自动渲染:

// 方式:在 gamebegindraw 中手动渲染
gameabc_face.gamebegindraw = function(gameid, spid, times, timelong) {
    var ctx = gameabc_face.dc;
    if (ctx) {
        gameabc_face.spineMgr.updateAndDraw(ctx);
    }
    // 标记自动渲染跳过(因为已在此手动渲染)
    gameabc_face.spineMgr._rendered = true;
};

注意:当前自动挂钩在 gameenddraw 末尾,如需精细控制层级, 可将 SpineMgr._inited 临时置 false 跳过自动渲染,手动选择渲染时机。

让 Spine 跟随某个精灵移动

gameabc_face.gamemydraw = function(gameid, spid, times, timelong) {
    // 让 Spine 角色跟随精灵 1 的位置
    if (spid === 1) {
        var sx = get_self(1, 18, 0, 0, 0);  // 获取精灵 1 的 X
        var sy = get_self(1, 19, 0, 0, 0);  // 获取精灵 1 的 Y
        gameabc_face.spineMgr.setPosition("hero", sx, sy);
    }
};

8. 性能优化建议

8.1 图集纹理

  • 纹理尺寸建议不超过 2048×2048
  • 导出时勾选 Power of two 确保尺寸为 2 的幂
  • 不要勾选 Premultiply alphaCanvas 2D 不需要预乘 Alpha

8.2 控制实例数量

  • Canvas 2D 渲染性能有限,建议同屏 Spine 实例不超过 5-8 个
  • 不可见的实例调用 setVisible(id, false),跳过渲染和更新
  • 不再需要的实例调用 remove(id) 释放内存

8.3 减少骨骼复杂度

  • 骨骼数量建议控制在 50 个以内
  • 减少网格变形Mesh Deform对 Canvas 2D 影响较大
  • 使用裁剪Clipping时性能开销大谨慎使用

8.4 动画混合时长

mixDuration 越长,过渡越平滑,但在切换瞬间需要同时计算两个动画。
建议设为 0.1 ~ 0.3 秒


9. 常见问题排查

Q1: 画面上看不到 Spine 动画

检查清单:

  1. 文件路径是否正确?

    • 确认 assets/spine/ 目录下有 .json.atlas.png
    • 文件名大小写必须一致Linux/Mac 服务器区分大小写)
  2. 是否通过 HTTP 访问?

    • file:// 协议无法加载跨域资源,必须使用 HTTP 服务器
  3. 打开浏览器控制台F12看报错

    • 404 错误:文件路径有误
    • JSON 解析错误:.json 文件格式异常
    • spine is not definedspine-canvas.js 未正确加载
  4. 坐标是否在可见范围内?

    • 项目设计尺寸为 1280×720检查 xy 是否在此范围
  5. logmessage 输出是否有 "[SpineMgr] xxx 构建完成"

    • 有 → 加载成功,检查坐标和缩放
    • 有 "构建失败" → 查看具体错误信息
    • 没有 → 资源还在加载中或路径错误

Q2: 动画显示位置不对

  • Spine 编辑器中设置骨骼原点的位置会影响运行时的锚点
  • 调整 setPosition 的坐标,或在 Spine 编辑器中修改根骨骼位置
  • 注意 Spine 的 Y 轴与 Canvas Y 轴方向可能不同

Q3: 动画速度太快或太慢

  • 检查 gameabc_Project 中的 fps 设置(默认 30
  • SpineMgr 内部是用 Date.now() 计算真实时间差的,不依赖帧率
  • 如果需要倍速播放,修改 state.timeScale
    var entry = gameabc_face.spineMgr._entries["hero"];
    entry.state.timeScale = 2.0;  // 2 倍速
    

Q4: 切换动画时有跳帧

  • 增大 mixDuration(加载时的 option 或修改 stateData.defaultMix
  • 使用 addAnimation 排队而不是直接 setAnimation 打断

Q5: 多个 Spine 实例重叠时闪烁

  • 确认没有同一个 id 加载两次
  • 检查 ctx.save() / ctx.restore() 是否配对SpineMgr 内部已处理)

Q6: spine-canvas.js 版本与 Spine 编辑器版本不匹配

  • spine-canvas.js 4.2 需搭配 Spine 编辑器 4.2.x 导出的数据
  • 如果使用 Spine 4.1 编辑器,请下载对应版本的运行时:
    https://unpkg.com/@esotericsoftware/spine-canvas@4.1/dist/iife/spine-canvas.js
    

10. 附录:文件加载顺序

index.html 中的 script 标签加载顺序至关重要:

1. spine-canvas.js      ← 先加载 Spine 运行时 (定义 window.spine)
2. gameabc.min.js        ← 再加载游戏引擎
3. SpineMgr.js           ← Spine 管理器 + defineProperty 自动挂钩渲染
4. gamemain.js           ← 游戏主逻辑(不需修改,直接调用 API 即可)
5. Spine_Event.js        ← Spine 事件回调 (依赖 gameabc_face)
6. Project1_Event.js     ← 精灵事件
7. gameabc_data.min.js   ← 项目配置数据 (引擎初始化)

不能调换顺序SpineMgr.js 必须在 gamemain.js 之前加载, 否则会出现 spine is not definedgameabc_face.spineMgr is undefined 的错误。


附录:完整最小示例 gamemain.js

gamemain.js 不需要任何修改,保持原样即可。在任意事件回调中直接调用 gameabc_face.spineMgr 的 API

// gamemain.js —— 无需修改框架,只需在回调中调用 API

gameabc_face.gamestart = function(gameid) {
    // 直接设置位置并播放动画(自动加载,无需 load
    gameabc_face.spineMgr.setPosition("mj_gangshangkaihua", 640, 500);
    gameabc_face.spineMgr.setAnimation("mj_gangshangkaihua", "animation", true);
};

// gameenddraw 保持原样Spine 渲染由 SpineMgr.js 自动处理
gameabc_face.gameenddraw = function(gameid, spid, times, timelong) {
    // 这里写其他自定义绘制逻辑,或留空
};

gameabc_face.mousedown = function(gameid, spid, downx, downy) {
    // 点击播放动画
    gameabc_face.spineMgr.setAnimation("mj_gangshangkaihua", "animation", false);
};

重要SpineMgr.js 是独立文件,通过 Object.defineProperty 自动拦截 gameenddraw 无论 gamemain.js 如何定义 gameenddrawSpine 渲染都会自动追加在其后执行。 gamemain.js 完全不需要修改。


参考链接