@mclawnet/agent 0.6.20 → 0.6.22
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/cli.js +63 -0
- package/dist/__tests__/checkpoint.test.d.ts +2 -0
- package/dist/__tests__/checkpoint.test.d.ts.map +1 -0
- package/dist/__tests__/fs-handler-decode.test.d.ts +2 -0
- package/dist/__tests__/fs-handler-decode.test.d.ts.map +1 -0
- package/dist/__tests__/idle-sweeper.test.d.ts +2 -0
- package/dist/__tests__/idle-sweeper.test.d.ts.map +1 -0
- package/dist/__tests__/mcp-config.test.d.ts +2 -0
- package/dist/__tests__/mcp-config.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-runtime-spawn.test.d.ts +2 -0
- package/dist/__tests__/schedule-runtime-spawn.test.d.ts.map +1 -0
- package/dist/__tests__/schedule-runtime.test.d.ts +2 -0
- package/dist/__tests__/schedule-runtime.test.d.ts.map +1 -0
- package/dist/__tests__/session-limit.test.d.ts +2 -0
- package/dist/__tests__/session-limit.test.d.ts.map +1 -0
- package/dist/__tests__/swarm-cli-client.test.d.ts +2 -0
- package/dist/__tests__/swarm-cli-client.test.d.ts.map +1 -0
- package/dist/__tests__/swarm-control-dispatch.test.d.ts +2 -0
- package/dist/__tests__/swarm-control-dispatch.test.d.ts.map +1 -0
- package/dist/__tests__/swarm-session-bridge.test.d.ts +2 -0
- package/dist/__tests__/swarm-session-bridge.test.d.ts.map +1 -0
- package/dist/backend-adapter.d.ts +43 -0
- package/dist/backend-adapter.d.ts.map +1 -1
- package/dist/checkpoint.d.ts +67 -0
- package/dist/checkpoint.d.ts.map +1 -0
- package/dist/{chunk-RIK7IXSW.js → chunk-WJWCYGLQ.js} +1130 -147
- package/dist/chunk-WJWCYGLQ.js.map +1 -0
- package/dist/errors.d.ts +40 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/fs-handler.d.ts.map +1 -1
- package/dist/hub-connection.d.ts +13 -0
- package/dist/hub-connection.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/schedule-runtime.d.ts +125 -0
- package/dist/schedule-runtime.d.ts.map +1 -0
- package/dist/session-manager.d.ts +102 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/skill-loader.d.ts +20 -0
- package/dist/skill-loader.d.ts.map +1 -1
- package/dist/start.d.ts +2 -0
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +1 -1
- package/dist/swarm-cli-client.d.ts +24 -0
- package/dist/swarm-cli-client.d.ts.map +1 -0
- package/dist/swarm-cli-client.js +83 -0
- package/dist/swarm-cli-client.js.map +1 -0
- package/dist/swarm-control-dispatch.d.ts +47 -0
- package/dist/swarm-control-dispatch.d.ts.map +1 -0
- package/dist/swarm-session-bridge.d.ts +22 -0
- package/dist/swarm-session-bridge.d.ts.map +1 -0
- package/package.json +6 -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-RIK7IXSW.js.map +0 -1
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
# Playable Ads Performance Optimization
|
|
2
|
+
|
|
3
|
+
## DrawCall Batching (Critical for Playables)
|
|
4
|
+
|
|
5
|
+
**Target: <10 DrawCalls for smooth 60fps playables**
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { _decorator, Component, Sprite, SpriteAtlas, SpriteFrame } from 'cc';
|
|
9
|
+
const { ccclass, property } = _decorator;
|
|
10
|
+
|
|
11
|
+
@ccclass('SpriteAtlasManager')
|
|
12
|
+
export class SpriteAtlasManager extends Component {
|
|
13
|
+
// ✅ EXCELLENT: Use sprite atlas for DrawCall batching
|
|
14
|
+
@property(SpriteAtlas)
|
|
15
|
+
private readonly characterAtlas: SpriteAtlas | null = null;
|
|
16
|
+
|
|
17
|
+
@property(SpriteAtlas)
|
|
18
|
+
private readonly uiAtlas: SpriteAtlas | null = null;
|
|
19
|
+
|
|
20
|
+
private readonly spriteFrameCache: Map<string, SpriteFrame> = new Map();
|
|
21
|
+
|
|
22
|
+
protected onLoad(): void {
|
|
23
|
+
if (!this.characterAtlas || !this.uiAtlas) {
|
|
24
|
+
throw new Error('SpriteAtlasManager: atlases are required');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ✅ EXCELLENT: Prewarm sprite frames from atlas
|
|
28
|
+
this.prewarmAtlas(this.characterAtlas, 'character');
|
|
29
|
+
this.prewarmAtlas(this.uiAtlas, 'ui');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private prewarmAtlas(atlas: SpriteAtlas, prefix: string): void {
|
|
33
|
+
const spriteFrames = atlas.getSpriteFrames();
|
|
34
|
+
for (const frame of spriteFrames) {
|
|
35
|
+
const key = `${prefix}_${frame.name}`;
|
|
36
|
+
this.spriteFrameCache.set(key, frame);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ✅ EXCELLENT: Get sprite frame from cache (batched in same DrawCall)
|
|
41
|
+
public getSpriteFrame(atlasName: string, frameName: string): SpriteFrame | null {
|
|
42
|
+
const key = `${atlasName}_${frameName}`;
|
|
43
|
+
return this.spriteFrameCache.get(key) ?? null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Usage: All sprites from same atlas = single DrawCall
|
|
48
|
+
@ccclass('CharacterSprite')
|
|
49
|
+
export class CharacterSprite extends Component {
|
|
50
|
+
@property(Sprite)
|
|
51
|
+
private readonly sprite: Sprite | null = null;
|
|
52
|
+
|
|
53
|
+
private atlasManager!: SpriteAtlasManager;
|
|
54
|
+
|
|
55
|
+
protected start(): void {
|
|
56
|
+
const manager = this.node.parent?.getComponent(SpriteAtlasManager);
|
|
57
|
+
if (!manager) throw new Error('SpriteAtlasManager not found');
|
|
58
|
+
this.atlasManager = manager;
|
|
59
|
+
|
|
60
|
+
// ✅ GOOD: Set sprite frame from atlas (batched)
|
|
61
|
+
const frame = this.atlasManager.getSpriteFrame('character', 'idle_01');
|
|
62
|
+
if (frame && this.sprite) {
|
|
63
|
+
this.sprite.spriteFrame = frame;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ❌ WRONG: Individual textures (multiple DrawCalls)
|
|
69
|
+
@property(SpriteFrame)
|
|
70
|
+
private characterIdleFrame: SpriteFrame | null = null; // DrawCall 1
|
|
71
|
+
|
|
72
|
+
@property(SpriteFrame)
|
|
73
|
+
private characterWalkFrame: SpriteFrame | null = null; // DrawCall 2
|
|
74
|
+
|
|
75
|
+
@property(SpriteFrame)
|
|
76
|
+
private characterJumpFrame: SpriteFrame | null = null; // DrawCall 3
|
|
77
|
+
// Result: 3 DrawCalls for 3 sprites!
|
|
78
|
+
|
|
79
|
+
// ✅ BETTER: Single atlas
|
|
80
|
+
@property(SpriteAtlas)
|
|
81
|
+
private characterAtlas: SpriteAtlas | null = null; // DrawCall 1 for all frames
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## GPU Skinning (Skeletal Animations)
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
import { _decorator, Component, SkeletalAnimation } from 'cc';
|
|
88
|
+
const { ccclass, property } = _decorator;
|
|
89
|
+
|
|
90
|
+
@ccclass('AnimationController')
|
|
91
|
+
export class AnimationController extends Component {
|
|
92
|
+
@property(SkeletalAnimation)
|
|
93
|
+
private readonly skeleton: SkeletalAnimation | null = null;
|
|
94
|
+
|
|
95
|
+
protected onLoad(): void {
|
|
96
|
+
if (!this.skeleton) {
|
|
97
|
+
throw new Error('AnimationController: skeleton is required');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ✅ EXCELLENT: Enable GPU skinning for better performance
|
|
101
|
+
// GPU handles bone transformations instead of CPU
|
|
102
|
+
this.skeleton.useBakedAnimation = true; // Baked animation data
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public playAnimation(animName: string, loop: boolean = false): void {
|
|
106
|
+
if (!this.skeleton) return;
|
|
107
|
+
|
|
108
|
+
const state = this.skeleton.getState(animName);
|
|
109
|
+
if (state) {
|
|
110
|
+
state.wrapMode = loop ? SkeletalAnimation.WrapMode.Loop : SkeletalAnimation.WrapMode.Normal;
|
|
111
|
+
this.skeleton.play(animName);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ❌ WRONG: CPU skinning (default, slower)
|
|
117
|
+
// Don't set useBakedAnimation to false for playables
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Object Pooling for Playables
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { _decorator, Component, Node, Prefab, instantiate, NodePool } from 'cc';
|
|
124
|
+
const { ccclass, property } = _decorator;
|
|
125
|
+
|
|
126
|
+
@ccclass('PlayableObjectPool')
|
|
127
|
+
export class PlayableObjectPool extends Component {
|
|
128
|
+
@property(Prefab)
|
|
129
|
+
private readonly bulletPrefab: Prefab | null = null;
|
|
130
|
+
|
|
131
|
+
@property(Prefab)
|
|
132
|
+
private readonly effectPrefab: Prefab | null = null;
|
|
133
|
+
|
|
134
|
+
private readonly bulletPool: NodePool = new NodePool();
|
|
135
|
+
private readonly effectPool: NodePool = new NodePool();
|
|
136
|
+
private static readonly PREWARM_COUNT: number = 20;
|
|
137
|
+
|
|
138
|
+
// ✅ EXCELLENT: Prewarm pools to avoid allocations during gameplay
|
|
139
|
+
protected onLoad(): void {
|
|
140
|
+
if (!this.bulletPrefab || !this.effectPrefab) {
|
|
141
|
+
throw new Error('PlayableObjectPool: prefabs are required');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Prewarm bullet pool
|
|
145
|
+
for (let i = 0; i < PlayableObjectPool.PREWARM_COUNT; i++) {
|
|
146
|
+
const bullet = instantiate(this.bulletPrefab);
|
|
147
|
+
this.bulletPool.put(bullet);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Prewarm effect pool
|
|
151
|
+
for (let i = 0; i < PlayableObjectPool.PREWARM_COUNT; i++) {
|
|
152
|
+
const effect = instantiate(this.effectPrefab);
|
|
153
|
+
this.effectPool.put(effect);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ✅ EXCELLENT: Get from pool (zero allocations in gameplay)
|
|
158
|
+
public getBullet(): Node {
|
|
159
|
+
if (this.bulletPool.size() > 0) {
|
|
160
|
+
const bullet = this.bulletPool.get()!;
|
|
161
|
+
bullet.active = true;
|
|
162
|
+
return bullet;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Fallback: create new (should be rare if prewarmed correctly)
|
|
166
|
+
if (!this.bulletPrefab) {
|
|
167
|
+
throw new Error('bulletPrefab is null');
|
|
168
|
+
}
|
|
169
|
+
return instantiate(this.bulletPrefab);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
public returnBullet(bullet: Node): void {
|
|
173
|
+
bullet.active = false;
|
|
174
|
+
this.bulletPool.put(bullet);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
protected onDestroy(): void {
|
|
178
|
+
this.bulletPool.clear();
|
|
179
|
+
this.effectPool.clear();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ❌ WRONG: Creating/destroying objects during gameplay
|
|
184
|
+
public shoot(): void {
|
|
185
|
+
const bullet = instantiate(this.bulletPrefab!); // Allocates every time
|
|
186
|
+
this.scheduleOnce(() => {
|
|
187
|
+
bullet.destroy(); // Triggers GC
|
|
188
|
+
}, 2.0);
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Update Loop Optimization for Playables
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
import { _decorator, Component, Node, Vec3 } from 'cc';
|
|
196
|
+
const { ccclass, property } = _decorator;
|
|
197
|
+
|
|
198
|
+
@ccclass('OptimizedUpdate')
|
|
199
|
+
export class OptimizedUpdate extends Component {
|
|
200
|
+
@property([Node])
|
|
201
|
+
private readonly enemies: Node[] = [];
|
|
202
|
+
|
|
203
|
+
// ✅ EXCELLENT: Preallocate to avoid allocations in update
|
|
204
|
+
private readonly tempVec3: Vec3 = new Vec3();
|
|
205
|
+
private readonly activeEnemies: Node[] = [];
|
|
206
|
+
private activeEnemiesDirty: boolean = true;
|
|
207
|
+
private frameCount: number = 0;
|
|
208
|
+
|
|
209
|
+
// ✅ EXCELLENT: Update expensive operations every N frames
|
|
210
|
+
protected update(dt: number): void {
|
|
211
|
+
this.frameCount++;
|
|
212
|
+
|
|
213
|
+
// Cheap operations: every frame
|
|
214
|
+
this.updateMovement(dt);
|
|
215
|
+
|
|
216
|
+
// Expensive operations: every 10 frames (6 times/second at 60fps)
|
|
217
|
+
if (this.frameCount % 10 === 0) {
|
|
218
|
+
this.updateAI();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Very expensive: every 60 frames (once per second at 60fps)
|
|
222
|
+
if (this.frameCount % 60 === 0) {
|
|
223
|
+
this.updatePathfinding();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
private updateMovement(dt: number): void {
|
|
228
|
+
// Use cached active enemies list
|
|
229
|
+
const activeEnemies = this.getActiveEnemies();
|
|
230
|
+
|
|
231
|
+
for (const enemy of activeEnemies) {
|
|
232
|
+
// Reuse preallocated vector
|
|
233
|
+
enemy.getPosition(this.tempVec3);
|
|
234
|
+
this.tempVec3.y += 10 * dt;
|
|
235
|
+
enemy.setPosition(this.tempVec3);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private getActiveEnemies(): Node[] {
|
|
240
|
+
if (this.activeEnemiesDirty) {
|
|
241
|
+
this.activeEnemies.length = 0;
|
|
242
|
+
for (const enemy of this.enemies) {
|
|
243
|
+
if (enemy.active) {
|
|
244
|
+
this.activeEnemies.push(enemy);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
this.activeEnemiesDirty = false;
|
|
248
|
+
}
|
|
249
|
+
return this.activeEnemies;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private updateAI(): void {
|
|
253
|
+
// Expensive AI logic
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private updatePathfinding(): void {
|
|
257
|
+
// Very expensive pathfinding
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ❌ WRONG: All logic in update, allocations everywhere
|
|
262
|
+
protected update(dt: number): void {
|
|
263
|
+
// Allocates array every frame
|
|
264
|
+
const activeEnemies = this.enemies.filter(e => e.active);
|
|
265
|
+
|
|
266
|
+
for (const enemy of activeEnemies) {
|
|
267
|
+
// Allocates vector every frame
|
|
268
|
+
const pos = enemy.position.clone();
|
|
269
|
+
pos.y += 10 * dt;
|
|
270
|
+
enemy.setPosition(pos);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Expensive operations every frame
|
|
274
|
+
this.updatePathfinding(); // 60 times/second!
|
|
275
|
+
this.updateAI(); // 60 times/second!
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Resource Loading and Preloading
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
import { _decorator, Component, resources, SpriteFrame, AudioClip } from 'cc';
|
|
283
|
+
const { ccclass } = _decorator;
|
|
284
|
+
|
|
285
|
+
@ccclass('ResourcePreloader')
|
|
286
|
+
export class ResourcePreloader extends Component {
|
|
287
|
+
private readonly loadedResources: Map<string, Asset> = new Map();
|
|
288
|
+
|
|
289
|
+
// ✅ EXCELLENT: Preload all resources at game start
|
|
290
|
+
protected async start(): Promise<void> {
|
|
291
|
+
await this.preloadAllResources();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private async preloadAllResources(): Promise<void> {
|
|
295
|
+
const resourcePaths = [
|
|
296
|
+
'sprites/character',
|
|
297
|
+
'sprites/enemies',
|
|
298
|
+
'audio/bgm',
|
|
299
|
+
'audio/sfx',
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
const promises = resourcePaths.map(path => this.preloadResource(path));
|
|
303
|
+
await Promise.all(promises);
|
|
304
|
+
|
|
305
|
+
console.log('All resources preloaded');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private async preloadResource(path: string): Promise<void> {
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
resources.load(path, (err, asset) => {
|
|
311
|
+
if (err) {
|
|
312
|
+
console.error(`Failed to load ${path}:`, err);
|
|
313
|
+
reject(err);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.loadedResources.set(path, asset);
|
|
318
|
+
resolve();
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
public getResource<T extends Asset>(path: string): T | null {
|
|
324
|
+
return (this.loadedResources.get(path) as T) ?? null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
protected onDestroy(): void {
|
|
328
|
+
// ✅ EXCELLENT: Release all loaded resources
|
|
329
|
+
for (const [path, asset] of this.loadedResources) {
|
|
330
|
+
asset.decRef();
|
|
331
|
+
}
|
|
332
|
+
this.loadedResources.clear();
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ❌ WRONG: Loading resources during gameplay
|
|
337
|
+
protected update(dt: number): void {
|
|
338
|
+
if (this.shouldSpawnEnemy()) {
|
|
339
|
+
// Loading during gameplay causes frame drops!
|
|
340
|
+
resources.load('sprites/enemy', SpriteFrame, (err, sprite) => {
|
|
341
|
+
this.spawnEnemy(sprite);
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ✅ BETTER: Preload and reuse
|
|
347
|
+
protected start(): void {
|
|
348
|
+
resources.load('sprites/enemy', SpriteFrame, (err, sprite) => {
|
|
349
|
+
this.enemySprite = sprite;
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
protected update(dt: number): void {
|
|
354
|
+
if (this.shouldSpawnEnemy() && this.enemySprite) {
|
|
355
|
+
this.spawnEnemy(this.enemySprite); // Instant, no loading
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Memory Management for Playables
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { _decorator, Component, Node } from 'cc';
|
|
364
|
+
const { ccclass } = _decorator;
|
|
365
|
+
|
|
366
|
+
@ccclass('MemoryOptimized')
|
|
367
|
+
export class MemoryOptimized extends Component {
|
|
368
|
+
// ✅ EXCELLENT: Use typed arrays for large datasets
|
|
369
|
+
private positions: Float32Array = new Float32Array(300); // 100 Vec3s
|
|
370
|
+
private velocities: Float32Array = new Float32Array(300);
|
|
371
|
+
|
|
372
|
+
// ✅ EXCELLENT: Reuse arrays instead of creating new ones
|
|
373
|
+
private readonly tempArray: number[] = [];
|
|
374
|
+
|
|
375
|
+
protected update(dt: number): void {
|
|
376
|
+
// Reuse array, don't allocate
|
|
377
|
+
this.tempArray.length = 0;
|
|
378
|
+
|
|
379
|
+
for (let i = 0; i < 100; i++) {
|
|
380
|
+
this.tempArray.push(i * dt);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ✅ EXCELLENT: WeakMap for caches (automatic cleanup)
|
|
385
|
+
private readonly nodeCache: WeakMap<Node, CachedData> = new WeakMap();
|
|
386
|
+
|
|
387
|
+
public getCachedData(node: Node): CachedData | undefined {
|
|
388
|
+
return this.nodeCache.get(node);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
protected onDestroy(): void {
|
|
392
|
+
// ✅ EXCELLENT: Clear references
|
|
393
|
+
this.tempArray.length = 0;
|
|
394
|
+
// WeakMap entries are auto-cleared when nodes are destroyed
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
## Summary: Playable Optimization Checklist
|
|
400
|
+
|
|
401
|
+
**DrawCall Batching (<10 target):**
|
|
402
|
+
- [ ] Use sprite atlases for all sprites (not individual textures)
|
|
403
|
+
- [ ] Prewarm sprite frame cache in onLoad()
|
|
404
|
+
- [ ] Group UI elements into single atlas
|
|
405
|
+
- [ ] Use same material for similar objects
|
|
406
|
+
|
|
407
|
+
**Animation Performance:**
|
|
408
|
+
- [ ] Enable GPU skinning (useBakedAnimation = true)
|
|
409
|
+
- [ ] Bake skeletal animations
|
|
410
|
+
- [ ] Limit simultaneous animations
|
|
411
|
+
|
|
412
|
+
**Object Pooling:**
|
|
413
|
+
- [ ] Pool bullets, effects, enemies (anything spawned frequently)
|
|
414
|
+
- [ ] Prewarm pools in onLoad() (at least 20 objects)
|
|
415
|
+
- [ ] Never instantiate/destroy during gameplay
|
|
416
|
+
|
|
417
|
+
**Update Loop:**
|
|
418
|
+
- [ ] Zero allocations in update()
|
|
419
|
+
- [ ] Throttle expensive operations (every 10-60 frames)
|
|
420
|
+
- [ ] Cache active object lists
|
|
421
|
+
- [ ] Reuse preallocated vectors/arrays
|
|
422
|
+
|
|
423
|
+
**Resource Management:**
|
|
424
|
+
- [ ] Preload all resources at game start
|
|
425
|
+
- [ ] Never load resources during gameplay
|
|
426
|
+
- [ ] Release resources in onDestroy()
|
|
427
|
+
- [ ] Use WeakMap for auto-cleanup caches
|
|
428
|
+
|
|
429
|
+
**Target: 60fps with <10 DrawCalls and <5MB bundle size for playable ads.**
|