8.7 KiB
03 · 事件 · 动画 · 音频 · Spine
本篇讲表现系统:模块间怎么用事件解耦、动画怎么发起与配置、音效/语音怎么分层、Spine 骨骼动画怎么播。四套系统的共同主线是——框架提供中立能力,子游戏只填玩法专属的配置。
1. EventBus:模块间事件总线
EventBus 是发布-订阅总线,用于解耦模块(控制器改完数据 emit,视图 on 后刷新),避免直接相互调用。
命名与中立原则
- 事件名两段式
"模块:动作",统一定义在EventBus.Events常量里,不硬编码字符串。 - 框架不预置任何事件常量:
gameabc-framework/system/EventBus.js只提供发布-订阅机制与一个空容器EventBus.Events = {},不内置哪怕通用语义的事件名。 - 全部事件由子游戏集中定义:在子游戏侧的事件常量文件把本子游戏用到的全部事件(通用语义 + 玩法专属)注册到
EventBus.Events,业务侧统一EventBus.Events.XXX引用。
// 子游戏事件常量文件 —— 在 EventBus 之后、使用者之前加载
(function () {
if (typeof EventBus === 'undefined' || !EventBus.Events) {
throw new Error('[GameEvents] EventBus 未加载;请检查 index.html 加载顺序');
}
var E = EventBus.Events;
E.GAME_STARTED = 'game:started'; // 通用语义事件
E.TILES_DEALT = 'mahjong:tilesDealt'; // 玩法专属事件
// ...本子游戏用到的全部事件
})();
框架
EventBus不预置任何事件常量、保持纯粹中立,避免「引用了未定义常量」导致的emit(undefined)(见 05「框架中立」)。
用法
EventBus.emit(EventBus.Events.GAME_STARTED, { playerCount: 4 }); // 发布
this.addEventListener(EventBus.Events.GAME_STARTED, function (d) { // 订阅(组件内)
self._onStart(d);
});
EventBus.once(EventBus.Events.GAME_FINISHED, fn); // 一次性
组件内务必用 BaseComponent.addEventListener 订阅(destroy 自动清理,见 02),不要裸用 EventBus.on。
2. 动画:AnimationManager + 配置
数据优先、表现延后(核心节奏)
收到服务端推送后的固定节奏:
1) 先更新数据模型(setXxx 写入组件 this.data,不动 UI)
2) 播放动画(纯视觉过渡,不改数据)
3) 动画完成回调里只刷新静态界面(refresh)
即使不播动画,静态界面也始终正确(据组件 this.data 调 refresh 即可还原,见 02 组件范式 / 04 重连)。动画的开始/结束/出错等生命周期回调里只刷新界面、绝不设置核心数据;动画期间绝不修改游戏数据。
AnimationManager API(框架,通用)
AnimationManager.move(id, fromX, fromY, toX, toY, duration, 'easeOut', cb);
AnimationManager.scale(id, 1.0, 1.3, 150, 'easeOut', cb);
AnimationManager.rotate(id, 0, 360, 300, 'linear', cb);
AnimationManager.fade(id, 1.0, 0.0, 200, 'linear', cb);
AnimationManager.frame(id, startFrame, endFrame, duration, { loop:false, callback:cb });
// 串/并行队列
var q = AnimationManager.createQueue('serial'); // 或 'parallel'
AnimationManager.addToQueue(q, { type:'move', spriteId:id, /* ... */ });
AnimationManager.playQueue(q, cb);
缓动可选:linear/easeIn/easeOut(最常用)/easeInOut/bounce/elastic。
动画参数集中配置(子游戏)
所有动画时长/帧区间/帧间隔/循环模式集中在子游戏的动画配置文件,做到“配置即改、不改代码”。业务代码从配置读取,禁止硬编码时长/帧数。
// 动画配置(子游戏):一个动作一份配置
SOME_ACTION: { PLAY_TYPE:'frame', RESOURCE_ID:554, START_FRAME:1, END_FRAME:35,
FRAME_INTERVAL:50, LOOP:false, HIDE_ON_COMPLETE:true }
// 使用:用工具方法从配置自动算帧数/时长,再交给框架播放
AnimationManager.frame(spriteId, opt.startFrame, opt.endFrame, opt.duration, opt);
框架动画 vs 游戏动画分层
AnimationManager(框架):通用的 move/scale/fade/frame/队列,不懂具体玩法元素。- 游戏动画封装(子游戏):玩法专属封装——算各元素坐标、组织分段动画、从动画配置取参数。
业务调游戏动画封装的语义化方法,由它再去调 AnimationManager。
3. 音频:AudioManager + 资源 + 游戏音频管理
三层分离,框架不含任何玩法专属的音效 ID 映射:
业务代码
└─ 游戏音频管理(子游戏:概念→键名,如 'peng'/牌值→资源键)
├─ 音效资源常量(子游戏:键名→文件 ID)
└─ AudioManager(框架:通用播放、按性别选语音)
└─ Utl(引擎播放)
各层职责
AudioManager(框架):playSound(file)、playVoice(baseId, sex)、playVoiceBySeat(seat, baseId)、playMusic/stopMusic。约定女音 ID = 男音 ID + 偏移;不含任何牌值/动作映射。- 音效资源常量(子游戏):集中定义音效/语音文件 ID(音效、男/女语音等分类)。新增音效只改这里。
- 游戏音频管理(子游戏):把“出牌/碰/某张牌”等概念映射到音效资源键,再调
AudioManager。不写文件名/数字 ID。通用音效直接走框架AudioManager.playSound(音效资源常量.某音效)。
声音文件手动准备:音频文件由开发者手动按平台命名规范放入资源目录(文件名/存放目录/ID→文件名转换规则以平台的资源管理接口规范为准),代码只按 ID 引用、不生成文件。因此音效资源常量每个 ID 必须注明【类型(背景音乐 / 音效 / 语音)+ 用途 + 时长 + 播放方式(循环 / 单次)】,让人据注释就能准确准备对应音频。
DO:新音效只改音效资源常量;业务只调游戏音频管理。DON'T:在游戏音频管理里写裸文件名/ID;硬编码性别映射。
4. Spine:骨骼动画
Spine 用于复杂特效动画(吃碰杠胡的炫光等),集成进 gameabc 绘制循环,零侵入主循环。
三层
| 层 | 归属 | 职责 |
|---|---|---|
| 框架 | spine/SpineMgr.js |
加载/播放/每帧更新绘制、回调转发 |
| 子游戏配置 | Spine 动作配置 | 概念→Spine 资源(文件 + 动画名)映射 |
| 子游戏分发 | Spine 回调分发 | 集中接收 Spine 回调并按模块分发 |
用法
// Spine 动作配置(子游戏):概念 → 资源
'chi': { spineId:'chi', animName:'play', scale:1.0 },
'tianhu': { spineId:'tianhu', animName:'play', scale:1.0 },
// 播放(SpineMgr)
SpineMgr.load(id, jsonFile, atlasFile, { scale:1.0, animation:'idle', loop:true });
SpineMgr.setAnimation(id, 'play', false, 0);
SpineMgr.playOnce(id, 'play', 0); // 播一次后隐藏
SpineMgr.playQueue(id, ['p1','p2'], true);
SpineMgr.updateAndDraw(ctx); // 每帧(引擎循环内自动)
回调集中分发
SpineMgr 把动画完成/帧事件转发到全局 gameabc_face.spine_onComplete/spine_onEvent,由子游戏的 Spine 回调分发器统一分发到各模块,不要在业务里散接回调。
资源手动制作与变更流程
Spine 骨骼资源(json / atlas / 贴图)由开发者手动制作并放入资源目录,代码只按概念引用、不生成资源。改了 Spine 动作配置的资源映射后,运行 Spine 数据构建脚本重新生成 Spine 数据清单(generated/spine_*)。
Spine 动作配置每个动作必须注明【概念(对应哪个玩法动作)+ 资源文件 + 动画名 + scale 等参数】,让人据注释就能准确准备对应 Spine 资源。
DO:概念走 Spine 动作配置、回调走 Spine 回调分发器、改资源后跑构建脚本。DON'T:硬编码 spineId/animName;绕过分发器直接接回调。
5. 本篇 DO / DON'T 总表
| 系统 | DO ✅ | DON'T ❌ |
|---|---|---|
| 事件 | 名字进 EventBus.Events;专属事件放子游戏事件常量文件 |
硬编码事件字符串;往框架塞玩法事件 |
| 动画 | 先改数据→播动画→回调刷 UI;参数取自动画配置 | 动画期改数据;硬编码时长/帧数 |
| 音频 | 新音效只改音效资源常量;业务只调游戏音频管理 | 游戏音频管理写裸 ID;框架含玩法映射 |
| Spine | 概念走 Spine 动作配置,回调走 Spine 回调分发器,改资源跑脚本 | 硬编码 spineId/animName;散接回调 |
下一篇 04-网络对接与启动编排 讲:怎么收发包、新旧架构如何对接、一局怎么启动。