Files
erqiwang_youle/docs/client/development-guide/03-事件·动画·音频·Spine.md
T
2026-07-06 17:16:05 +08:00

8.7 KiB
Raw Blame History

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-网络对接与启动编排 讲:怎么收发包、新旧架构如何对接、一局怎么启动。