初始化仓库:友乐/gameabc 房卡游戏平台脚手架(client + server + docs)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
# 03 · 事件 · 动画 · 音频 · Spine
|
||||
|
||||
本篇讲**表现系统**:模块间怎么用事件解耦、动画怎么发起与配置、音效/语音怎么分层、Spine 骨骼动画怎么播。四套系统的共同主线是——**框架提供中立能力,子游戏只填玩法专属的配置**。
|
||||
|
||||
---
|
||||
|
||||
## 1. EventBus:模块间事件总线
|
||||
|
||||
`EventBus` 是发布-订阅总线,用于解耦模块(控制器改完数据 `emit`,视图 `on` 后刷新),避免直接相互调用。
|
||||
|
||||
### 命名与中立原则
|
||||
|
||||
- 事件名两段式 `"模块:动作"`,统一定义在 `EventBus.Events` 常量里,**不硬编码字符串**。
|
||||
- **框架不预置任何事件常量**:`gameabc-framework/system/EventBus.js` 只提供发布-订阅机制与一个**空容器** `EventBus.Events = {}`,不内置哪怕通用语义的事件名。
|
||||
- **全部事件由子游戏集中定义**:在子游戏侧的事件常量文件把本子游戏用到的**全部**事件(通用语义 + 玩法专属)注册到 `EventBus.Events`,业务侧统一 `EventBus.Events.XXX` 引用。
|
||||
|
||||
```js
|
||||
// 子游戏事件常量文件 —— 在 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.PLAYER_DISCARDED = 'player:discarded';
|
||||
E.SCENE_CHANGED = 'ui:sceneChanged';
|
||||
// 玩法专属事件
|
||||
E.TILES_DEALT = 'mahjong:tilesDealt';
|
||||
E.MELD_FORMED = 'mahjong:meldFormed';
|
||||
// ...本子游戏用到的全部事件
|
||||
})();
|
||||
```
|
||||
|
||||
> 这正是本仓库的落点:框架 `EventBus` 不预置任何事件常量、保持纯粹中立,所有事件(含通用语义)都由子游戏在其事件常量文件定义——既保证框架可被任意玩法复用,也避免「引用了未定义常量」导致的 `emit(undefined)`(见 05「框架中立」)。
|
||||
|
||||
### 用法
|
||||
|
||||
```js
|
||||
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) 先更新数据模型(不动 UI)
|
||||
2) 播放动画(纯视觉过渡,不改数据)
|
||||
3) 动画完成回调里刷新静态界面
|
||||
```
|
||||
|
||||
好处:动画是“锦上添花”,**即使不播动画,静态界面也始终正确**(重画函数据本地数据即可还原,见 04 重连)。**动画期间绝不修改游戏数据。**
|
||||
|
||||
### AnimationManager API(框架,通用)
|
||||
|
||||
```js
|
||||
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`。
|
||||
|
||||
### 动画参数集中配置(子游戏)
|
||||
|
||||
**所有动画时长/帧区间/帧间隔/循环模式集中在子游戏的动画配置文件**,做到“配置即改、不改代码”。业务代码从配置读取,**禁止硬编码时长/帧数**。
|
||||
|
||||
```js
|
||||
// 动画配置(子游戏):一个动作一份配置
|
||||
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**。
|
||||
|
||||
```js
|
||||
// 子游戏音频管理:按概念播放,内部映射到资源键并按座位性别选男/女语音
|
||||
// 通用音效直接走框架:AudioManager.playSound(音效资源常量.某音效)
|
||||
```
|
||||
|
||||
**声音文件手动准备**:音频文件由开发者**手动**按平台命名规范放入资源目录(文件名/存放目录/ID→文件名转换规则以《[友乐…规范](../../../docs/important/client/友乐游戏引擎精灵与资源管理接口规范.md)》为准),代码只按 ID 引用、不生成文件。因此**音效资源常量每个 ID 必须注明【类型(背景音乐 / 音效 / 语音)+ 用途 + 时长 + 播放方式(循环 / 单次)】**,让人据注释就能准确准备对应音频。
|
||||
|
||||
**DO**:新音效只改音效资源常量;业务只调游戏音频管理。**DON'T**:在游戏音频管理里写裸文件名/ID;硬编码性别映射。
|
||||
|
||||
---
|
||||
|
||||
## 4. Spine:骨骼动画
|
||||
|
||||
Spine 用于复杂特效动画(吃碰杠胡的炫光等),集成进 gameabc 绘制循环,零侵入主循环。
|
||||
|
||||
### 三层
|
||||
|
||||
| 层 | 归属 | 职责 |
|
||||
|----|------|------|
|
||||
| 框架 | `spine/SpineMgr.js` | 加载/播放/每帧更新绘制、回调转发 |
|
||||
| 子游戏配置 | Spine 动作配置 | 概念→Spine 资源(文件 + 动画名)映射 |
|
||||
| 子游戏分发 | Spine 回调分发 | 集中接收 Spine 回调并按模块分发 |
|
||||
|
||||
### 用法
|
||||
|
||||
```js
|
||||
// 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-网络对接与启动编排](./04-网络对接与启动编排.md) 讲:怎么收发包、新旧架构如何对接、一局怎么启动。
|
||||
</content>
|
||||
Reference in New Issue
Block a user