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

807 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
// 先播放 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 上的位置。**
```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