diff --git a/docs/client/development-guide/02-渲染与UI组件体系.md b/docs/client/development-guide/02-渲染与UI组件体系.md index 1427bf5..6ee9a71 100644 --- a/docs/client/development-guide/02-渲染与UI组件体系.md +++ b/docs/client/development-guide/02-渲染与UI组件体系.md @@ -93,7 +93,7 @@ ID 必须与编辑器中实际存在的精灵**完全一致**。超范围 → `S - **精灵结构常量**:每个精灵注明【精灵类型(图片精灵 / 文字精灵)+ 功能 + 关联的图片资源 ID + 帧数说明】。 - **图片资源常量**:每个资源注明【帧数(单帧 / N 帧,且每帧含义)+ 用途 + 尺寸】。 -(声音、Spine 资源的注释要求见 03。资源 ID 范围与注释格式细则以《[友乐游戏引擎精灵与资源管理接口规范](../../../docs/important/client/友乐游戏引擎精灵与资源管理接口规范.md)》为准。) +(声音、Spine 资源的注释要求见 03。资源 ID 范围与注释格式细则以《友乐游戏引擎精灵与资源管理接口规范》为准。) ### 三者如何配合(新增一块 UI 的流程) @@ -169,6 +169,40 @@ this.addEventListener(EventBus.Events.GAME_STARTED, fn); `BaseComponent.addEventListener` 会包装处理函数(绑定 `this`、捕获异常、销毁后忽略事件),并记录下来在 `destroy()` 时统一 `EventBus.off`。**销毁组件用 `destroy()` 而非只 `hide()`** —— 只有 `destroy()` 才清理监听。 +### 组件数据自持 + set-refresh 范式(核心) + +**前端只负责界面展示与玩家交互;所有核心运算与胜负裁定以服务端为准、数据以服务端为权威**(见 01/05)。组件本地保存的只是「用于画界面的数据副本」,据此渲染,不做权威计算。为此每个组件遵循统一范式: + +1. **数据集中在 `this.data`**:组件(及其下每个「零件 UI」)的自有数据全部收拢到 `this.data.*`,**不散落**在实例其他字段或全局;界面能否重建只取决于 `this.data`。 + +2. **每个 UI 都有 `setXxx` / `refreshXxx` 成对方法**:组件本身、以及组件内每个「零件 UI」(如玩家信息条、分数、手牌区、按钮组)都提供一对—— + - `setXxx(...)`:**只写数据**到 `this.data`,不碰精灵; + - `refreshXxx()`:**只据 `this.data` 画界面**,不改数据。 + 两者职责单一、互不越界。 + +3. **组件有一个总 `refresh()`**:顺序调用其下所有零件的 `refreshXxx()`,把整块界面据 `this.data` 完整重画。**任何时候调用 `refresh()` 都能无歧义重建正确界面**(断线重连、切 app 回来、网页刷新都复用它,不另写一套渲染)。 + +```js +var GameView = Object.create(BaseComponent); +GameView.init = function (config) { + this.data = { players: [], score: 0 /* ...自有数据只放这里 */ }; + // ... +}; + +// 零件 UI:分数。set 只写数据,refresh 只据数据画 +GameView.setScore = function (v) { this.data.score = v; }; +GameView.refreshScore = function () { SpriteManager.setText(SCORE_ID, this.data.score); }; + +// 总 refresh:据 this.data 重画所有零件 +GameView.refresh = function () { + this.refreshPlayers(); + this.refreshScore(); + // ...其余零件 +}; +``` + +**收包的正确流程**:先 `setXxx` 把数据写进 `this.data`(必要时 `refresh` 刷新静态界面),**再**做动画/表现。动画只是体验层,其「开始/结束/出错」等生命周期回调里**只刷新界面、绝不设置核心数据**。这样即使动画卡住/播错、或开发前期还没做动画,数据、逻辑与界面依然正确、互不影响。 + --- ## 4. UIManager:注册与场景切换 @@ -295,6 +329,7 @@ list.onClick = function (type, rowIndex, rowData) { /* ... */ }; | 检查 `SpriteManager` 返回值 | 忽略 `false` 返回 | | 精灵/资源/坐标全进常量文件 | 在业务代码内联裸数字 | | 组件继承 `BaseComponent`,事件用 `addEventListener` | 自建组件类 / 直接 `EventBus.on` | +| 自有数据集中 `this.data`;每个 UI/零件有 set/refresh 对;总 `refresh` 可据数据重建界面 | 数据散落各处 / `set` 里画界面 / `refresh` 里改数据 / 动画回调写核心数据 | | 销毁用 `destroy()`,`onDestroy` 清动态精灵与定时器 | 只 `hide()` 就当销毁 | | 动态列表用 `DynamicSpriteList` 并 `destroy` | 手搓 SpriteCopy 又忘清理 | diff --git a/docs/client/development-guide/03-事件·动画·音频·Spine.md b/docs/client/development-guide/03-事件·动画·音频·Spine.md index 2ebc922..7ce876b 100644 --- a/docs/client/development-guide/03-事件·动画·音频·Spine.md +++ b/docs/client/development-guide/03-事件·动画·音频·Spine.md @@ -55,12 +55,12 @@ EventBus.once(EventBus.Events.GAME_FINISHED, fn); // 一次性 收到服务端推送后的固定节奏: ``` -1) 先更新数据模型(不动 UI) +1) 先更新数据模型(setXxx 写入组件 this.data,不动 UI) 2) 播放动画(纯视觉过渡,不改数据) -3) 动画完成回调里刷新静态界面 +3) 动画完成回调里只刷新静态界面(refresh) ``` -好处:动画是“锦上添花”,**即使不播动画,静态界面也始终正确**(重画函数据本地数据即可还原,见 04 重连)。**动画期间绝不修改游戏数据。** +好处:动画是“锦上添花”,**即使不播动画,静态界面也始终正确**(据组件 `this.data` 调 `refresh` 即可还原,见 02 组件范式 / 04 重连)。**动画的开始/结束/出错等生命周期回调里只刷新界面、绝不设置核心数据**;动画期间**绝不修改游戏数据**。这样动画卡住/播错、或开发前期没有动画时,数据、逻辑与界面依然正确、互不影响。 ### AnimationManager API(框架,通用) @@ -124,7 +124,7 @@ AnimationManager.frame(spriteId, opt.startFrame, opt.endFrame, opt.duration, opt // 通用音效直接走框架:AudioManager.playSound(音效资源常量.某音效) ``` -**声音文件手动准备**:音频文件由开发者**手动**按平台命名规范放入资源目录(文件名/存放目录/ID→文件名转换规则以《[友乐…规范](../../../docs/important/client/友乐游戏引擎精灵与资源管理接口规范.md)》为准),代码只按 ID 引用、不生成文件。因此**音效资源常量每个 ID 必须注明【类型(背景音乐 / 音效 / 语音)+ 用途 + 时长 + 播放方式(循环 / 单次)】**,让人据注释就能准确准备对应音频。 +**声音文件手动准备**:音频文件由开发者**手动**按平台命名规范放入资源目录(文件名/存放目录/ID→文件名转换规则以《友乐游戏引擎精灵与资源管理接口规范》为准),代码只按 ID 引用、不生成文件。因此**音效资源常量每个 ID 必须注明【类型(背景音乐 / 音效 / 语音)+ 用途 + 时长 + 播放方式(循环 / 单次)】**,让人据注释就能准确准备对应音频。 **DO**:新音效只改音效资源常量;业务只调游戏音频管理。**DON'T**:在游戏音频管理里写裸文件名/ID;硬编码性别映射。