降耗②·试点:精简 client 02(规范零丢失,−15%)
保守精简:31 条规范、三张参考表(API/ID范围/生命周期)、set-refresh 核心范式全部保留;只压缩冗长代码示例、把 ✅/❌ 代码块转文字、删两处 演进历史/范例背景注。10,631 → 8,967 字符(−1,664,~15%)。 作为其余 12 篇编号正文精简的尺度基准。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,49 +11,34 @@
|
|||||||
gameabc 的画面由**精灵(Sprite)**组成,按**图层(Layer)**、**群组(Group)**组织。操作精灵走一条严格分层链:
|
gameabc 的画面由**精灵(Sprite)**组成,按**图层(Layer)**、**群组(Group)**组织。操作精灵走一条严格分层链:
|
||||||
|
|
||||||
```
|
```
|
||||||
UI 组件
|
UI 组件 → SpriteManager(业务级 API:ID 范围校验 + 单位换算)
|
||||||
└─ SpriteManager 业务级 API:ID 范围校验 + 单位换算
|
→ GameABCUtils(唯一直接调引擎原生 API 的模块) → gameabc.min.js(引擎)
|
||||||
└─ GameABCUtils 唯一直接调引擎原生 API 的模块(底层原子操作)
|
|
||||||
└─ gameabc.min.js 引擎
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**铁律:UI 代码只调 `SpriteManager`,禁止直接调 `GameABCUtils` 或引擎原生 API**(`set_self`/`get_self` 等)。分层的意义:ID 校验、单位换算、引擎版本隔离都集中在边界,业务层无感。
|
**铁律:UI 代码只调 `SpriteManager`,禁止直接调 `GameABCUtils` 或引擎原生 API**(`set_self`/`get_self` 等)。ID 校验、单位换算、引擎版本隔离都集中在这层边界,业务层无感。
|
||||||
|
|
||||||
### SpriteManager 常用 API
|
### SpriteManager 常用 API
|
||||||
|
|
||||||
| API | 作用 | 说明 |
|
| API | 作用 | 说明 |
|
||||||
|-----|------|------|
|
|-----|------|------|
|
||||||
| `show(id)` / `hide(id)` | 显示/隐藏精灵 | 返回 `boolean`,**失败要查返回值** |
|
| `show(id)` / `hide(id)` | 显示/隐藏 | 返回 `boolean`,**失败要查返回值** |
|
||||||
| `setFrame(id, frame)` | 切换多帧图片的帧 | 一个精灵多帧,靠切帧表现不同牌面,**胜过为每张牌建一个精灵** |
|
| `setFrame(id, frame)` | 切换多帧图片的帧 | 一精灵多帧、靠切帧表现不同牌面,**胜过为每张牌建一个精灵** |
|
||||||
| `setPosition(id, x, y)` | 设置坐标 | |
|
| `setPosition(id, x, y)` | 设置坐标 | |
|
||||||
| `setScale(id, scale)` | 缩放 | 业务用倍数(`1.2`),框架自动转引擎百分比 |
|
| `setScale(id, scale)` | 缩放 | 业务用倍数(`1.2`),框架自动转引擎百分比 |
|
||||||
| `setOpacity(id, o)` | 透明度 | 业务用 `0.0–1.0`,框架自动转 `0–255` |
|
| `setOpacity(id, o)` | 透明度 | 业务用 `0.0–1.0`,框架自动转 `0–255` |
|
||||||
| `setText(id, text)` / `setTextWithWidth(...)` | 文字 | 文字精灵 |
|
| `setText(id, text)` / `setTextWithWidth(...)` | 文字 | 文字精灵 |
|
||||||
| `showGroup(gid)` / `hideGroup(gid)` | 群组批量 | 整块 UI 显隐 |
|
| `showGroup/hideGroup(gid)` | 群组批量 | 整块 UI 显隐 |
|
||||||
| `showLayer(lid)` / `hideLayer(lid)` | 图层批量 | 整个界面显隐 |
|
| `showLayer/hideLayer(lid)` | 图层批量 | 整个界面显隐 |
|
||||||
| `exists(id)` | 存在性检查 | |
|
| `exists(id)` | 存在性检查 | |
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// 显示手牌:一个精灵切帧表现不同牌面
|
// 一精灵切帧表现不同牌面;刷新弃牌区先全 hide 再按数据 show + setFrame(帧号 = code-1)
|
||||||
for (var i = 0; i < handCards.length; i++) {
|
SpriteManager.show(sid); SpriteManager.setFrame(sid, card.code - 1);
|
||||||
var sid = handSpriteIds[i];
|
|
||||||
SpriteManager.show(sid);
|
|
||||||
SpriteManager.setFrame(sid, handCards[i].code - 1); // 帧号 = code-1
|
|
||||||
}
|
|
||||||
// 刷新弃牌区:先全隐藏,再按数据显示
|
|
||||||
for (var k = 0; k < maxDiscards; k++) { SpriteManager.hide(discardIds[k]); }
|
|
||||||
for (var j = 0; j < discards.length; j++) {
|
|
||||||
SpriteManager.show(discardIds[j]);
|
|
||||||
SpriteManager.setFrame(discardIds[j], discards[j].code - 1);
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 精灵 ID 二元性
|
### 精灵 ID 二元性
|
||||||
|
|
||||||
精灵 ID 有两种,`SpriteManager` 透明支持,开发者无需区分:
|
精灵 ID 有两种,`SpriteManager` 透明支持、无需区分:**数字 ID**(编辑器预置,如 `1121`)与**字符串 ID**(运行时由 `SpriteCopyUtils` 动态复制,格式 `"容器IDadd标签"`,如 `"2836add0"`)。
|
||||||
|
|
||||||
- **数字 ID**:编辑器里预置的精灵(如 `1121`)。
|
|
||||||
- **字符串 ID**:运行时由 `SpriteCopyUtils` 动态复制出的精灵,格式 `"容器IDadd标签"`(如 `"2836add0"`)。
|
|
||||||
|
|
||||||
### ID 范围(必须遵守)
|
### ID 范围(必须遵守)
|
||||||
|
|
||||||
@@ -66,15 +51,15 @@ for (var j = 0; j < discards.length; j++) {
|
|||||||
| 声音 | **≥ 101** | 1–100 |
|
| 声音 | **≥ 101** | 1–100 |
|
||||||
| 图层 | **101–200、301–400、501–600、701+** | 1–100、201–300、401–500、601–700 |
|
| 图层 | **101–200、301–400、501–600、701+** | 1–100、201–300、401–500、601–700 |
|
||||||
|
|
||||||
图层按 100 为段与框架交替分配:子游戏用 `101–200`(常规界面)、`301–400`(弹窗,示例用 302)、`501–600`、`701+`;其余段为框架保留,**不可占用**。
|
图层按 100 为段与框架交替分配:子游戏用 `101–200`(常规界面)、`301–400`(弹窗,示例用 302)、`501–600`、`701+`;其余段框架保留,**不可占用**。
|
||||||
|
|
||||||
ID 必须与编辑器中实际存在的精灵**完全一致**。超范围 → `SpriteManager` 校验失败返回 `false`;编辑器里不存在的 ID → 操作静默无效。**绝不随意编造 ID。**
|
ID 必须与编辑器中实际存在的精灵**完全一致**。超范围 → 校验失败返回 `false`;编辑器里不存在的 ID → 操作静默无效。**绝不随意编造 ID。**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 资源与布局常量三件套
|
## 2. 资源与布局常量三件套
|
||||||
|
|
||||||
界面所需的“数据”全部集中在常量文件里,业务代码只引用、不内联裸值。三类常量各司其职:
|
界面所需的“数据”全部集中在常量文件里,业务代码只引用、不内联裸值。三类常量各司其职,外加一个**整合入口**统一为一份精灵常量并**最后加载**:
|
||||||
|
|
||||||
| 常量类别 | 管什么 | 一句话 |
|
| 常量类别 | 管什么 | 一句话 |
|
||||||
|----------|--------|--------|
|
|----------|--------|--------|
|
||||||
@@ -82,13 +67,9 @@ ID 必须与编辑器中实际存在的精灵**完全一致**。超范围 → `S
|
|||||||
| 图片资源常量 | **图片资源 ID** | 每个精灵用哪张图、几帧、每帧什么、尺寸 |
|
| 图片资源常量 | **图片资源 ID** | 每个精灵用哪张图、几帧、每帧什么、尺寸 |
|
||||||
| 布局常量 | **坐标/尺寸/偏移** | 纯数据,精灵摆在哪、多大、怎么排 |
|
| 布局常量 | **坐标/尺寸/偏移** | 纯数据,精灵摆在哪、多大、怎么排 |
|
||||||
|
|
||||||
外加一个**整合入口**把三类统一为一份精灵常量,并**最后加载**。
|
### 资源手动创建,常量靠注释指路
|
||||||
|
|
||||||
### 资源与精灵手动创建,常量靠注释指路
|
**精灵、图片、声音、Spine 资源全部由开发者手动创建**——精灵在编辑器里逐个建好,图片/音频/Spine 文件按平台规范放入资源目录;**代码不创建任何资源,只按常量 ID 引用**,ID 必须与编辑器/资源目录里实际存在的完全一致(见 §1,绝不编造)。因此**常量注释必须写准,让人据注释就能准确创建对应资源**:
|
||||||
|
|
||||||
**精灵、图片、声音、Spine 资源全部由开发者手动创建**——精灵在游戏编辑器里逐个建好,图片/音频/Spine 文件按平台规范放入资源目录;**代码不创建任何资源,只按常量 ID 引用**,ID 必须与编辑器/资源目录里实际存在的**完全一致**(见 §1,绝不编造)。
|
|
||||||
|
|
||||||
正因资源是"先手动建、再按 ID 引用",**常量定义必须写准注释,让人据注释就能准确创建出对应资源**:
|
|
||||||
|
|
||||||
- **精灵结构常量**:每个精灵注明【精灵类型(图片精灵 / 文字精灵)+ 功能 + 关联的图片资源 ID + 帧数说明】。
|
- **精灵结构常量**:每个精灵注明【精灵类型(图片精灵 / 文字精灵)+ 功能 + 关联的图片资源 ID + 帧数说明】。
|
||||||
- **图片资源常量**:每个资源注明【帧数(单帧 / N 帧,且每帧含义)+ 用途 + 尺寸】。
|
- **图片资源常量**:每个资源注明【帧数(单帧 / N 帧,且每帧含义)+ 用途 + 尺寸】。
|
||||||
@@ -97,22 +78,16 @@ ID 必须与编辑器中实际存在的精灵**完全一致**。超范围 → `S
|
|||||||
|
|
||||||
### 三者如何配合(新增一块 UI 的流程)
|
### 三者如何配合(新增一块 UI 的流程)
|
||||||
|
|
||||||
以“新增一个弹窗”为例:
|
以“新增一个弹窗”为例:① **精灵结构常量**定义精灵结构与 ID(弹窗图层 301–400 段、群组、各精灵 1001–3000);② **图片资源常量**定义所需图片资源 ID(背景、行模板…),注明帧数/帧义/尺寸;③ **布局常量**定义坐标尺寸(列表容器、行高、各列偏移);④ **整合入口**并入统一的精灵常量;⑤ **写组件**只引用精灵常量与 `SpriteManager`,不出现任何裸数字。
|
||||||
|
|
||||||
1. **精灵结构常量**:定义精灵结构与 ID(弹窗图层 301–400 段、群组、各精灵 ID 1001–3000)。
|
|
||||||
2. **图片资源常量**:定义所需图片资源 ID(背景、行模板…),注明帧数/帧义/尺寸。
|
|
||||||
3. **布局常量**:定义坐标尺寸(列表容器、行高、各列偏移)。
|
|
||||||
4. **整合入口**:把新结构并入统一的精灵常量。
|
|
||||||
5. **写组件**:UI 代码只引用精灵常量与 `SpriteManager`,不出现任何裸数字。
|
|
||||||
|
|
||||||
### 几条约定
|
### 几条约定
|
||||||
|
|
||||||
- **按钮只要 1 帧普通态图**:点击的缩放/透明反馈由框架统一处理,不要为“按下态”单独配图。
|
- **按钮只要 1 帧普通态图**:点击的缩放/透明反馈由框架统一处理,不为“按下态”单独配图。
|
||||||
- **帧动画只在图片资源常量记资源 ID**,帧区间/帧间隔/循环等播放参数放动画配置(见 03),不在资源文件重复。
|
- **帧动画只在图片资源常量记资源 ID**,帧区间/帧间隔/循环等播放参数放动画配置(见 03),不在资源文件重复。
|
||||||
- **布局文件是纯数据**:无函数、无副作用。侧视角的“透视倾斜”用每张牌累积的透视偏移量表达。
|
- **布局文件是纯数据**:无函数、无副作用。侧视角的“透视倾斜”用每张牌累积的透视偏移量表达。
|
||||||
- **多文件用保护性声明**:`var XxxConstants = XxxConstants || {};` 便于拆分到主界面/弹窗多个文件,加载后合并为一份。
|
- **多文件用保护性声明** `var XxxConstants = XxxConstants || {};`,便于拆到主界面/弹窗多个文件、加载后合并为一份。
|
||||||
|
|
||||||
> 模板 `gameabc-framework/templates/*.template.js` 是这三类文件的**写法蓝本**,含详细的字段注释与 `@see` 指向实现文件。派生新子游戏时复制模板再按实际资源填充。
|
> 模板 `gameabc-framework/templates/*.template.js` 是这三类文件的**写法蓝本**(含字段注释与 `@see` 指向实现文件)。派生新子游戏时复制模板再按实际资源填充。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -120,36 +95,21 @@ ID 必须与编辑器中实际存在的精灵**完全一致**。超范围 → `S
|
|||||||
|
|
||||||
所有视图组件继承框架的 `BaseComponent`,获得统一的**生命周期**与**事件自动清理**。
|
所有视图组件继承框架的 `BaseComponent`,获得统一的**生命周期**与**事件自动清理**。
|
||||||
|
|
||||||
### 标准结构
|
### 标准结构与生命周期
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var MyView = Object.create(BaseComponent);
|
var MyView = Object.create(BaseComponent);
|
||||||
|
|
||||||
MyView.init = function (config) {
|
MyView.init = function (config) {
|
||||||
// ⚠️ 必须在 init 内重新声明实例属性,避免原型共享污染
|
// ⚠️ 必须在 init 内重新声明实例属性,避免原型共享污染
|
||||||
this.sprites = [];
|
this.sprites = []; this.eventListeners = {}; this.data = { /* 自有数据只放这里,见 §3 范式 */ };
|
||||||
this.eventListeners = {};
|
|
||||||
this.isVisible = false;
|
|
||||||
this.isInitialized = false;
|
|
||||||
this.isDestroyed = false;
|
|
||||||
this.name = config.name || 'MyView';
|
|
||||||
this.layer = config.layer || 102;
|
this.layer = config.layer || 102;
|
||||||
|
this.addSprite(spriteConstants.SOME_PANEL_BG); // 精灵 ID 从常量取(见 §2)
|
||||||
// 创建精灵(ID 从精灵常量取,见 §2)
|
|
||||||
this.addSprite(spriteConstants.SOME_PANEL_BG);
|
|
||||||
|
|
||||||
// 注册事件(用 addEventListener,destroy 时自动清理)
|
|
||||||
var self = this;
|
var self = this;
|
||||||
this.addEventListener(EventBus.Events.GAME_STARTED, function (data) {
|
this.addEventListener(EventBus.Events.GAME_STARTED, function (d) { self._onGameStarted(d); });
|
||||||
self._onGameStarted(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.isInitialized = true;
|
this.isInitialized = true;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
### 生命周期钩子
|
|
||||||
|
|
||||||
| 钩子 | 何时 | 是否覆盖 |
|
| 钩子 | 何时 | 是否覆盖 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| `init(config)` | 创建时 | ✅ 子类必须实现:建精灵、注册事件 |
|
| `init(config)` | 创建时 | ✅ 子类必须实现:建精灵、注册事件 |
|
||||||
@@ -159,46 +119,20 @@ MyView.init = function (config) {
|
|||||||
|
|
||||||
### 事件必须走 `addEventListener`(防内存泄漏)
|
### 事件必须走 `addEventListener`(防内存泄漏)
|
||||||
|
|
||||||
```js
|
直接 `EventBus.on(...)` 在 `destroy` 时不会自动 `off`、监听残留 → 泄漏;一律改用 `this.addEventListener(...)`:它包装处理函数(绑定 `this`、捕获异常、销毁后忽略事件)并记录下来,在 `destroy()` 时统一 `EventBus.off`。**销毁组件用 `destroy()` 而非只 `hide()`**——只有 `destroy()` 才清理监听。
|
||||||
// ❌ 直接 EventBus.on:destroy 时不会自动 off,监听残留 → 泄漏
|
|
||||||
EventBus.on(EventBus.Events.GAME_STARTED, fn);
|
|
||||||
|
|
||||||
// ✅ BaseComponent.addEventListener:destroy() 自动 off
|
|
||||||
this.addEventListener(EventBus.Events.GAME_STARTED, fn);
|
|
||||||
```
|
|
||||||
|
|
||||||
`BaseComponent.addEventListener` 会包装处理函数(绑定 `this`、捕获异常、销毁后忽略事件),并记录下来在 `destroy()` 时统一 `EventBus.off`。**销毁组件用 `destroy()` 而非只 `hide()`** —— 只有 `destroy()` 才清理监听。
|
|
||||||
|
|
||||||
### 组件数据自持 + set-refresh 范式(核心)
|
### 组件数据自持 + set-refresh 范式(核心)
|
||||||
|
|
||||||
**前端只负责界面展示与玩家交互;所有核心运算与胜负裁定以服务端为准、数据以服务端为权威**(见 01/05)。组件本地保存的只是「用于画界面的数据副本」,据此渲染,不做权威计算。为此每个组件遵循统一范式:
|
**前端只负责界面展示与玩家交互;所有核心运算与胜负裁定以服务端为准、数据以服务端为权威**(见 01/05)。组件本地保存的只是「用于画界面的数据副本」,据此渲染、不做权威计算。为此每个组件遵循统一范式:
|
||||||
|
|
||||||
1. **数据集中在 `this.data`**:组件(及其下每个「零件 UI」)的自有数据全部收拢到 `this.data.*`,**不散落**在实例其他字段或全局;界面能否重建只取决于 `this.data`。
|
1. **数据集中在 `this.data`**:组件及其下每个「零件 UI」的自有数据全部收拢到 `this.data.*`,**不散落**在实例其他字段或全局;界面能否重建只取决于 `this.data`。
|
||||||
|
2. **每个 UI 都有 `setXxx` / `refreshXxx` 成对方法**:`setXxx(...)` **只写数据**到 `this.data`、不碰精灵;`refreshXxx()` **只据 `this.data` 画界面**、不改数据。两者职责单一、互不越界。
|
||||||
2. **每个 UI 都有 `setXxx` / `refreshXxx` 成对方法**:组件本身、以及组件内每个「零件 UI」(如玩家信息条、分数、手牌区、按钮组)都提供一对——
|
3. **组件有一个总 `refresh()`**:顺序调用其下所有零件的 `refreshXxx()`,据 `this.data` 完整重画。**任何时候调 `refresh()` 都能无歧义重建正确界面**(断线重连、切 app 回来、网页刷新都复用它,不另写一套渲染)。
|
||||||
- `setXxx(...)`:**只写数据**到 `this.data`,不碰精灵;
|
|
||||||
- `refreshXxx()`:**只据 `this.data` 画界面**,不改数据。
|
|
||||||
两者职责单一、互不越界。
|
|
||||||
|
|
||||||
3. **组件有一个总 `refresh()`**:顺序调用其下所有零件的 `refreshXxx()`,把整块界面据 `this.data` 完整重画。**任何时候调用 `refresh()` 都能无歧义重建正确界面**(断线重连、切 app 回来、网页刷新都复用它,不另写一套渲染)。
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var GameView = Object.create(BaseComponent);
|
GameView.setScore = function (v) { this.data.score = v; }; // 只写数据
|
||||||
GameView.init = function (config) {
|
GameView.refreshScore = function () { SpriteManager.setText(SCORE_ID, this.data.score); }; // 只据数据画
|
||||||
this.data = { players: [], score: 0 /* ...自有数据只放这里 */ };
|
GameView.refresh = function () { this.refreshPlayers(); this.refreshScore(); /* ...其余零件 */ };
|
||||||
// ...
|
|
||||||
};
|
|
||||||
|
|
||||||
// 零件 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` 刷新静态界面),**再**做动画/表现。动画只是体验层,其「开始/结束/出错」等生命周期回调里**只刷新界面、绝不设置核心数据**。这样即使动画卡住/播错、或开发前期还没做动画,数据、逻辑与界面依然正确、互不影响。
|
**收包的正确流程**:先 `setXxx` 把数据写进 `this.data`(必要时 `refresh` 刷新静态界面),**再**做动画/表现。动画只是体验层,其「开始/结束/出错」等生命周期回调里**只刷新界面、绝不设置核心数据**。这样即使动画卡住/播错、或开发前期还没做动画,数据、逻辑与界面依然正确、互不影响。
|
||||||
@@ -207,112 +141,70 @@ GameView.refresh = function () {
|
|||||||
|
|
||||||
## 4. UIManager:注册与场景切换
|
## 4. UIManager:注册与场景切换
|
||||||
|
|
||||||
`UIManager` 管理所有组件实例与**场景**(一组组件的具名快照)。
|
`UIManager` 管理所有组件实例与**场景**(一组组件的具名快照):`registerComponent(name, comp)` 注册组件、`registerScene(sceneName, [组件名...])` 注册场景、`switchToScene(name)` 切换(自动隐藏旧场景组件、显示新场景组件)。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// 启动时(在启动编排内)
|
UIManager.init(config); // 见下:注入全局 UI 常量
|
||||||
UIManager.init(); // 若有 Loading/Message/Confirm 全局 UI,则注入其精灵常量(见下)
|
UIManager.registerComponent('GameView', MyView.create({ name: 'GameView', layer: 102 }));
|
||||||
|
|
||||||
var gameView = MyView.create({ name: 'GameView', layer: 102 });
|
|
||||||
UIManager.registerComponent('GameView', gameView);
|
|
||||||
UIManager.registerComponent('RoomView', RoomView.create({ name: 'RoomView' }));
|
|
||||||
|
|
||||||
// 注册场景:场景名 → 组件名列表
|
|
||||||
UIManager.registerScene(UIManager.SCENES.GAME, ['GameView', 'SomeOtherView']);
|
UIManager.registerScene(UIManager.SCENES.GAME, ['GameView', 'SomeOtherView']);
|
||||||
UIManager.registerScene(UIManager.SCENES.ROOM, ['RoomView']);
|
|
||||||
|
|
||||||
// 切换场景:自动隐藏旧场景组件、显示新场景组件
|
|
||||||
UIManager.switchToScene(UIManager.SCENES.GAME);
|
UIManager.switchToScene(UIManager.SCENES.GAME);
|
||||||
```
|
```
|
||||||
|
|
||||||
`UIManager` 还提供全局 UI:`showLoading/hideLoading`、`showMessage`、`showConfirm`。这三个全局 UI 的精灵常量由**子游戏注入**——框架不读取任何子游戏全局名,子游戏在 `init` 时传入配置:
|
全局 UI `showLoading/hideLoading`、`showMessage`、`showConfirm` 的精灵常量由**子游戏注入**——框架不读任何子游戏全局名,子游戏在 `init` 时传入(不传则跳过全局 UI;也可后续 `UIManager.configureGlobalUI(config)`):
|
||||||
|
|
||||||
```js
|
```js
|
||||||
UIManager.init({
|
UIManager.init({ loadingUI: spriteConstants.LOADING_UI, messageUI: spriteConstants.MESSAGE_UI, confirmUI: spriteConstants.CONFIRM_UI });
|
||||||
loadingUI: spriteConstants.LOADING_UI,
|
|
||||||
messageUI: spriteConstants.MESSAGE_UI,
|
|
||||||
confirmUI: spriteConstants.CONFIRM_UI
|
|
||||||
}); // 不传则跳过全局 UI;也可后续 UIManager.configureGlobalUI(config)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**游戏专属弹窗**用“向 UIManager 挂方法”的扩展模式(在独立 JS 里 `UIManager.showCreateRoom = function(){...}`),保持框架本身中立。
|
**游戏专属弹窗**用“向 UIManager 挂方法”的扩展模式(独立 JS 里 `UIManager.showCreateRoom = function(){...}`),保持框架本身中立。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. 精灵交互事件:SpriteEventController
|
## 5. 精灵交互事件:SpriteEventController
|
||||||
|
|
||||||
精灵的点击/拖拽/绘制等交互,由框架 `gameabc-framework/system/SpriteEventController.js` 统一分发——它是**游戏中立**的通用能力,玩法专属逻辑通过钩子注册,不写进框架。
|
精灵的点击/拖拽/绘制交互由框架 `gameabc-framework/system/SpriteEventController.js` 统一分发——**游戏中立**,玩法专属逻辑通过钩子注册、不写进框架。平台引擎交互先进受限入口 `Game_Modify.utlmousedown/mouseup/utlmousemove/utlgamemydrawbegin/gamemydraw`,这些入口**只把事件转调**给 `SpriteEventController.handle*`,由它按「精灵 ID」分发到注册回调。
|
||||||
|
|
||||||
平台引擎的交互事件先进受限入口 `Game_Modify.utlmousedown/mouseup/utlmousemove/utlgamemydrawbegin/gamemydraw`,这些入口**只把事件转调**给 `SpriteEventController.handle*`,由它按「精灵 ID」分发到注册的回调。
|
|
||||||
|
|
||||||
### 按精灵 ID 注册回调
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
// 在组件 init 里注册(精灵 ID 从常量取,见 §2)
|
SpriteEventController.registerMouseUp(sprites.BTN_START, function (event) { self._onStart(); }); // event 含 spriteId/坐标/偏移
|
||||||
SpriteEventController.registerMouseUp(sprites.BTN_START, function (event) {
|
SpriteEventController.registerMouseMove(sprites.HAND_AREA, function (event) { self._onDrag(event.offset); });
|
||||||
self._onStart(); // event 含 spriteId/坐标/偏移等
|
// 批量 registerBatch(spriteIds, 'mouseDown', handler);注销 unregister(spriteId[, eventType])
|
||||||
});
|
|
||||||
SpriteEventController.registerMouseMove(sprites.HAND_AREA, function (event) {
|
|
||||||
self._onDrag(event.offset);
|
|
||||||
});
|
|
||||||
// 批量:registerBatch(spriteIds, 'mouseDown', handler)
|
|
||||||
// 注销:unregister(spriteId[, eventType])
|
|
||||||
```
|
```
|
||||||
|
|
||||||
支持的事件类型:`mouseDown` / `mouseDownNoMove`(长按) / `mouseUp` / `mouseMove`(拖拽) / `drawBegin` / `draw`。
|
支持的事件类型:`mouseDown` / `mouseDownNoMove`(长按) / `mouseUp` / `mouseMove`(拖拽) / `drawBegin` / `draw`。
|
||||||
|
|
||||||
### 全局 draw 钩子
|
对**每个**绘制精灵统一处理(不针对某个固定 ID)用 `registerGlobalDraw(fn)`——框架对每个 draw 事件都回调它、但**不认识**其业务含义,保持中立(“精牌标记”这类玩法叠绘即以此挂载,框架零感知,见 05「框架中立」)。
|
||||||
|
|
||||||
需要对**每个**绘制精灵统一处理(不针对某个固定 ID)的能力,用 `registerGlobalDraw` 注册——框架对每个 draw 事件都回调它,但**不认识**其业务含义,保持中立:
|
|
||||||
|
|
||||||
```js
|
|
||||||
// 例:某种标记叠绘——子游戏自行挂载,框架零感知
|
|
||||||
SpriteEventController.registerGlobalDraw(function (spriteId) {
|
|
||||||
// 子游戏自定义的叠绘逻辑
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
> 这套通用精灵事件分发原先躺在子游戏 `controllers` 里、还被框架反向依赖;现已抽进框架,并把「精牌标记」这类玩法耦合改为全局 draw 钩子(见 05「框架中立」)。
|
|
||||||
|
|
||||||
### 更高层:手势识别 `SpriteGestureRecognizer`
|
### 更高层:手势识别 `SpriteGestureRecognizer`
|
||||||
|
|
||||||
需要识别**双击 / 滑动 / 点击**等手势时,用框架 `system/SpriteGestureRecognizer.js`(建立在 `SpriteEventController` 之上)。它把原始按下/移动/松开识别为语义手势,业务只写回调:
|
识别**双击 / 滑动 / 点击**等手势用框架 `system/SpriteGestureRecognizer.js`(建在 `SpriteEventController` 之上),把原始按下/移动/松开识别为语义手势,业务只写回调:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var handle = SpriteGestureRecognizer.attach(spriteIds, {
|
var handle = SpriteGestureRecognizer.attach(spriteIds, {
|
||||||
onPress: function (e) {}, // 按下即时反馈(如元素站起)
|
onPress: fn, // 按下即时反馈(如元素站起)
|
||||||
onDoubleTap: function (e) {}, // 双击同一精灵
|
onDoubleTap: fn, // 双击同一精灵
|
||||||
onSwipe: function (e) {}, // 沿 e.direction 滑动越阈
|
onSwipe: fn, // 沿 e.direction 滑动越阈
|
||||||
onTap: function (e) {} // 点击(小位移按下→松开)
|
onTap: fn // 点击(小位移按下→松开)
|
||||||
}, { doubleTapInterval: 300, swipe: { direction: 'up', threshold: 50 }, minMove: 10 });
|
}, { doubleTapInterval: 300, swipe: { direction: 'up', threshold: 50 }, minMove: 10 });
|
||||||
// handle.detach() / resetDoubleTap() / resetDrag()
|
// handle.detach() / resetDoubleTap() / resetDrag()
|
||||||
```
|
```
|
||||||
|
|
||||||
阈值由子游戏注入(框架只给默认值、不反读子游戏常量);双击/上滑/点击判定全在框架,选中/出牌等业务全留子游戏。
|
阈值由子游戏注入(框架只给默认值、不反读子游戏常量);双击/上滑/点击判定全在框架,选中/出牌等业务全留子游戏。
|
||||||
|
|
||||||
> 范例:手牌的「双击出牌 / 上划出牌 / 点击选中取消」由子游戏的交互处理器用本识别器实现——手势识别归框架,玩法业务归子游戏。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. 动态列表:SpriteCopyUtils 与 DynamicSpriteList
|
## 6. 动态列表:SpriteCopyUtils 与 DynamicSpriteList
|
||||||
|
|
||||||
行数不定的列表(如听牌提示、战绩行)用**动态复制精灵**实现,不要为每行预置精灵。
|
行数不定的列表(听牌提示、战绩行)用**动态复制精灵**实现,不为每行预置精灵:
|
||||||
|
|
||||||
- **`SpriteCopyUtils`**(底层):从“模板精灵”复制出带 `tag` 的子精灵,返回字符串 ID;`create/remove/removeRange`。
|
- **`SpriteCopyUtils`**(底层):从“模板精灵”复制出带 `tag` 的子精灵、返回字符串 ID;`create/remove/removeRange`。
|
||||||
- **`DynamicSpriteList`**(高级):封装容器裁剪区、行高、每列模板、滚动与点击,开发者只 `setData` + `onClick`。
|
- **`DynamicSpriteList`**(高级):封装容器裁剪区、行高、每列模板、滚动与点击,开发者只 `setData` + `onClick`。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
var list = new DynamicSpriteList({
|
var list = new DynamicSpriteList({
|
||||||
containerId: spriteConstants.SOME_LIST_CONTAINER,
|
containerId: spriteConstants.SOME_LIST_CONTAINER, clipArea: { x: 0, y: 0, width: 640, height: 480 }, rowHeight: 80,
|
||||||
clipArea: { x: 0, y: 0, width: 640, height: 480 },
|
templates: { rowBg: { spriteId: 2837 }, card: { spriteId: 2838, offset: { x: 20, y: 10 } }, score: { spriteId: 2839, offset: { x: 120, y: 10 } } }
|
||||||
rowHeight: 80,
|
|
||||||
templates: {
|
|
||||||
rowBg: { spriteId: 2837 },
|
|
||||||
card: { spriteId: 2838, offset: { x: 20, y: 10 } },
|
|
||||||
score: { spriteId: 2839, offset: { x: 120, y: 10 } }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
list.setData(rows); // 数据驱动
|
list.setData(rows); // 数据驱动
|
||||||
list.onClick = function (type, rowIndex, rowData) { /* ... */ };
|
list.onClick = function (type, rowIndex, rowData) { /* ... */ };
|
||||||
// 组件 onDestroy 中:list.destroy(); ← 必须,否则复制精灵残留
|
// 组件 onDestroy 中:list.destroy(); ← 必须,否则复制精灵残留
|
||||||
```
|
```
|
||||||
@@ -334,4 +226,3 @@ list.onClick = function (type, rowIndex, rowData) { /* ... */ };
|
|||||||
| 动态列表用 `DynamicSpriteList` 并 `destroy` | 手搓 SpriteCopy 又忘清理 |
|
| 动态列表用 `DynamicSpriteList` 并 `destroy` | 手搓 SpriteCopy 又忘清理 |
|
||||||
|
|
||||||
下一篇 [03-事件·动画·音频·Spine](./03-事件·动画·音频·Spine.md) 讲表现系统:事件总线、动画、音频与 Spine。
|
下一篇 [03-事件·动画·音频·Spine](./03-事件·动画·音频·Spine.md) 讲表现系统:事件总线、动画、音频与 Spine。
|
||||||
</content>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user