@mclawnet/agent 0.6.19 → 0.6.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-RO47ET27.js → chunk-M2CDVPQF.js} +2 -2
- package/dist/{chunk-CBZIH6FY.js → chunk-PJ5M6Q36.js} +2 -2
- package/dist/chunk-PJ5M6Q36.js.map +1 -0
- package/dist/{chunk-MSDIRBXF.js → chunk-RIK7IXSW.js} +2 -2
- package/dist/index.js +2 -2
- package/dist/{linux-6AR7SXHW.js → linux-IHA4O633.js} +3 -3
- package/dist/{macos-BTP5JW3U.js → macos-G4VK2253.js} +8 -3
- package/dist/macos-G4VK2253.js.map +1 -0
- package/dist/service/index.js +5 -5
- package/dist/service/macos.d.ts.map +1 -1
- package/dist/start.js +2 -2
- package/dist/{windows-IQNSUMN6.js → windows-P6U3JLUZ.js} +3 -3
- package/package.json +4 -4
- package/skills/cocos-creator-3x-cn/SKILL.md +475 -0
- package/skills/cocos-creator-3x-cn/references/framework/asset-management.md +322 -0
- package/skills/cocos-creator-3x-cn/references/framework/component-system.md +348 -0
- package/skills/cocos-creator-3x-cn/references/framework/event-patterns.md +410 -0
- package/skills/cocos-creator-3x-cn/references/framework/playable-optimization.md +257 -0
- package/skills/cocos-creator-3x-cn/references/language/performance.md +363 -0
- package/skills/cocos-creator-3x-cn/references/language/quality-hygiene.md +307 -0
- package/skills/cocos-creator-3x-cn/references/review/architecture-review.md +183 -0
- package/skills/cocos-creator-3x-cn/references/review/quality-review.md +251 -0
- package/skills/cocos-performance-optimizer/SKILL.md +214 -0
- package/skills/game-development/2d-games/SKILL.md +129 -0
- package/skills/game-development/3d-games/SKILL.md +145 -0
- package/skills/game-development/SKILL.md +175 -0
- package/skills/game-development/game-art/SKILL.md +195 -0
- package/skills/game-development/game-audio/SKILL.md +200 -0
- package/skills/game-development/game-design/SKILL.md +139 -0
- package/skills/game-development/mobile-games/SKILL.md +118 -0
- package/skills/game-development/multiplayer/SKILL.md +142 -0
- package/skills/game-development/pc-games/SKILL.md +154 -0
- package/skills/game-development/vr-ar/SKILL.md +133 -0
- package/skills/game-development/web-games/SKILL.md +160 -0
- package/skills/game-engine/SKILL.md +140 -0
- package/skills/game-engine/assets/2d-maze-game.md +528 -0
- package/skills/game-engine/assets/2d-platform-game.md +1855 -0
- package/skills/game-engine/assets/gameBase-template-repo.md +310 -0
- package/skills/game-engine/assets/paddle-game-template.md +1528 -0
- package/skills/game-engine/assets/simple-2d-engine.md +507 -0
- package/skills/game-engine/references/3d-web-games.md +754 -0
- package/skills/game-engine/references/algorithms.md +843 -0
- package/skills/game-engine/references/basics.md +343 -0
- package/skills/game-engine/references/game-control-mechanisms.md +617 -0
- package/skills/game-engine/references/game-engine-core-principles.md +695 -0
- package/skills/game-engine/references/game-publishing.md +352 -0
- package/skills/game-engine/references/techniques.md +894 -0
- package/skills/game-engine/references/terminology.md +354 -0
- package/skills/game-engine/references/web-apis.md +1394 -0
- package/skills/theone-cocos-standards/SKILL.md +557 -0
- package/skills/theone-cocos-standards/references/framework/component-system.md +645 -0
- package/skills/theone-cocos-standards/references/framework/event-patterns.md +433 -0
- package/skills/theone-cocos-standards/references/framework/playable-optimization.md +429 -0
- package/skills/theone-cocos-standards/references/framework/size-optimization.md +308 -0
- package/skills/theone-cocos-standards/references/language/modern-typescript.md +658 -0
- package/skills/theone-cocos-standards/references/language/performance.md +580 -0
- package/skills/theone-cocos-standards/references/language/quality-hygiene.md +582 -0
- package/skills/theone-cocos-standards/references/review/architecture-review.md +250 -0
- package/skills/theone-cocos-standards/references/review/performance-review.md +288 -0
- package/skills/theone-cocos-standards/references/review/quality-review.md +239 -0
- package/dist/chunk-CBZIH6FY.js.map +0 -1
- package/dist/macos-BTP5JW3U.js.map +0 -1
- /package/dist/{chunk-RO47ET27.js.map → chunk-M2CDVPQF.js.map} +0 -0
- /package/dist/{chunk-MSDIRBXF.js.map → chunk-RIK7IXSW.js.map} +0 -0
- /package/dist/{linux-6AR7SXHW.js.map → linux-IHA4O633.js.map} +0 -0
- /package/dist/{windows-IQNSUMN6.js.map → windows-P6U3JLUZ.js.map} +0 -0
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
# 事件系统模式 — Cocos Creator 3.8
|
|
2
|
+
|
|
3
|
+
> 📖 本文件为 `cocos-creator-3x-cn` 技能参考文件,提供事件系统的深入指导。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. 事件系统架构
|
|
8
|
+
|
|
9
|
+
Cocos Creator 3.8 事件系统由三层构成:
|
|
10
|
+
|
|
11
|
+
| 层级 | 入口对象 | 说明 |
|
|
12
|
+
|------|---------|------|
|
|
13
|
+
| 全局输入事件 | `input` | 键盘/鼠标/触摸/重力,与节点树无关 |
|
|
14
|
+
| 节点事件 | `node.on(...)` | UI 触摸/鼠标,与 UI 节点树关联 |
|
|
15
|
+
| 自定义事件 | `EventTarget` | 游戏逻辑事件总线 |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 2. 全局输入事件 (input)
|
|
20
|
+
|
|
21
|
+
### 2.1 完整 API
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { input, Input, EventTouch, EventMouse, EventKeyboard, EventAcceleration, KeyCode } from 'cc';
|
|
25
|
+
|
|
26
|
+
// 注册(在 onEnable)
|
|
27
|
+
input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
|
28
|
+
input.on(Input.EventType.KEY_DOWN, this.onKeyDown, this);
|
|
29
|
+
|
|
30
|
+
// 注销(在 onDisable)
|
|
31
|
+
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
|
32
|
+
input.off(Input.EventType.KEY_DOWN, this.onKeyDown, this);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 2.2 触摸事件
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
private onTouchStart(event: EventTouch): void {
|
|
39
|
+
const screenPos = event.getLocation(); // Vec2 屏幕坐标
|
|
40
|
+
const uiPos = event.getUILocation(); // Vec2 UI 坐标
|
|
41
|
+
const delta = event.getDelta(); // Vec2 位移
|
|
42
|
+
const touch = event.touch!; // Touch 对象
|
|
43
|
+
const touchId = touch.getID(); // 触点 ID(多点触控)
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**事件类型**:
|
|
48
|
+
- `TOUCH_START` — 手指按下
|
|
49
|
+
- `TOUCH_MOVE` — 手指移动
|
|
50
|
+
- `TOUCH_END` — 手指在节点区域内抬起
|
|
51
|
+
- `TOUCH_CANCEL` — 手指在节点区域外抬起
|
|
52
|
+
|
|
53
|
+
### 2.3 键盘事件
|
|
54
|
+
|
|
55
|
+
```typescript
|
|
56
|
+
private onKeyDown(event: EventKeyboard): void {
|
|
57
|
+
switch (event.keyCode) {
|
|
58
|
+
case KeyCode.KEY_W:
|
|
59
|
+
case KeyCode.ARROW_UP:
|
|
60
|
+
this.moveUp();
|
|
61
|
+
break;
|
|
62
|
+
case KeyCode.SPACE:
|
|
63
|
+
this.jump();
|
|
64
|
+
break;
|
|
65
|
+
case KeyCode.ESCAPE:
|
|
66
|
+
this.pause();
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
**事件类型**:
|
|
73
|
+
- `KEY_DOWN` — 键按下
|
|
74
|
+
- `KEY_PRESSING` — 键持续按下
|
|
75
|
+
- `KEY_UP` — 键释放
|
|
76
|
+
|
|
77
|
+
### 2.4 鼠标事件
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { EventMouse } from 'cc';
|
|
81
|
+
|
|
82
|
+
private onMouseWheel(event: EventMouse): void {
|
|
83
|
+
const scrollY = event.getScrollY();
|
|
84
|
+
// 缩放逻辑
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**事件类型**: `MOUSE_DOWN`, `MOUSE_MOVE`, `MOUSE_UP`, `MOUSE_WHEEL`
|
|
89
|
+
|
|
90
|
+
### 2.5 重力传感
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
protected onLoad(): void {
|
|
94
|
+
input.setAccelerometerEnabled(true);
|
|
95
|
+
input.on(Input.EventType.DEVICEMOTION, this.onDeviceMotion, this);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private onDeviceMotion(event: EventAcceleration): void {
|
|
99
|
+
console.log(event.acc.x, event.acc.y, event.acc.z);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 3. 节点事件
|
|
106
|
+
|
|
107
|
+
### 3.1 UI 节点触摸
|
|
108
|
+
|
|
109
|
+
2D UI 节点触摸事件依赖 `UITransform` 组件:
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { _decorator, Component, Node, EventTouch } from 'cc';
|
|
113
|
+
|
|
114
|
+
@ccclass('NodeEventDemo')
|
|
115
|
+
export class NodeEventDemo extends Component {
|
|
116
|
+
protected onEnable(): void {
|
|
117
|
+
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
|
|
118
|
+
this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
|
119
|
+
this.node.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
|
|
120
|
+
this.node.on(Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
protected onDisable(): void {
|
|
124
|
+
this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
|
|
125
|
+
this.node.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
|
|
126
|
+
this.node.off(Node.EventType.TOUCH_END, this.onTouchEnd, this);
|
|
127
|
+
this.node.off(Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private onTouchStart(event: EventTouch): void {
|
|
131
|
+
// event.getUILocation() — UI 坐标
|
|
132
|
+
// event.propagationStopped = true — 阻止冒泡
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### 3.2 事件传递机制
|
|
138
|
+
|
|
139
|
+
节点事件遵循 Web 标准的事件传递:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
捕获阶段: Scene Root → ... → Parent → Target
|
|
143
|
+
目标阶段: Target
|
|
144
|
+
冒泡阶段: Target → Parent → ... → Scene Root
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
// 默认在冒泡阶段监听
|
|
149
|
+
this.node.on(Node.EventType.TOUCH_START, callback, this);
|
|
150
|
+
|
|
151
|
+
// 在捕获阶段监听(第四个参数为 true)
|
|
152
|
+
this.node.on(Node.EventType.TOUCH_START, callback, this, true);
|
|
153
|
+
|
|
154
|
+
// 阻止冒泡
|
|
155
|
+
private onTouchStart(event: EventTouch): void {
|
|
156
|
+
event.propagationStopped = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 立即停止(连同一节点上的其他监听器也不触发)
|
|
160
|
+
private onTouchStart2(event: EventTouch): void {
|
|
161
|
+
event.propagationImmediateStopped = true;
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### 3.3 事件穿透 (v3.4+)
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
// 默认情况下,同级节点中顶层节点会吞噬事件
|
|
169
|
+
// 使用 preventSwallow 允许事件穿透到下层同级节点
|
|
170
|
+
private onTouchStart(event: EventTouch): void {
|
|
171
|
+
event.preventSwallow = true;
|
|
172
|
+
}
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**注意**: TOUCH_END 穿透需要对应的 TOUCH_START 也设置穿透。
|
|
176
|
+
|
|
177
|
+
### 3.4 事件拦截
|
|
178
|
+
|
|
179
|
+
`Button`、`Toggle`、`BlockInputEvents` 组件会自动阻止事件冒泡:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
// 如果不需要按钮组件,但需要拦截事件:
|
|
183
|
+
// 添加 BlockInputEvents 组件到节点
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### 3.5 节点系统事件
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
// 变换
|
|
190
|
+
this.node.on(Node.EventType.TRANSFORM_CHANGED, (type: TransformBit) => {
|
|
191
|
+
if (type & TransformBit.POSITION) { /* 位置改变 */ }
|
|
192
|
+
if (type & TransformBit.ROTATION) { /* 旋转改变 */ }
|
|
193
|
+
if (type & TransformBit.SCALE) { /* 缩放改变 */ }
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// 2D 节点事件
|
|
197
|
+
this.node.on(Node.EventType.SIZE_CHANGED, () => { /* UITransform 尺寸变化 */ });
|
|
198
|
+
this.node.on(Node.EventType.ANCHOR_CHANGED, () => { /* 锚点变化 */ });
|
|
199
|
+
this.node.on(Node.EventType.CHILD_ADDED, (child: Node) => { /* 添加子节点 */ });
|
|
200
|
+
this.node.on(Node.EventType.CHILD_REMOVED, (child: Node) => { /* 移除子节点 */ });
|
|
201
|
+
this.node.on(Node.EventType.ACTIVE_IN_HIERARCHY_CHANGED, () => { /* 激活状态变化 */ });
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 3.6 暂停/恢复节点事件
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// 暂停当前节点的系统事件
|
|
208
|
+
this.node.pauseSystemEvents();
|
|
209
|
+
// 暂停当前节点和所有子节点的系统事件
|
|
210
|
+
this.node.pauseSystemEvents(true);
|
|
211
|
+
|
|
212
|
+
// 恢复
|
|
213
|
+
this.node.resumeSystemEvents();
|
|
214
|
+
this.node.resumeSystemEvents(true);
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 4. 自定义事件 (EventTarget)
|
|
220
|
+
|
|
221
|
+
### 4.1 基础用法
|
|
222
|
+
|
|
223
|
+
```typescript
|
|
224
|
+
import { EventTarget } from 'cc';
|
|
225
|
+
|
|
226
|
+
// 创建事件总线(推荐单例模式)
|
|
227
|
+
class GameEvents {
|
|
228
|
+
private static _instance: EventTarget;
|
|
229
|
+
public static get instance(): EventTarget {
|
|
230
|
+
if (!this._instance) {
|
|
231
|
+
this._instance = new EventTarget();
|
|
232
|
+
}
|
|
233
|
+
return this._instance;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// 监听
|
|
238
|
+
GameEvents.instance.on('score-changed', (score: number) => {
|
|
239
|
+
console.log('Score:', score);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// 发射
|
|
243
|
+
GameEvents.instance.emit('score-changed', 100);
|
|
244
|
+
|
|
245
|
+
// 一次性监听
|
|
246
|
+
GameEvents.instance.once('game-start', () => { /* 只触发一次 */ });
|
|
247
|
+
|
|
248
|
+
// 取消监听
|
|
249
|
+
GameEvents.instance.off('score-changed', callback, target);
|
|
250
|
+
// 取消该类型所有监听
|
|
251
|
+
GameEvents.instance.off('score-changed');
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
**⚠️ 限制**: `emit` 最多支持 5 个参数。
|
|
255
|
+
|
|
256
|
+
### 4.2 类型安全的事件系统
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
// 定义事件类型映射
|
|
260
|
+
interface GameEventMap {
|
|
261
|
+
'game-start': () => void;
|
|
262
|
+
'game-over': (score: number, isWin: boolean) => void;
|
|
263
|
+
'level-up': (level: number) => void;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 类型安全的事件总线封装
|
|
267
|
+
class TypedEventTarget {
|
|
268
|
+
private _et = new EventTarget();
|
|
269
|
+
|
|
270
|
+
public on<K extends keyof GameEventMap>(type: K, callback: GameEventMap[K], target?: any): void {
|
|
271
|
+
this._et.on(type, callback as any, target);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
public off<K extends keyof GameEventMap>(type: K, callback: GameEventMap[K], target?: any): void {
|
|
275
|
+
this._et.off(type, callback as any, target);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
public emit<K extends keyof GameEventMap>(type: K, ...args: Parameters<GameEventMap[K]>): void {
|
|
279
|
+
this._et.emit(type, ...args);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### 4.3 节点自定义事件派发
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
import { Event } from 'cc';
|
|
288
|
+
|
|
289
|
+
// 自定义事件类(不要直接 new Event,它是抽象类)
|
|
290
|
+
class DamageEvent extends Event {
|
|
291
|
+
public readonly damage: number;
|
|
292
|
+
constructor(damage: number, bubbles = true) {
|
|
293
|
+
super('damage', bubbles);
|
|
294
|
+
this.damage = damage;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// 派发(支持冒泡)
|
|
299
|
+
this.node.dispatchEvent(new DamageEvent(50));
|
|
300
|
+
|
|
301
|
+
// 父节点监听
|
|
302
|
+
parentNode.on('damage', (event: DamageEvent) => {
|
|
303
|
+
console.log('Damage:', event.damage);
|
|
304
|
+
event.propagationStopped = true; // 可选:阻止继续冒泡
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 5. 3D 物体触摸检测
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { _decorator, Component, Camera, geometry, input, Input, EventTouch, PhysicsSystem, Node } from 'cc';
|
|
314
|
+
const { ccclass, property } = _decorator;
|
|
315
|
+
|
|
316
|
+
@ccclass('RaycastTouch')
|
|
317
|
+
export class RaycastTouch extends Component {
|
|
318
|
+
@property(Camera)
|
|
319
|
+
private readonly cameraCom!: Camera;
|
|
320
|
+
|
|
321
|
+
@property(Node)
|
|
322
|
+
private readonly targetNode!: Node;
|
|
323
|
+
|
|
324
|
+
private _ray = new geometry.Ray();
|
|
325
|
+
|
|
326
|
+
protected onEnable(): void {
|
|
327
|
+
input.on(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
protected onDisable(): void {
|
|
331
|
+
input.off(Input.EventType.TOUCH_START, this.onTouchStart, this);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private onTouchStart(event: EventTouch): void {
|
|
335
|
+
const touch = event.touch!;
|
|
336
|
+
this.cameraCom.screenPointToRay(touch.getLocationX(), touch.getLocationY(), this._ray);
|
|
337
|
+
if (PhysicsSystem.instance.raycast(this._ray)) {
|
|
338
|
+
const results = PhysicsSystem.instance.raycastResults;
|
|
339
|
+
for (const result of results) {
|
|
340
|
+
if (result.collider.node === this.targetNode) {
|
|
341
|
+
console.log('Hit target!');
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## 6. 多点触控控制
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { macro } from 'cc';
|
|
356
|
+
|
|
357
|
+
// 关闭多点触控(项目全局设置)
|
|
358
|
+
macro.ENABLE_MULTI_TOUCH = false;
|
|
359
|
+
|
|
360
|
+
// 也可以在菜单中设置:项目 → 项目设置 → Macro Config
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
## 7. 最佳实践
|
|
366
|
+
|
|
367
|
+
### 事件监听配对原则
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// ✅ 正确:onEnable / onDisable 配对
|
|
371
|
+
protected onEnable(): void {
|
|
372
|
+
input.on(Input.EventType.TOUCH_START, this.onTouch, this);
|
|
373
|
+
this.node.on(Node.EventType.TOUCH_START, this.onNodeTouch, this);
|
|
374
|
+
}
|
|
375
|
+
protected onDisable(): void {
|
|
376
|
+
input.off(Input.EventType.TOUCH_START, this.onTouch, this);
|
|
377
|
+
this.node.off(Node.EventType.TOUCH_START, this.onNodeTouch, this);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ❌ 错误:只注册不注销 → 事件泄漏
|
|
381
|
+
protected onLoad(): void {
|
|
382
|
+
input.on(Input.EventType.TOUCH_START, this.onTouch, this);
|
|
383
|
+
}
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### 使用枚举而非字符串
|
|
387
|
+
|
|
388
|
+
```typescript
|
|
389
|
+
// ✅ 正确
|
|
390
|
+
this.node.on(Node.EventType.TOUCH_START, callback, this);
|
|
391
|
+
input.on(Input.EventType.KEY_DOWN, callback, this);
|
|
392
|
+
|
|
393
|
+
// ❌ 不推荐
|
|
394
|
+
this.node.on('touch-start', callback, this);
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### 避免在回调中创建闭包
|
|
398
|
+
|
|
399
|
+
```typescript
|
|
400
|
+
// ✅ 正确:使用方法引用
|
|
401
|
+
protected onEnable(): void {
|
|
402
|
+
input.on(Input.EventType.TOUCH_START, this.onTouch, this);
|
|
403
|
+
}
|
|
404
|
+
private onTouch(event: EventTouch): void { /* ... */ }
|
|
405
|
+
|
|
406
|
+
// ❌ 不推荐:匿名函数无法取消监听
|
|
407
|
+
protected onEnable2(): void {
|
|
408
|
+
input.on(Input.EventType.TOUCH_START, (e) => { /* ... */ });
|
|
409
|
+
}
|
|
410
|
+
```
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# 可试玩广告优化 — Cocos Creator 3.8
|
|
2
|
+
|
|
3
|
+
> 📖 本文件为 `cocos-creator-3x-cn` 技能参考文件,提供可试玩广告的包体与性能优化指导。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. 可试玩广告概述
|
|
8
|
+
|
|
9
|
+
可试玩广告 (Playable Ad) 是一种交互式广告格式,通常要求:
|
|
10
|
+
- **单文件 HTML** 输出(内联所有资源)
|
|
11
|
+
- **包体限制**: 2MB-5MB(根据平台要求)
|
|
12
|
+
- **快速加载**: 3 秒内可交互
|
|
13
|
+
- **全程流畅**: 30fps+ 无卡顿
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. 包体优化策略
|
|
18
|
+
|
|
19
|
+
### 2.1 引擎模块裁剪
|
|
20
|
+
|
|
21
|
+
在 **项目设置 → 功能裁剪** 中关闭不需要的模块:
|
|
22
|
+
|
|
23
|
+
| 可安全关闭的模块 | 节省大小 |
|
|
24
|
+
|----------------|---------|
|
|
25
|
+
| 3D 相关(Mesh/Skinning/Animation3D) | ≈200KB |
|
|
26
|
+
| 物理系统(不需要时) | ≈150KB |
|
|
27
|
+
| 粒子系统(不需要时) | ≈100KB |
|
|
28
|
+
| 地形系统 | ≈50KB |
|
|
29
|
+
| WebSocket/SocketIO | ≈30KB |
|
|
30
|
+
| 原生渲染器 | ≈100KB |
|
|
31
|
+
| DragonBones/Spine | ≈80KB |
|
|
32
|
+
|
|
33
|
+
### 2.2 资源压缩
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
图片压缩优先级:
|
|
37
|
+
1. 使用 TinyPNG/Squoosh 压缩 PNG → 通常减少 60-80%
|
|
38
|
+
2. 考虑 JPEG 替代透明度不重要的大图
|
|
39
|
+
3. 使用图集合并碎图(减少 draw call + JSON 合并)
|
|
40
|
+
4. 降低分辨率:2048→1024 或 1024→512
|
|
41
|
+
5. 使用 2 的幂次方尺寸(GPU 友好)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2.3 代码优化
|
|
45
|
+
|
|
46
|
+
```typescript
|
|
47
|
+
// ✅ 使用 import 按需导入(tree-shaking 友好)
|
|
48
|
+
import { _decorator, Component, tween, Vec3 } from 'cc';
|
|
49
|
+
|
|
50
|
+
// ❌ 避免导入不使用的模块
|
|
51
|
+
// import * as cc from 'cc';
|
|
52
|
+
|
|
53
|
+
// ✅ 使用 const enum 替代 Enum(编译后内联为数值)
|
|
54
|
+
const enum GameState {
|
|
55
|
+
Idle = 0,
|
|
56
|
+
Playing = 1,
|
|
57
|
+
GameOver = 2,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ✅ 使用 DEBUG 宏排除调试代码
|
|
61
|
+
import { DEBUG } from 'cc/env';
|
|
62
|
+
if (DEBUG) {
|
|
63
|
+
console.log('debug info');
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 2.4 构建配置
|
|
68
|
+
|
|
69
|
+
构建发布面板关键设置:
|
|
70
|
+
- **压缩纹理**: 启用,选择合适格式(WebP/ASTC/ETC2)
|
|
71
|
+
- **MD5 Cache**: 可试玩广告不需要,关闭
|
|
72
|
+
- **内联所有 SpriteFrame**: 启用
|
|
73
|
+
- **合并所有 JSON**: 启用(Asset Bundle 压缩类型选"合并所有 JSON")
|
|
74
|
+
- **勾选 Zip 压缩**: 根据平台(Web 可不用)
|
|
75
|
+
- **Source Maps**: 禁用
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## 3. 性能优化策略
|
|
80
|
+
|
|
81
|
+
### 3.1 Draw Call 优化
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
// 1. 使用图集(SpriteAtlas)合并同材质精灵
|
|
85
|
+
// 2. 使用自动图集功能
|
|
86
|
+
|
|
87
|
+
// 3. 手动管理合批
|
|
88
|
+
// 同一图集 + 同一材质 + 相邻渲染顺序 = 自动合批
|
|
89
|
+
|
|
90
|
+
// 4. 避免打断合批的操作:
|
|
91
|
+
// - 不同材质/纹理的穿插
|
|
92
|
+
// - 使用 Mask 组件
|
|
93
|
+
// - 修改渲染状态
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 3.2 内存优化
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
// 1. 对象池减少 GC
|
|
100
|
+
import { NodePool, instantiate, Prefab } from 'cc';
|
|
101
|
+
|
|
102
|
+
class BulletPool {
|
|
103
|
+
private _pool = new NodePool('Bullet');
|
|
104
|
+
private _prefab: Prefab | null = null;
|
|
105
|
+
|
|
106
|
+
public init(prefab: Prefab): void {
|
|
107
|
+
this._prefab = prefab;
|
|
108
|
+
// 预热
|
|
109
|
+
for (let i = 0; i < 10; i++) {
|
|
110
|
+
const node = instantiate(prefab);
|
|
111
|
+
this._pool.put(node);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public get(): Node {
|
|
116
|
+
if (this._pool.size() > 0) {
|
|
117
|
+
return this._pool.get()!;
|
|
118
|
+
}
|
|
119
|
+
return instantiate(this._prefab!);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public put(node: Node): void {
|
|
123
|
+
this._pool.put(node);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public clear(): void {
|
|
127
|
+
this._pool.clear();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 2. 避免 update 中创建临时对象
|
|
132
|
+
// ❌
|
|
133
|
+
protected update(dt: number): void {
|
|
134
|
+
const pos = new Vec3(this.node.position); // 每帧创建
|
|
135
|
+
}
|
|
136
|
+
// ✅
|
|
137
|
+
private _tempVec3 = new Vec3();
|
|
138
|
+
protected update(dt: number): void {
|
|
139
|
+
Vec3.copy(this._tempVec3, this.node.position);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 3.3 渲染优化
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// 1. 减少透明节点数量
|
|
147
|
+
// 2. 合理使用 Canvas 分层
|
|
148
|
+
// 3. 避免频繁更新 Label(使用 BMFont 替代动态字体)
|
|
149
|
+
// 4. 使用 Cache Mode = CHAR 减少 Label 纹理
|
|
150
|
+
|
|
151
|
+
// 5. 控制节点树深度,减少递归遍历
|
|
152
|
+
// 6. 隐藏不可见节点(active = false 比 opacity = 0 更高效)
|
|
153
|
+
node.active = false; // ✅ 跳过渲染和更新
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 3.4 逻辑优化
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// 1. 减少 find/getComponent 调用频率
|
|
160
|
+
// ❌ 每帧查找
|
|
161
|
+
update() {
|
|
162
|
+
const sprite = find('Canvas/Player')?.getComponent(Sprite);
|
|
163
|
+
}
|
|
164
|
+
// ✅ 缓存引用
|
|
165
|
+
private _sprite: Sprite | null = null;
|
|
166
|
+
start() {
|
|
167
|
+
this._sprite = find('Canvas/Player')?.getComponent(Sprite) ?? null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 2. 使用 schedule 替代 update 中的低频逻辑
|
|
171
|
+
start() {
|
|
172
|
+
this.schedule(this.checkEnemies, 0.5); // 每 0.5 秒检查一次
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 3. 使用位运算优化频繁计算
|
|
176
|
+
const LAYER_ENEMY = 1 << 0;
|
|
177
|
+
const LAYER_BULLET = 1 << 1;
|
|
178
|
+
function canCollide(a: number, b: number): boolean {
|
|
179
|
+
return (a & b) !== 0;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 4. 可试玩广告特定优化
|
|
186
|
+
|
|
187
|
+
### 4.1 启动优化
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// 1. 首屏资源最小化
|
|
191
|
+
// 只加载第一帧需要的资源,其余异步加载
|
|
192
|
+
|
|
193
|
+
// 2. 使用加载界面遮罩异步加载
|
|
194
|
+
@ccclass('LoadingScreen')
|
|
195
|
+
export class LoadingScreen extends Component {
|
|
196
|
+
protected start(): void {
|
|
197
|
+
const tasks = [
|
|
198
|
+
this.loadPrefabs(),
|
|
199
|
+
this.loadAudio(),
|
|
200
|
+
this.loadTextures(),
|
|
201
|
+
];
|
|
202
|
+
Promise.all(tasks).then(() => {
|
|
203
|
+
this.startGame();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private loadPrefabs(): Promise<void> {
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
resources.loadDir('prefabs', Prefab, (err, prefabs) => {
|
|
210
|
+
resolve();
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 4.2 CTA (Call To Action)
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
// 安装按钮跳转
|
|
221
|
+
function gotoStore() {
|
|
222
|
+
const url = 'https://play.google.com/store/apps/details?id=com.example.game';
|
|
223
|
+
if (typeof window !== 'undefined') {
|
|
224
|
+
// 广告 SDK 提供的跳转方法
|
|
225
|
+
if ((window as any).mraid) {
|
|
226
|
+
(window as any).mraid.open(url);
|
|
227
|
+
} else {
|
|
228
|
+
window.open(url);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 4.3 包体检查清单
|
|
235
|
+
|
|
236
|
+
- [ ] 引擎模块裁剪完成
|
|
237
|
+
- [ ] 图片全部压缩(TinyPNG/WebP)
|
|
238
|
+
- [ ] 音频格式优化(MP3 CBR 64kbps 或更低)
|
|
239
|
+
- [ ] 删除未使用的资源
|
|
240
|
+
- [ ] DEBUG 代码通过宏排除
|
|
241
|
+
- [ ] console.log 在构建版本中移除
|
|
242
|
+
- [ ] Source Maps 关闭
|
|
243
|
+
- [ ] JSON 合并启用
|
|
244
|
+
- [ ] 最终 HTML 文件大小 ≤ 目标限制
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 5. 平台特定注意事项
|
|
249
|
+
|
|
250
|
+
| 平台 | 包体限制 | 特殊要求 |
|
|
251
|
+
|------|---------|---------|
|
|
252
|
+
| Facebook | 2MB (HTML) / 5MB (ZIP) | 需要 MRAID 合规 |
|
|
253
|
+
| Google Ads | 5MB (ZIP) | 支持多文件 |
|
|
254
|
+
| Unity Ads | 5MB | 单 HTML 文件 |
|
|
255
|
+
| IronSource | 5MB | 单 HTML 文件 |
|
|
256
|
+
| AppLovin | 5MB | 支持 MRAID |
|
|
257
|
+
| TikTok | 2MB (单文件) / 5MB (多文件) | 需要 SDK 回调 |
|