# Spine 动画集成手册 > 本手册针对 **gameabc 引擎** 的 Spine 项目,说明如何在 Canvas 2D 游戏中加载、 > 控制和管理 Spine 骨骼动画。 > 运行时版本:**spine-canvas 4.2** | 引擎:**gameabc** --- ## 目录 1. [项目结构总览](#1-项目结构总览) 2. [快速开始:5 分钟跑通](#2-快速开始5-分钟跑通) 3. [从 Spine 编辑器导出资源](#3-从-spine-编辑器导出资源) 4. [SpineMgr 完整 API 参考](#4-spinemgr-完整-api-参考) 5. [事件系统](#5-事件系统) 6. [常见用法示例](#6-常见用法示例) 7. [与 gameabc 精灵系统配合](#7-与-gameabc-精灵系统配合) 8. [性能优化建议](#8-性能优化建议) 9. [常见问题排查](#9-常见问题排查) 10. [附录:文件加载顺序](#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 事件回调中直接调用即可: ```javascript // 设置位置 + 播放动画,无需先 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.json`、`hero.atlas`、`hero.png`。 > > **可选**:如果资源不在默认路径 `assets/spine/`,可手动调用一次 `init()`: > ```javascript > gameabc_face.spineMgr.init("other/path/"); > ``` ### 第三步:打开 index.html 即可运行 用浏览器打开 `index.html`(需要 HTTP 服务器环境,不能直接 `file://`): ```bash # 简易方法:在项目目录下启动 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 不需要预乘 | 5. 点击 **Export** 按钮 ### 导出后得到的文件 ``` hero.json ← 骨骼数据,包含骨骼、插槽、动画等 hero.atlas ← 图集描述,记录每个区域在纹理中的位置 hero.png ← 图集纹理图片 ``` > **注意**:如果图集很大被拆分成多张(`hero.png`、`hero2.png`),所有 `.png` > 都需要放到 `assets/spine/` 目录中。`.atlas` 文件会自动引用它们。 --- ## 4. SpineMgr 完整 API 参考 `SpineMgr` 挂载在 `gameabc_face.spineMgr` 上,所有调用形如: ```javascript gameabc_face.spineMgr.方法名(参数); ``` --- ### 4.1 init(basePath) *(可选)* **手动设置资源根路径。通常无需调用——`load()` 会自动以默认路径 `"assets/spine/"` 初始化。** 仅在资源放在其他目录时才需要调用: ```javascript 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 等初始参数 ```javascript 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 加载资源并在就绪后播放。** ```javascript // 无需先 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) **将动画添加到播放队列,在当前动画结束后自动播放。** ```javascript // 先播放 attack,attack 完成后自动切换到 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 上的位置。** ```javascript gameabc_face.spineMgr.setPosition("hero", 640, 500); ``` > Spine 的坐标原点在骨骼的根骨骼处。Y 轴向上为正(与 Canvas 的 Y 轴方向相反), > spine-canvas 运行时已做了内部转换。 --- ### 4.6 setScale(id, sx, sy) **设置缩放。** ```javascript 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) **水平/垂直翻转。** ```javascript gameabc_face.spineMgr.setFlip("hero", true, false); // 水平翻转 ``` --- ### 4.8 setVisible(id, visible) **显示或隐藏 Spine 实例。** ```javascript gameabc_face.spineMgr.setVisible("hero", false); // 隐藏 gameabc_face.spineMgr.setVisible("hero", true); // 显示 ``` --- ### 4.9 setSkin(id, skinName) **切换皮肤。** ```javascript gameabc_face.spineMgr.setSkin("hero", "warrior"); ``` > 切换皮肤后会自动重置插槽到 Setup Pose。皮肤名必须在 Spine 编辑器中预定义。 --- ### 4.10 getAnimations(id) **获取该实例所有可用动画名称列表。** ```javascript var anims = gameabc_face.spineMgr.getAnimations("hero"); // 返回: ["idle", "walk", "run", "attack", "die"] logmessage("动画列表: " + anims.join(", ")); ``` --- ### 4.11 getSkins(id) **获取该实例所有可用皮肤名称列表。** ```javascript var skins = gameabc_face.spineMgr.getSkins("hero"); // 返回: ["default", "warrior", "mage"] ``` --- ### 4.12 playOnce(id, animName, track) **播放一次动画后自动隐藏。** 自动显示实例、播放指定动画(不循环),动画完成后自动设置 `visible = false`。 ```javascript // 播放一次攻击动画,播完自动隐藏 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) **按顺序播放一组动画(队列),全部播完后可选择隐藏或保持显示。** 自动显示实例,队列中每个动画均播放一次(不循环),依次播放。 ```javascript // 播放 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 实例,释放内存。** ```javascript gameabc_face.spineMgr.remove("hero"); ``` --- ### 4.15 removeAll() **销毁所有 Spine 实例。** ```javascript gameabc_face.spineMgr.removeAll(); ``` ### 4.16 spine_onComplete(spineId, animName, trackIndex) *(事件回调)* **动画完成回调。每次动画播放一轮结束时触发。** 在 `js/Spine_Event.js` 中定义。 ```javascript // 在 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` 中定义。 ```javascript // 在 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.16](#416-spine_oncompletespineid-animname-trackindex--事件回调) 和 [4.17](#417-spine_oneventspineid-eventname-intvalue-floatvalue-stringvalue--事件回调)。 ### 5.1 动画完成事件 spine_onComplete **每次动画循环播放一轮结束时触发。** ```javascript // 在 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 关键帧时触发。** ```javascript // 在 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** 面板中创建并命名事件(如 `hit`、`footstep`) 4. 可为事件设置 int / float / string 参数 5. 导出 JSON 后,运行时会自动解析并触发回调 --- ## 6. 常见用法示例 ### 6.1 加载多个角色 ```javascript // 在 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 播放一次后隐藏 / 队列播放 ```javascript // 播放一次攻击动画,完成后自动隐藏 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 点击切换动画(攻击→恢复) ```javascript 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 角色移动 + 动画联动 ```javascript 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)为基础动画,高轨道覆盖部分骨骼: ```javascript // track 0: 下半身走路 gameabc_face.spineMgr.setAnimation("hero", "walk", true, 0); // track 1: 上半身射击(在 Spine 编辑器中只设置上半身骨骼的关键帧) gameabc_face.spineMgr.setAnimation("hero", "shoot", false, 1); ``` ### 6.6 动态切换皮肤(换装系统) ```javascript // 查看有哪些皮肤 var skins = gameabc_face.spineMgr.getSkins("hero"); logmessage("可用皮肤: " + skins.join(", ")); // 切换到战士皮肤 gameabc_face.spineMgr.setSkin("hero", "warrior"); // 切换到法师皮肤 gameabc_face.spineMgr.setSkin("hero", "mage"); ``` ### 6.7 运行时查询动画列表 ```javascript 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` 拦截实现, 无论开发者如何重新定义 `gameenddraw`,Spine 渲染都不会丢失), 因此会**覆盖在所有 gameabc 精灵之上**。 开发者在 `gameenddraw` 中编写的自定义逻辑会先执行,Spine 渲染在其后自动执行。 ### 如果需要 Spine 在精灵之下渲染 可以通过手动控制渲染时机来实现。在 `gamebegindraw` 中手动调用渲染,并禁用自动渲染: ```javascript // 方式:在 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 跟随某个精灵移动 ```javascript 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 alpha**(Canvas 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 defined`:`spine-canvas.js` 未正确加载 4. **坐标是否在可见范围内?** - 项目设计尺寸为 1280×720,检查 `x` 和 `y` 是否在此范围 5. **logmessage 输出是否有 "[SpineMgr] xxx 构建完成"?** - 有 → 加载成功,检查坐标和缩放 - 有 "构建失败" → 查看具体错误信息 - 没有 → 资源还在加载中或路径错误 ### Q2: 动画显示位置不对 - Spine 编辑器中设置骨骼原点的位置会影响运行时的锚点 - 调整 `setPosition` 的坐标,或在 Spine 编辑器中修改根骨骼位置 - 注意 Spine 的 Y 轴与 Canvas Y 轴方向可能不同 ### Q3: 动画速度太快或太慢 - 检查 `gameabc_Project` 中的 `fps` 设置(默认 30) - SpineMgr 内部是用 `Date.now()` 计算真实时间差的,不依赖帧率 - 如果需要倍速播放,修改 `state.timeScale`: ```javascript 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 defined` 或 `gameabc_face.spineMgr is undefined` 的错误。 --- ## 附录:完整最小示例 gamemain.js `gamemain.js` **不需要任何修改**,保持原样即可。在任意事件回调中直接调用 `gameabc_face.spineMgr` 的 API: ```javascript // 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` 如何定义 `gameenddraw`,Spine 渲染都会自动追加在其后执行。 > `gamemain.js` 完全不需要修改。 --- > **参考链接** > - Spine 官方运行时文档: https://zh.esotericsoftware.com/spine-api-reference > - Spine Player 在线演示: https://jp.esotericsoftware.com/spine-player > - spine-canvas npm 包: https://www.npmjs.com/package/@esotericsoftware/spine-canvas