theone-cocos-standards
Enforces TheOne Studio Cocos Creator development standards including TypeScript coding patterns, Cocos Creator 3.x architecture (Component system, EventDispatcher), and playable ads optimization guidelines. Triggers when writing, reviewing, or refactoring Cocos TypeScript code, implementing playable ads features, optimizing performance/bundle size, or reviewing code changes.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install the1studio-theone-training-skills-theone-cocos-standards
Repository
Skill path: .claude/skills/theone-cocos-standards
Enforces TheOne Studio Cocos Creator development standards including TypeScript coding patterns, Cocos Creator 3.x architecture (Component system, EventDispatcher), and playable ads optimization guidelines. Triggers when writing, reviewing, or refactoring Cocos TypeScript code, implementing playable ads features, optimizing performance/bundle size, or reviewing code changes.
Open repositoryBest for
Primary workflow: Write Technical Docs.
Technical facets: Full Stack, Tech Writer.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: The1Studio.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install theone-cocos-standards into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/The1Studio/theone-training-skills before adding theone-cocos-standards to shared team environments
- Use theone-cocos-standards for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: theone-cocos-standards
description: Enforces TheOne Studio Cocos Creator development standards including TypeScript coding patterns, Cocos Creator 3.x architecture (Component system, EventDispatcher), and playable ads optimization guidelines. Triggers when writing, reviewing, or refactoring Cocos TypeScript code, implementing playable ads features, optimizing performance/bundle size, or reviewing code changes.
---
# TheOne Studio Cocos Creator Development Standards
⚠️ **Cocos Creator 3.x (TypeScript 4.1+):** All patterns and examples are compatible with Cocos Creator 3.x playable ads development.
## Skill Purpose
This skill enforces TheOne Studio's comprehensive Cocos Creator development standards with **CODE QUALITY FIRST**:
**Priority 1: Code Quality & Hygiene** (MOST IMPORTANT)
- TypeScript strict mode, ESLint configuration, access modifiers (public/private/protected)
- Throw exceptions (never silent errors)
- console.log for development, remove in production builds
- readonly for immutable fields, const for constants
- No inline comments (use descriptive names)
- Proper error handling and type safety
**Priority 2: Modern TypeScript Patterns**
- Array methods (map/filter/reduce) over loops
- Arrow functions, destructuring, spread operators
- Optional chaining, nullish coalescing
- Type guards, utility types (Partial, Required, Readonly)
- Modern TypeScript features
**Priority 3: Cocos Creator Architecture**
- Component-based Entity-Component (EC) system
- Lifecycle methods: onLoad→start→onEnable→update→onDisable→onDestroy
- EventDispatcher pattern for custom events
- Node event system (EventTouch, keyboard events)
- Resource management and pooling for playables
**Priority 4: Playable Ads Performance**
- DrawCall batching (<10 DrawCalls target)
- Sprite atlas configuration (auto-atlas enabled)
- GPU skinning for skeletal animations
- Zero allocations in update() loop
- Bundle size <5MB (texture compression, code minification)
## When This Skill Triggers
- Writing or refactoring Cocos Creator TypeScript code
- Implementing playable ads features
- Working with component lifecycle and events
- Optimizing performance for playable ads
- Reviewing code changes or pull requests
- Setting up playable project architecture
- Reducing bundle size or DrawCall counts
## Quick Reference Guide
### What Do You Need Help With?
| Priority | Task | Reference |
|----------|------|-----------|
| **🔴 PRIORITY 1: Code Quality (Check FIRST)** | | |
| 1 | TypeScript strict mode, ESLint, access modifiers | [Quality & Hygiene](references/language/quality-hygiene.md) ⭐ |
| 1 | Throw exceptions, proper error handling | [Quality & Hygiene](references/language/quality-hygiene.md) ⭐ |
| 1 | console.log (development only), remove in production | [Quality & Hygiene](references/language/quality-hygiene.md) ⭐ |
| 1 | readonly/const, no inline comments, descriptive names | [Quality & Hygiene](references/language/quality-hygiene.md) ⭐ |
| **🟡 PRIORITY 2: Modern TypeScript Patterns** | | |
| 2 | Array methods, arrow functions, destructuring | [Modern TypeScript](references/language/modern-typescript.md) |
| 2 | Optional chaining, nullish coalescing | [Modern TypeScript](references/language/modern-typescript.md) |
| 2 | Type guards, utility types | [Modern TypeScript](references/language/modern-typescript.md) |
| **🟢 PRIORITY 3: Cocos Architecture** | | |
| 3 | Component system, @property decorator | [Component System](references/framework/component-system.md) |
| 3 | Lifecycle methods (onLoad→start→update→onDestroy) | [Component System](references/framework/component-system.md) |
| 3 | EventDispatcher, Node events, cleanup | [Event Patterns](references/framework/event-patterns.md) |
| 3 | Resource loading, pooling, memory management | [Playable Optimization](references/framework/playable-optimization.md) |
| **🔵 PRIORITY 4: Performance & Review** | | |
| 4 | DrawCall batching, sprite atlas, GPU skinning | [Playable Optimization](references/framework/playable-optimization.md) |
| 4 | Update loop optimization, zero allocations | [Performance](references/language/performance.md) |
| 4 | Bundle size reduction (<5MB target) | [Size Optimization](references/framework/size-optimization.md) |
| 4 | Architecture review (components, lifecycle, events) | [Architecture Review](references/review/architecture-review.md) |
| 4 | TypeScript quality review | [Quality Review](references/review/quality-review.md) |
| 4 | Performance review (DrawCalls, allocations) | [Performance Review](references/review/performance-review.md) |
## 🔴 CRITICAL: Code Quality Rules (CHECK FIRST!)
### ⚠️ MANDATORY QUALITY STANDARDS
**ALWAYS enforce these BEFORE writing any code:**
1. **Enable TypeScript strict mode** - "strict": true in tsconfig.json
2. **Use ESLint configuration** - @typescript-eslint rules enabled
3. **Use access modifiers** - public/private/protected on all members
4. **Throw exceptions for errors** - NEVER silent failures or undefined returns
5. **console.log for development only** - Remove all console statements in production builds
6. **Use readonly for immutable fields** - Mark fields that aren't reassigned
7. **Use const for constants** - Constants should be const, not let
8. **No inline comments** - Use descriptive names; code should be self-explanatory
9. **Proper null/undefined handling** - Use optional chaining and nullish coalescing
10. **Type safety** - Avoid `any` type, use proper types and interfaces
**Example: Enforce Quality First**
```typescript
// ✅ EXCELLENT: All quality rules enforced
import { _decorator, Component, Node, EventTouch } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PlayerController')
export class PlayerController extends Component {
// 3. Access modifier, 6. readonly for immutable
@property(Node)
private readonly targetNode: Node | null = null;
// 7. const for constants
private static readonly MAX_HEALTH: number = 100;
private currentHealth: number = 100;
// Lifecycle: onLoad → start → onEnable
protected onLoad(): void {
// 4. Throw exception for errors
if (!this.targetNode) {
throw new Error('PlayerController: targetNode is not assigned');
}
// 9. Proper event listener setup
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
}
protected onDestroy(): void {
// 9. Always cleanup event listeners
this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
}
private onTouchStart(event: EventTouch): void {
// 5. console.log only for development (remove in production)
if (CC_DEBUG) {
console.log('Touch detected');
}
this.takeDamage(10);
}
// 8. Descriptive method names (no inline comments needed)
private takeDamage(amount: number): void {
this.currentHealth -= amount;
if (this.currentHealth <= 0) {
this.handlePlayerDeath();
}
}
private handlePlayerDeath(): void {
// Death logic
}
}
```
## ⚠️ Cocos Creator Architecture Rules (AFTER Quality)
### Component System Fundamentals
**Entity-Component (EC) System:**
- Components extend `Component` class
- Use `@ccclass` and `@property` decorators
- Lifecycle: onLoad → start → onEnable → update → lateUpdate → onDisable → onDestroy
**Execution Order:**
1. **onLoad()** - Component initialization, one-time setup
2. **start()** - After all components loaded, can reference other components
3. **onEnable()** - When component/node enabled (can be called multiple times)
4. **update(dt)** - Every frame (use sparingly for playables)
5. **lateUpdate(dt)** - After all update() calls
6. **onDisable()** - When component/node disabled
7. **onDestroy()** - Cleanup, remove listeners, release resources
**Universal Rules:**
- ✅ Initialize in onLoad(), reference other components in start()
- ✅ Register events in onEnable(), unregister in onDisable()
- ✅ Always cleanup listeners in onDestroy()
- ✅ Avoid heavy logic in update() (performance critical for playables)
- ✅ Use readonly for @property fields that shouldn't be reassigned
- ✅ Throw exceptions for missing required references
## Brief Examples
### 🔴 Code Quality First
```typescript
// ✅ EXCELLENT: Quality rules enforced
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('GameManager')
export class GameManager extends Component {
@property(Node)
private readonly playerNode: Node | null = null;
private static readonly MAX_SCORE: number = 1000;
private currentScore: number = 0;
protected onLoad(): void {
// Throw exception for missing required references
if (!this.playerNode) {
throw new Error('GameManager: playerNode is required');
}
if (CC_DEBUG) {
console.log('GameManager initialized'); // Development only
}
}
public addScore(points: number): void {
if (points <= 0) {
throw new Error('GameManager.addScore: points must be positive');
}
this.currentScore = Math.min(
this.currentScore + points,
GameManager.MAX_SCORE
);
}
}
```
### 🟡 Modern TypeScript Patterns
```typescript
// ✅ GOOD: Array methods instead of loops
const activeEnemies = allEnemies.filter(e => e.isActive);
const enemyPositions = activeEnemies.map(e => e.node.position);
// ✅ GOOD: Optional chaining and nullish coalescing
const playerName = player?.name ?? 'Unknown';
// ✅ GOOD: Destructuring
const { x, y } = this.node.position;
// ✅ GOOD: Arrow functions
this.enemies.forEach(enemy => enemy.takeDamage(10));
// ✅ GOOD: Type guards
function isPlayer(node: Node): node is PlayerNode {
return node.getComponent(PlayerController) !== null;
}
```
### 🟢 Cocos Creator Component Pattern
```typescript
import { _decorator, Component, Node, EventTouch, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('TouchHandler')
export class TouchHandler extends Component {
@property(Node)
private readonly targetNode: Node | null = null;
private readonly tempVec3: Vec3 = new Vec3(); // Reusable vector
// 1. onLoad: Initialize component
protected onLoad(): void {
if (!this.targetNode) {
throw new Error('TouchHandler: targetNode is required');
}
}
// 2. start: Reference other components (if needed)
protected start(): void {
// Can safely access other components here
}
// 3. onEnable: Register event listeners
protected onEnable(): void {
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
this.node.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
}
// 4. onDisable: Unregister event listeners
protected onDisable(): void {
this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
this.node.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
}
// 5. onDestroy: Final cleanup
protected onDestroy(): void {
// Release any additional resources
}
private onTouchStart(event: EventTouch): void {
// Handle touch
}
private onTouchMove(event: EventTouch): void {
// Reuse vector to avoid allocations
this.targetNode!.getPosition(this.tempVec3);
this.tempVec3.y += 10;
this.targetNode!.setPosition(this.tempVec3);
}
}
```
### 🟢 Event Dispatcher Pattern
```typescript
import { _decorator, Component, EventTarget } from 'cc';
const { ccclass } = _decorator;
// Custom event types
export enum GameEvent {
SCORE_CHANGED = 'score_changed',
LEVEL_COMPLETE = 'level_complete',
PLAYER_DIED = 'player_died',
}
export interface ScoreChangedEvent {
oldScore: number;
newScore: number;
}
@ccclass('EventManager')
export class EventManager extends Component {
private static instance: EventManager | null = null;
private readonly eventTarget: EventTarget = new EventTarget();
protected onLoad(): void {
if (EventManager.instance) {
throw new Error('EventManager: instance already exists');
}
EventManager.instance = this;
}
public static emit(event: GameEvent, data?: any): void {
if (!EventManager.instance) {
throw new Error('EventManager: instance not initialized');
}
EventManager.instance.eventTarget.emit(event, data);
}
public static on(event: GameEvent, callback: Function, target?: any): void {
if (!EventManager.instance) {
throw new Error('EventManager: instance not initialized');
}
EventManager.instance.eventTarget.on(event, callback, target);
}
public static off(event: GameEvent, callback: Function, target?: any): void {
if (!EventManager.instance) {
throw new Error('EventManager: instance not initialized');
}
EventManager.instance.eventTarget.off(event, callback, target);
}
}
// Usage in component
@ccclass('ScoreDisplay')
export class ScoreDisplay extends Component {
protected onEnable(): void {
EventManager.on(GameEvent.SCORE_CHANGED, this.onScoreChanged, this);
}
protected onDisable(): void {
EventManager.off(GameEvent.SCORE_CHANGED, this.onScoreChanged, this);
}
private onScoreChanged(data: ScoreChangedEvent): void {
console.log(`Score: ${data.oldScore} → ${data.newScore}`);
}
}
```
### 🔵 Playable Performance Optimization
```typescript
import { _decorator, Component, Node, Sprite, SpriteAtlas } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('OptimizedSpriteManager')
export class OptimizedSpriteManager extends Component {
// Use sprite atlas for DrawCall batching
@property(SpriteAtlas)
private readonly characterAtlas: SpriteAtlas | null = null;
// Preallocate arrays to avoid allocations in update()
private readonly tempNodes: Node[] = [];
private frameCount: number = 0;
protected onLoad(): void {
if (!this.characterAtlas) {
throw new Error('OptimizedSpriteManager: characterAtlas is required');
}
// Prewarm sprite frames from atlas
this.prewarmSpriteFrames();
}
private prewarmSpriteFrames(): void {
// Load all sprites from atlas (batched in single DrawCall)
const spriteFrame = this.characterAtlas!.getSpriteFrame('character_idle');
if (!spriteFrame) {
throw new Error('Sprite frame not found in atlas');
}
}
// Optimize update: avoid allocations, use object pooling
protected update(dt: number): void {
// Run expensive operations every N frames instead of every frame
this.frameCount++;
if (this.frameCount % 10 === 0) {
this.updateExpensiveOperation();
}
}
private updateExpensiveOperation(): void {
// Reuse array instead of creating new one
this.tempNodes.length = 0;
// Batch operations to reduce DrawCalls
}
}
```
## Code Review Checklist
### Quick Validation (before committing)
**🔴 Code Quality (CHECK FIRST):**
- [ ] TypeScript strict mode enabled in tsconfig.json
- [ ] ESLint rules passing (no errors)
- [ ] All access modifiers correct (public/private/protected)
- [ ] Exceptions thrown for errors (no silent failures)
- [ ] console.log removed or wrapped in CC_DEBUG
- [ ] readonly used for non-reassigned fields
- [ ] const used for constants
- [ ] No inline comments (self-explanatory code)
- [ ] Proper null/undefined handling
- [ ] No `any` types (use proper types)
**🟡 Modern TypeScript Patterns:**
- [ ] Array methods used instead of manual loops
- [ ] Arrow functions for callbacks
- [ ] Optional chaining (?.) for safe property access
- [ ] Nullish coalescing (??) for default values
- [ ] Destructuring for cleaner code
- [ ] Type guards for type narrowing
**🟢 Cocos Creator Architecture:**
- [ ] Component lifecycle methods in correct order
- [ ] onLoad() for initialization, start() for references
- [ ] Event listeners registered in onEnable()
- [ ] Event listeners unregistered in onDisable()
- [ ] Resources released in onDestroy()
- [ ] @property decorator used correctly
- [ ] Required references validated (throw if null)
**🔵 Playable Performance:**
- [ ] No allocations in update() loop
- [ ] Sprite atlas used for DrawCall batching
- [ ] GPU skinning enabled for skeletal animations
- [ ] Expensive operations throttled (not every frame)
- [ ] Object pooling for frequently created objects
- [ ] Texture compression enabled
- [ ] Bundle size <5MB target
- [ ] DrawCall count <10 target
## Common Mistakes to Avoid
### ❌ DON'T:
1. **Ignore TypeScript strict mode** → Enable "strict": true
2. **Silent error handling** → Throw exceptions for errors
3. **Leave console.log in production** → Remove or wrap in CC_DEBUG
4. **Skip access modifiers** → Use public/private/protected
5. **Use `any` type** → Define proper types and interfaces
6. **Add inline comments** → Use descriptive names instead
7. **Skip event cleanup** → Always unregister in onDisable/onDestroy
8. **Allocate in update()** → Preallocate and reuse objects
9. **Forget sprite atlas** → Use atlas for DrawCall batching
10. **Heavy logic in update()** → Throttle expensive operations
11. **Skip null checks** → Validate required references in onLoad
12. **Mutable @property fields** → Use readonly when appropriate
13. **Manual loops over arrays** → Use map/filter/reduce
14. **Ignore bundle size** → Monitor and optimize (<5MB target)
### ✅ DO:
1. **Enable TypeScript strict mode** ("strict": true)
2. **Throw exceptions for errors** (never silent failures)
3. **Use console.log for development only** (remove in production)
4. **Use access modifiers** (public/private/protected)
5. **Define proper types** (avoid `any`)
6. **Use descriptive names** (no inline comments)
7. **Always cleanup events** (onDisable/onDestroy)
8. **Preallocate objects** (reuse in update())
9. **Use sprite atlas** (DrawCall batching)
10. **Throttle expensive operations** (not every frame)
11. **Validate required references** (throw in onLoad if null)
12. **Use readonly for @property** (when appropriate)
13. **Use array methods** (map/filter/reduce)
14. **Monitor bundle size** (<5MB target for playables)
## Review Severity Levels
### 🔴 Critical (Must Fix)
- **TypeScript strict mode disabled** - Must enable "strict": true
- **Silent error handling** - Must throw exceptions for errors
- **console.log in production code** - Remove or wrap in CC_DEBUG
- **Missing access modifiers** - All members must have modifiers
- **Using `any` type without justification** - Define proper types
- **Inline comments instead of descriptive names** - Rename and remove comments
- **Event listeners not cleaned up** - Memory leak, must unregister
- **Missing required reference validation** - Must throw in onLoad if null
- **Allocations in update() loop** - Performance critical, must preallocate
- **No sprite atlas for multiple sprites** - DrawCall explosion, must use atlas
- **Bundle size >5MB** - Exceeds playable limit, must optimize
### 🟡 Important (Should Fix)
- **Missing readonly on @property fields** - Should be readonly when not reassigned
- **Missing const for constants** - Should use const instead of let
- **Manual loops instead of array methods** - Should use map/filter/reduce
- **Missing optional chaining** - Should use ?. for safe access
- **Missing nullish coalescing** - Should use ?? for default values
- **Heavy logic in update()** - Should throttle expensive operations
- **No object pooling for frequent allocations** - Should implement pooling
- **Texture compression not enabled** - Should enable for smaller bundle
- **DrawCall count >10** - Should optimize batching
### 🟢 Nice to Have (Suggestion)
- Could use arrow function for callback
- Could destructure for cleaner code
- Could use type guard for type safety
- Could improve naming for clarity
- Could add interface for better typing
- Could optimize algorithm for better performance
## Detailed References
### TypeScript Language Standards
- [Quality & Hygiene](references/language/quality-hygiene.md) - Strict mode, ESLint, access modifiers, error handling
- [Modern TypeScript](references/language/modern-typescript.md) - Array methods, optional chaining, type guards, utility types
- [Performance](references/language/performance.md) - Update loop optimization, zero allocations, caching
### Cocos Creator Framework
- [Component System](references/framework/component-system.md) - EC system, lifecycle methods, @property decorator
- [Event Patterns](references/framework/event-patterns.md) - EventDispatcher, Node events, subscription cleanup
- [Playable Optimization](references/framework/playable-optimization.md) - DrawCall batching, sprite atlas, GPU skinning, resource pooling
- [Size Optimization](references/framework/size-optimization.md) - Bundle size reduction, texture compression, build optimization
### Code Review
- [Architecture Review](references/review/architecture-review.md) - Component violations, lifecycle errors, event leaks
- [Quality Review](references/review/quality-review.md) - TypeScript quality issues, access modifiers, error handling
- [Performance Review](references/review/performance-review.md) - Playable-specific performance problems, DrawCalls, allocations
## Summary
This skill provides comprehensive Cocos Creator development standards for TheOne Studio's playable ads team:
- **TypeScript Excellence**: Strict mode, modern patterns, type safety
- **Cocos Architecture**: Component lifecycle, event patterns, resource management
- **Playable Performance**: DrawCall batching, GPU skinning, <5MB bundles
- **Code Quality**: Enforced quality, hygiene, and performance rules
Use the Quick Reference Guide above to navigate to the specific pattern you need.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/language/quality-hygiene.md
```markdown
# TypeScript Quality & Code Hygiene
## Enable TypeScript Strict Mode
```typescript
// ✅ GOOD: Enable strict mode in tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true
}
}
// Declare nullable explicitly
public playerName: string | null = null; // Can be null
public requiredName: string = ''; // Never null
// ❌ BAD: Ignoring nullability
public playerName: string; // Uninitialized, can be undefined
```
## Use Access Modifiers (public/private/protected)
```typescript
// ✅ GOOD: Explicit access modifiers
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('GameService')
export class GameService extends Component {
// Private implementation details
private readonly playerNodes: Node[] = [];
private currentLevel: number = 1;
// Protected for subclass access
protected readonly maxHealth: number = 100;
// Public API only when necessary
public getCurrentLevel(): number {
return this.currentLevel;
}
// Private helper methods
private loadGameData(): void {
// Implementation
}
}
// ❌ BAD: No access modifiers (implicitly public)
@ccclass('GameService')
export class GameService extends Component {
playerNodes: Node[] = []; // Implicitly public
currentLevel: number = 1; // Implicitly public
}
```
## Enable ESLint with TypeScript Support
```json
// ✅ GOOD: .eslintrc.json configuration
{
"parser": "@typescript-eslint/parser",
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"plugins": ["@typescript-eslint"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-member-accessibility": "error"
}
}
```
## Throw Exceptions for Errors
**Critical Rule**: Throw exceptions instead of silent failures or returning undefined.
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PlayerService')
export class PlayerService extends Component {
@property(Node)
private readonly playerNode: Node | null = null;
// ✅ EXCELLENT: Throw exception for errors
protected onLoad(): void {
if (!this.playerNode) {
throw new Error('PlayerService: playerNode is required');
}
}
public getPlayer(id: string): Player {
const player = this.players.get(id);
if (!player) {
// Throw exception, don't return undefined
throw new Error(`Player not found: ${id}`);
}
return player;
}
public loadLevel(levelId: number): void {
if (levelId < 1 || levelId > 100) {
throw new RangeError(`Invalid level ID: ${levelId}. Must be 1-100.`);
}
const levelData = this.loadLevelData(levelId);
if (!levelData) {
throw new Error(`Failed to load level data for level ${levelId}`);
}
this.initializeLevel(levelData);
}
}
// ❌ WRONG: Silent failure
public getPlayer(id: string): Player | undefined {
const player = this.players.get(id);
// Returning undefined - caller doesn't know why it failed
return player;
}
// ❌ WRONG: Logging error instead of throwing
public loadLevel(levelId: number): void {
if (levelId < 1) {
console.error('Invalid level ID'); // Don't just log
return; // Silent failure
}
}
```
## Logging: console.log for Development Only
**Logging Guidelines:**
- **console.log**: Use ONLY for development debugging
- **Remove in production**: Wrap in `CC_DEBUG` or remove entirely
- **Performance impact**: console.log can slow down playable ads
- **Bundle size**: Logging strings increase bundle size
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('GameManager')
export class GameManager extends Component {
private currentScore: number = 0;
// ✅ EXCELLENT: Conditional logging for development
protected onLoad(): void {
if (CC_DEBUG) {
console.log('GameManager initialized');
}
}
public addScore(points: number): void {
this.currentScore += points;
// ✅ GOOD: Development-only debug logging
if (CC_DEBUG) {
console.log(`Score updated: ${this.currentScore}`);
}
}
private loadGameData(): void {
try {
const data = this.fetchData();
this.processData(data);
} catch (error) {
// ✅ GOOD: Log errors in development
if (CC_DEBUG) {
console.error('Failed to load game data:', error);
}
// Always throw for caller to handle
throw error;
}
}
}
// ❌ WRONG: Unconditional console.log in production
public addScore(points: number): void {
console.log(`Adding ${points} points`); // Will be in production build
this.currentScore += points;
}
// ❌ WRONG: Verbose logging everywhere
public update(dt: number): void {
console.log('Update called'); // Called every frame!
console.log(`Delta time: ${dt}`); // Performance impact
}
// ✅ BETTER: Remove logs in production or use build-time removal
// Configure build process to strip console.log in production builds
```
**Production Build Configuration:**
```javascript
// Build configuration to remove console.log in production
// rollup.config.js or webpack.config.js
export default {
plugins: [
// Remove console statements in production
terser({
compress: {
drop_console: true, // Remove all console.* calls
pure_funcs: ['console.log', 'console.debug'], // Remove specific calls
}
})
]
};
```
## Use readonly for Immutable Fields
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PlayerController')
export class PlayerController extends Component {
// ✅ GOOD: readonly for @property fields that aren't reassigned
@property(Node)
private readonly targetNode: Node | null = null;
@property(Number)
private readonly moveSpeed: number = 100;
// ✅ GOOD: readonly for injected dependencies
private readonly eventManager: EventManager;
// Regular mutable field
private currentHealth: number = 100;
constructor(eventManager: EventManager) {
super();
this.eventManager = eventManager;
}
// ❌ WRONG: Can't reassign readonly field
public setTarget(node: Node): void {
// this.targetNode = node; // Error: Cannot assign to 'targetNode' because it is a read-only property
}
}
// ❌ BAD: Mutable when shouldn't be
@ccclass('GameConfig')
export class GameConfig extends Component {
@property(Number)
private maxHealth: number = 100; // Should be readonly
}
```
## Use const for Constants
```typescript
// ✅ GOOD: const for constants
const MAX_PLAYERS = 4;
const DEFAULT_PLAYER_NAME = 'Player';
const GAME_VERSION = '1.0.0';
// ✅ GOOD: Static readonly for class constants
@ccclass('GameRules')
export class GameRules extends Component {
private static readonly MAX_HEALTH: number = 100;
private static readonly MIN_LEVEL: number = 1;
private static readonly MAX_LEVEL: number = 50;
public static isValidLevel(level: number): boolean {
return level >= GameRules.MIN_LEVEL && level <= GameRules.MAX_LEVEL;
}
}
// ✅ GOOD: Enum for related constants
export enum GameState {
LOADING = 'loading',
PLAYING = 'playing',
PAUSED = 'paused',
GAME_OVER = 'game_over',
}
// ❌ BAD: let for constants
let maxPlayers = 4; // Should be const
let defaultPlayerName = 'Player'; // Should be const
// ❌ BAD: Magic numbers without constants
public checkHealth(): boolean {
return this.health > 0 && this.health <= 100; // What is 100?
}
// ✅ BETTER: Named constant
private static readonly MAX_HEALTH: number = 100;
public checkHealth(): boolean {
return this.health > 0 && this.health <= GameRules.MAX_HEALTH;
}
```
## No Inline Comments (Use Descriptive Names)
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
// ✅ EXCELLENT: Self-explanatory code, no inline comments
@ccclass('PlayerController')
export class PlayerController extends Component {
@property(Node)
private readonly healthBarNode: Node | null = null;
private currentHealth: number = 100;
private static readonly MAX_HEALTH: number = 100;
private static readonly CRITICAL_HEALTH_THRESHOLD: number = 20;
public takeDamage(amount: number): void {
this.currentHealth = Math.max(0, this.currentHealth - amount);
this.updateHealthBar();
if (this.isHealthCritical()) {
this.triggerLowHealthWarning();
}
if (this.isDead()) {
this.handlePlayerDeath();
}
}
private isHealthCritical(): boolean {
return this.currentHealth <= PlayerController.CRITICAL_HEALTH_THRESHOLD;
}
private isDead(): boolean {
return this.currentHealth === 0;
}
private triggerLowHealthWarning(): void {
// Implementation
}
private handlePlayerDeath(): void {
// Implementation
}
private updateHealthBar(): void {
if (!this.healthBarNode) return;
const healthPercentage = this.currentHealth / PlayerController.MAX_HEALTH;
this.healthBarNode.scale = new Vec3(healthPercentage, 1, 1);
}
}
// ❌ BAD: Inline comments explaining unclear code
@ccclass('PlayerController')
export class PlayerController extends Component {
private h: number = 100; // health
public td(a: number): void { // take damage
this.h = Math.max(0, this.h - a); // subtract damage but don't go below 0
this.uh(); // update health bar
if (this.h <= 20) { // if health is critical
this.tlhw(); // trigger low health warning
}
if (this.h === 0) { // if dead
this.hpd(); // handle player death
}
}
}
// ❌ BAD: Comments explaining what code does (should be obvious)
public addScore(points: number): void {
// Add points to current score
this.currentScore += points;
// Check if score exceeds maximum
if (this.currentScore > MAX_SCORE) {
// Set score to maximum
this.currentScore = MAX_SCORE;
}
}
// ✅ BETTER: Descriptive names make comments unnecessary
public addScore(points: number): void {
this.currentScore += points;
this.clampScoreToMaximum();
}
private clampScoreToMaximum(): void {
this.currentScore = Math.min(this.currentScore, MAX_SCORE);
}
```
**When Comments ARE Appropriate:**
```typescript
// ✅ GOOD: Documenting WHY, not WHAT
/**
* Calculates damage using quadratic formula to create smooth damage curve.
* Linear damage felt too harsh for new players during playtesting.
*/
private calculateDamage(baseAmount: number, level: number): number {
return baseAmount * Math.pow(level, 0.8);
}
// ✅ GOOD: Documenting complex algorithms
/**
* A* pathfinding algorithm implementation.
* Uses Manhattan distance heuristic for grid-based movement.
* @see https://en.wikipedia.org/wiki/A*_search_algorithm
*/
private findPath(start: Vec2, end: Vec2): Vec2[] {
// Implementation
}
// ✅ GOOD: Documenting workarounds
/**
* WORKAROUND: Cocos Creator 3.8.x has a bug where sprite atlas
* frames aren't properly loaded on first access. Accessing once
* in onLoad() ensures they're cached for later use.
* @see https://github.com/cocos/cocos-engine/issues/12345
*/
protected onLoad(): void {
this.atlas?.getSpriteFrame('dummy');
}
```
## Proper Null/Undefined Handling
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PlayerManager')
export class PlayerManager extends Component {
@property(Node)
private readonly playerNode: Node | null = null;
// ✅ EXCELLENT: Explicit validation and error handling
protected onLoad(): void {
if (!this.playerNode) {
throw new Error('PlayerManager: playerNode is required');
}
}
// ✅ GOOD: Optional chaining for safe access
public getPlayerName(): string {
return this.playerNode?.name ?? 'Unknown';
}
// ✅ GOOD: Nullish coalescing for default values
public getPlayerHealth(): number {
return this.playerNode?.getComponent(PlayerController)?.health ?? 0;
}
// ✅ GOOD: Type guard for type safety
private isValidPlayer(node: Node | null): node is Node {
return node !== null && node.getComponent(PlayerController) !== null;
}
public updatePlayer(): void {
if (this.isValidPlayer(this.playerNode)) {
// TypeScript knows playerNode is Node (not null)
const controller = this.playerNode.getComponent(PlayerController)!;
controller.update();
}
}
}
// ❌ BAD: No null checks
public updatePlayer(): void {
this.playerNode.position = new Vec3(0, 0, 0); // Can crash if null
}
// ❌ BAD: Unsafe type assertions
public getController(): PlayerController {
return this.playerNode!.getComponent(PlayerController)!; // Unsafe!
}
```
## Avoid `any` Type
```typescript
// ✅ GOOD: Proper types and interfaces
interface PlayerData {
id: string;
name: string;
level: number;
health: number;
}
@ccclass('PlayerService')
export class PlayerService extends Component {
private readonly players: Map<string, PlayerData> = new Map();
public addPlayer(data: PlayerData): void {
this.players.set(data.id, data);
}
public getPlayer(id: string): PlayerData | undefined {
return this.players.get(id);
}
}
// ❌ BAD: Using any type
@ccclass('PlayerService')
export class PlayerService extends Component {
private players: any = {}; // Type safety lost
public addPlayer(data: any): void { // No type checking
this.players[data.id] = data;
}
public getPlayer(id: string): any { // Caller doesn't know structure
return this.players[id];
}
}
// ✅ GOOD: Use generics instead of any
class DataStore<T> {
private data: Map<string, T> = new Map();
public set(key: string, value: T): void {
this.data.set(key, value);
}
public get(key: string): T | undefined {
return this.data.get(key);
}
}
// ✅ GOOD: Use unknown for truly unknown types (safer than any)
function parseJSON(json: string): unknown {
return JSON.parse(json);
}
// Then validate and narrow the type
const result = parseJSON('{"name": "Player"}');
if (isPlayerData(result)) {
// result is now typed as PlayerData
console.log(result.name);
}
function isPlayerData(obj: unknown): obj is PlayerData {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'level' in obj &&
'health' in obj
);
}
```
## Summary: Quality Checklist
**Before committing code, verify:**
- [ ] TypeScript strict mode enabled in tsconfig.json
- [ ] ESLint configuration active and passing
- [ ] All class members have access modifiers (public/private/protected)
- [ ] Exceptions thrown for errors (no silent failures)
- [ ] console.log removed or wrapped in CC_DEBUG
- [ ] readonly used for non-reassigned fields
- [ ] const used for constants (not let)
- [ ] No inline comments (code is self-explanatory)
- [ ] Optional chaining (?.) for safe property access
- [ ] Nullish coalescing (??) for default values
- [ ] No `any` types without justification
- [ ] Required references validated in onLoad()
**Quality is the foundation of all other patterns. Get this right first.**
```
### references/language/modern-typescript.md
```markdown
# Modern TypeScript Patterns
## Array Methods Over Loops
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass } = _decorator;
interface Enemy {
node: Node;
isActive: boolean;
health: number;
damage: number;
}
@ccclass('EnemyManager')
export class EnemyManager extends Component {
private readonly enemies: Enemy[] = [];
// ✅ EXCELLENT: Array methods for filtering
public getActiveEnemies(): Enemy[] {
return this.enemies.filter(enemy => enemy.isActive);
}
// ✅ EXCELLENT: Array methods for mapping
public getEnemyPositions(): Vec3[] {
return this.enemies.map(enemy => enemy.node.position.clone());
}
// ✅ EXCELLENT: Array methods for reduction
public getTotalDamage(): number {
return this.enemies.reduce((total, enemy) => total + enemy.damage, 0);
}
// ✅ EXCELLENT: Chaining array methods
public getActiveEnemyDamage(): number {
return this.enemies
.filter(enemy => enemy.isActive)
.reduce((total, enemy) => total + enemy.damage, 0);
}
// ✅ EXCELLENT: find instead of manual loop
public findEnemyById(id: string): Enemy | undefined {
return this.enemies.find(enemy => enemy.node.uuid === id);
}
// ✅ EXCELLENT: some/every for existence checks
public hasActiveEnemies(): boolean {
return this.enemies.some(enemy => enemy.isActive);
}
public areAllEnemiesDead(): boolean {
return this.enemies.every(enemy => enemy.health <= 0);
}
}
// ❌ BAD: Manual loops
public getActiveEnemies(): Enemy[] {
const active: Enemy[] = [];
for (let i = 0; i < this.enemies.length; i++) {
if (this.enemies[i].isActive) {
active.push(this.enemies[i]);
}
}
return active;
}
// ❌ BAD: Manual accumulation
public getTotalDamage(): number {
let total = 0;
for (const enemy of this.enemies) {
total += enemy.damage;
}
return total;
}
```
## Arrow Functions and Callbacks
```typescript
import { _decorator, Component, Node, EventTouch } from 'cc';
const { ccclass } = _decorator;
@ccclass('InputHandler')
export class InputHandler extends Component {
private readonly buttons: Node[] = [];
// ✅ EXCELLENT: Arrow functions for callbacks
protected onEnable(): void {
this.buttons.forEach(button => {
button.on(Node.EventType.TOUCH_START, this.onButtonClick, this);
});
}
protected onDisable(): void {
this.buttons.forEach(button => {
button.off(Node.EventType.TOUCH_START, this.onButtonClick, this);
});
}
// ✅ GOOD: Arrow function preserves this context
private readonly onButtonClick = (event: EventTouch): void => {
const button = event.target as Node;
this.handleButtonClick(button);
};
// ✅ GOOD: Arrow function for event handling
private setupAsyncOperation(): void {
setTimeout(() => {
this.processData();
}, 1000);
}
// ✅ GOOD: Arrow function in Promise chains
private async loadData(): Promise<void> {
fetch('data.json')
.then(response => response.json())
.then(data => this.processData(data))
.catch(error => this.handleError(error));
}
}
// ❌ BAD: Function expression loses this context
protected onEnable(): void {
this.buttons.forEach(function(button) {
// 'this' is undefined or wrong context
button.on(Node.EventType.TOUCH_START, this.onButtonClick, this);
});
}
// ❌ BAD: Verbose function syntax
private setupAsyncOperation(): void {
const self = this;
setTimeout(function() {
self.processData();
}, 1000);
}
```
## Destructuring
```typescript
import { _decorator, Component, Node, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
interface PlayerData {
id: string;
name: string;
level: number;
health: number;
position: { x: number; y: number; z: number };
}
@ccclass('PlayerController')
export class PlayerController extends Component {
// ✅ EXCELLENT: Destructuring in parameters
public updatePlayer({ id, name, level, health, position }: PlayerData): void {
console.log(`Updating ${name} (${id}) at level ${level}`);
// ✅ EXCELLENT: Nested destructuring
const { x, y, z } = position;
this.node.setPosition(x, y, z);
}
// ✅ EXCELLENT: Destructuring with defaults
public loadConfig({ speed = 100, jumpHeight = 50, maxHealth = 100 } = {}): void {
this.speed = speed;
this.jumpHeight = jumpHeight;
this.maxHealth = maxHealth;
}
// ✅ EXCELLENT: Array destructuring
public getPlayerPosition(): Vec3 {
const [x, y, z] = [this.node.position.x, this.node.position.y, this.node.position.z];
return new Vec3(x, y, z);
}
// ✅ EXCELLENT: Rest operator with destructuring
public handleInput({ type, ...eventData }: InputEvent): void {
switch (type) {
case 'touch':
this.handleTouch(eventData);
break;
case 'key':
this.handleKey(eventData);
break;
}
}
}
// ❌ BAD: No destructuring
public updatePlayer(playerData: PlayerData): void {
console.log(`Updating ${playerData.name} (${playerData.id}) at level ${playerData.level}`);
this.node.setPosition(playerData.position.x, playerData.position.y, playerData.position.z);
}
// ❌ BAD: Verbose property access
public loadConfig(config: Config): void {
this.speed = config.speed !== undefined ? config.speed : 100;
this.jumpHeight = config.jumpHeight !== undefined ? config.jumpHeight : 50;
this.maxHealth = config.maxHealth !== undefined ? config.maxHealth : 100;
}
```
## Spread Operator
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
interface GameConfig {
playerName: string;
difficulty: string;
soundEnabled: boolean;
}
@ccclass('GameManager')
export class GameManager extends Component {
private readonly defaultConfig: GameConfig = {
playerName: 'Player',
difficulty: 'normal',
soundEnabled: true,
};
// ✅ EXCELLENT: Spread for object merging
public createConfig(overrides: Partial<GameConfig>): GameConfig {
return { ...this.defaultConfig, ...overrides };
}
// ✅ EXCELLENT: Spread for array concatenation
private readonly baseEnemies: string[] = ['goblin', 'orc'];
private readonly bossEnemies: string[] = ['dragon', 'demon'];
public getAllEnemies(): string[] {
return [...this.baseEnemies, ...this.bossEnemies];
}
// ✅ EXCELLENT: Spread for array cloning
public cloneEnemyList(): string[] {
return [...this.baseEnemies];
}
// ✅ EXCELLENT: Spread in function calls
public calculateMaxValue(...values: number[]): number {
return Math.max(...values);
}
// ✅ EXCELLENT: Spread for immutable updates
public addEnemy(enemy: string): void {
this.baseEnemies = [...this.baseEnemies, enemy];
}
}
// ❌ BAD: Manual merging
public createConfig(overrides: Partial<GameConfig>): GameConfig {
const config: GameConfig = {
playerName: overrides.playerName ?? this.defaultConfig.playerName,
difficulty: overrides.difficulty ?? this.defaultConfig.difficulty,
soundEnabled: overrides.soundEnabled ?? this.defaultConfig.soundEnabled,
};
return config;
}
// ❌ BAD: Manual concatenation
public getAllEnemies(): string[] {
const enemies: string[] = [];
for (const enemy of this.baseEnemies) {
enemies.push(enemy);
}
for (const enemy of this.bossEnemies) {
enemies.push(enemy);
}
return enemies;
}
```
## Optional Chaining (?.)
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
interface Player {
name: string;
stats?: {
health?: number;
level?: number;
};
inventory?: {
items?: Item[];
};
}
@ccclass('PlayerManager')
export class PlayerManager extends Component {
@property(Node)
private readonly playerNode: Node | null = null;
// ✅ EXCELLENT: Optional chaining for safe access
public getPlayerName(): string | undefined {
return this.playerNode?.name;
}
// ✅ EXCELLENT: Deep optional chaining
public getPlayerHealth(player: Player): number | undefined {
return player?.stats?.health;
}
// ✅ EXCELLENT: Optional chaining with arrays
public getFirstItem(player: Player): Item | undefined {
return player?.inventory?.items?.[0];
}
// ✅ EXCELLENT: Optional chaining with methods
public getComponentName(): string | undefined {
return this.playerNode?.getComponent(PlayerController)?.getName?.();
}
// ✅ EXCELLENT: Combining with nullish coalescing
public getDisplayName(): string {
return this.playerNode?.name ?? 'Unknown Player';
}
}
// ❌ BAD: Manual null checking
public getPlayerName(): string | undefined {
if (this.playerNode !== null && this.playerNode !== undefined) {
return this.playerNode.name;
}
return undefined;
}
// ❌ BAD: Nested null checks
public getPlayerHealth(player: Player): number | undefined {
if (player) {
if (player.stats) {
if (player.stats.health !== undefined) {
return player.stats.health;
}
}
}
return undefined;
}
```
## Nullish Coalescing (??)
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
interface GameConfig {
playerName?: string;
maxHealth?: number;
soundVolume?: number;
enableTutorial?: boolean;
}
@ccclass('ConfigManager')
export class ConfigManager extends Component {
// ✅ EXCELLENT: Nullish coalescing for defaults
public loadConfig(config: GameConfig): void {
const playerName = config.playerName ?? 'Player';
const maxHealth = config.maxHealth ?? 100;
const soundVolume = config.soundVolume ?? 0.5;
const enableTutorial = config.enableTutorial ?? true;
console.log({ playerName, maxHealth, soundVolume, enableTutorial });
}
// ✅ EXCELLENT: Nullish coalescing preserves falsy values
public getVolume(volume?: number): number {
// Returns 0 if volume is 0 (not using || which would return 1)
return volume ?? 1;
}
// ✅ EXCELLENT: Chaining nullish coalescing
public getPlayerName(primaryName?: string, secondaryName?: string): string {
return primaryName ?? secondaryName ?? 'Unknown';
}
// ✅ EXCELLENT: Nullish coalescing with optional chaining
public getHealthDisplay(player?: Player): string {
const health = player?.stats?.health ?? 0;
return `Health: ${health}`;
}
}
// ❌ BAD: Using || operator (treats 0, '', false as null)
public getVolume(volume?: number): number {
return volume || 1; // Returns 1 even if volume is 0
}
// ❌ BAD: Manual null/undefined checks
public loadConfig(config: GameConfig): void {
const playerName = config.playerName !== null && config.playerName !== undefined
? config.playerName
: 'Player';
}
// ❌ BAD: Verbose ternary
public getPlayerName(name?: string): string {
return name !== undefined && name !== null ? name : 'Unknown';
}
```
## Type Guards
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass } = _decorator;
// ✅ EXCELLENT: Type guard for interface
interface Player {
type: 'player';
health: number;
level: number;
}
interface Enemy {
type: 'enemy';
health: number;
damage: number;
}
type Entity = Player | Enemy;
function isPlayer(entity: Entity): entity is Player {
return entity.type === 'player';
}
function isEnemy(entity: Entity): entity is Enemy {
return entity.type === 'enemy';
}
@ccclass('CombatManager')
export class CombatManager extends Component {
public handleEntity(entity: Entity): void {
if (isPlayer(entity)) {
// TypeScript knows entity is Player
console.log(`Player level: ${entity.level}`);
} else if (isEnemy(entity)) {
// TypeScript knows entity is Enemy
console.log(`Enemy damage: ${entity.damage}`);
}
}
// ✅ EXCELLENT: Type guard for null/undefined
private isValidNode(node: Node | null | undefined): node is Node {
return node !== null && node !== undefined;
}
public processNode(node: Node | null): void {
if (this.isValidNode(node)) {
// TypeScript knows node is Node (not null)
node.setPosition(0, 0, 0);
}
}
// ✅ EXCELLENT: Type guard for component
private hasPlayerController(node: Node): node is Node & { getComponent(PlayerController): PlayerController } {
return node.getComponent(PlayerController) !== null;
}
public updatePlayer(node: Node): void {
if (this.hasPlayerController(node)) {
// TypeScript knows component exists
const controller = node.getComponent(PlayerController)!;
controller.update();
}
}
}
// ❌ BAD: No type guards, type assertions everywhere
public handleEntity(entity: Entity): void {
if (entity.type === 'player') {
console.log(`Player level: ${(entity as Player).level}`); // Type assertion
} else {
console.log(`Enemy damage: ${(entity as Enemy).damage}`); // Type assertion
}
}
```
## Utility Types
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
interface GameConfig {
playerName: string;
maxHealth: number;
difficulty: string;
soundEnabled: boolean;
}
@ccclass('ConfigManager')
export class ConfigManager extends Component {
// ✅ EXCELLENT: Partial for optional properties
public updateConfig(updates: Partial<GameConfig>): void {
// All properties are optional
}
// ✅ EXCELLENT: Required for mandatory properties
public validateConfig(config: Required<GameConfig>): void {
// All properties are required
}
// ✅ EXCELLENT: Readonly for immutable objects
private readonly defaultConfig: Readonly<GameConfig> = {
playerName: 'Player',
maxHealth: 100,
difficulty: 'normal',
soundEnabled: true,
};
// ✅ EXCELLENT: Pick for selecting properties
public getDisplayInfo(config: GameConfig): Pick<GameConfig, 'playerName' | 'difficulty'> {
return {
playerName: config.playerName,
difficulty: config.difficulty,
};
}
// ✅ EXCELLENT: Omit for excluding properties
public getPublicConfig(config: GameConfig): Omit<GameConfig, 'soundEnabled'> {
const { soundEnabled, ...publicConfig } = config;
return publicConfig;
}
// ✅ EXCELLENT: Record for key-value mappings
private readonly difficultyMultipliers: Record<string, number> = {
easy: 0.5,
normal: 1.0,
hard: 1.5,
expert: 2.0,
};
// ✅ EXCELLENT: ReturnType for function return types
private createPlayer(): { name: string; level: number } {
return { name: 'Player', level: 1 };
}
type PlayerType = ReturnType<typeof this.createPlayer>;
}
```
## Async/Await Patterns
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('DataManager')
export class DataManager extends Component {
// ✅ EXCELLENT: Async/await for sequential operations
public async loadGameData(): Promise<void> {
try {
const playerData = await this.fetchPlayerData();
const levelData = await this.fetchLevelData(playerData.currentLevel);
await this.initializeGame(playerData, levelData);
} catch (error) {
console.error('Failed to load game data:', error);
throw error;
}
}
// ✅ EXCELLENT: Promise.all for parallel operations
public async loadAllData(): Promise<void> {
try {
const [playerData, configData, assetsData] = await Promise.all([
this.fetchPlayerData(),
this.fetchConfigData(),
this.fetchAssetsData(),
]);
this.initializeWithData(playerData, configData, assetsData);
} catch (error) {
console.error('Failed to load data:', error);
throw error;
}
}
// ✅ EXCELLENT: Promise.allSettled for partial failures
public async loadDataWithFallback(): Promise<void> {
const results = await Promise.allSettled([
this.fetchPlayerData(),
this.fetchConfigData(),
this.fetchAssetsData(),
]);
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
console.log(`Data ${index} loaded:`, result.value);
} else {
console.error(`Data ${index} failed:`, result.reason);
}
});
}
// ✅ EXCELLENT: Error handling with async/await
public async savePlayerData(data: PlayerData): Promise<boolean> {
try {
await this.validateData(data);
await this.uploadData(data);
return true;
} catch (error) {
if (error instanceof ValidationError) {
console.error('Invalid data:', error.message);
} else if (error instanceof NetworkError) {
console.error('Network error:', error.message);
} else {
console.error('Unknown error:', error);
}
return false;
}
}
private async fetchPlayerData(): Promise<PlayerData> {
// Implementation
}
private async fetchLevelData(level: number): Promise<LevelData> {
// Implementation
}
}
// ❌ BAD: Promise chains (callback hell)
public loadGameData(): void {
this.fetchPlayerData()
.then(playerData => {
return this.fetchLevelData(playerData.currentLevel);
})
.then(levelData => {
return this.initializeGame(playerData, levelData); // playerData not in scope!
})
.catch(error => {
console.error('Failed:', error);
});
}
```
## Summary: Modern TypeScript Checklist
**Use these patterns for cleaner, more maintainable code:**
- [ ] Array methods (map/filter/reduce) instead of manual loops
- [ ] Arrow functions for callbacks and event handlers
- [ ] Destructuring for cleaner parameter handling
- [ ] Spread operator for object/array operations
- [ ] Optional chaining (?.) for safe property access
- [ ] Nullish coalescing (??) for default values
- [ ] Type guards for type-safe narrowing
- [ ] Utility types (Partial, Required, Readonly, Pick, Omit, Record)
- [ ] Async/await for asynchronous operations
- [ ] Promise.all/allSettled for parallel operations
**Modern TypeScript makes code more concise, readable, and type-safe.**
```
### references/framework/component-system.md
```markdown
# Cocos Creator Component System
## Entity-Component (EC) System Overview
Cocos Creator uses an Entity-Component (EC) architecture where:
- **Node** = Entity (game object container)
- **Component** = Behavior/functionality attached to Node
- **Scene** = Collection of Node hierarchies
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
// ✅ EXCELLENT: Complete component structure
@ccclass('PlayerController')
export class PlayerController extends Component {
// @property decorator exposes fields to Inspector
@property(Node)
private readonly targetNode: Node | null = null;
@property(Number)
private readonly moveSpeed: number = 100;
// Private fields not exposed
private currentHealth: number = 100;
private static readonly MAX_HEALTH: number = 100;
// Lifecycle methods in execution order:
// 1. onLoad() - Component initialization
// 2. start() - After all components loaded
// 3. onEnable() - When enabled (can be called multiple times)
// 4. update(dt) - Every frame
// 5. lateUpdate(dt) - After all update() calls
// 6. onDisable() - When disabled
// 7. onDestroy() - Cleanup
}
```
## @ccclass Decorator
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
// ✅ EXCELLENT: @ccclass with explicit name
@ccclass('GameManager')
export class GameManager extends Component {
// Component implementation
}
// ✅ GOOD: @ccclass without name (uses class name)
@ccclass
export class PlayerController extends Component {
// Component implementation
}
// ❌ WRONG: Missing @ccclass decorator
export class GameManager extends Component {
// Won't work - Cocos can't serialize this component
}
// ❌ WRONG: Not extending Component
@ccclass('GameManager')
export class GameManager {
// Won't work - must extend Component
}
```
## @property Decorator
```typescript
import { _decorator, Component, Node, Sprite, Label } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PropertyExamples')
export class PropertyExamples extends Component {
// ✅ EXCELLENT: Node reference
@property(Node)
private readonly playerNode: Node | null = null;
// ✅ EXCELLENT: Component reference
@property(Sprite)
private readonly spriteComponent: Sprite | null = null;
// ✅ EXCELLENT: Primitive types
@property(Number)
private readonly moveSpeed: number = 100;
@property(String)
private readonly playerName: string = 'Player';
@property(Boolean)
private readonly enableDebug: boolean = false;
// ✅ EXCELLENT: Array of nodes
@property([Node])
private readonly enemyNodes: Node[] = [];
// ✅ EXCELLENT: Array of numbers
@property([Number])
private readonly levelScores: number[] = [];
// ✅ EXCELLENT: Enum property
@property({ type: Enum(GameState) })
private currentState: GameState = GameState.LOADING;
// ✅ EXCELLENT: Property with custom display name and tooltip
@property({
type: Number,
displayName: 'Movement Speed',
tooltip: 'Player movement speed in units per second',
min: 0,
max: 500,
step: 10,
})
private readonly speed: number = 100;
// ✅ EXCELLENT: readonly for properties that shouldn't be reassigned
@property(Node)
private readonly targetNode: Node | null = null; // Can't reassign after initialization
// Private field (not exposed to Inspector)
private currentHealth: number = 100;
}
// ❌ WRONG: Property without type
@property
private playerNode: Node | null = null; // Won't serialize correctly
// ❌ WRONG: Mutable property that should be readonly
@property(Node)
private targetNode: Node | null = null; // Should be readonly if not reassigned
```
## Component Lifecycle Methods
### 1. onLoad() - Initialization
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('GameManager')
export class GameManager extends Component {
@property(Node)
private readonly playerNode: Node | null = null;
@property(Node)
private readonly uiRoot: Node | null = null;
// ✅ EXCELLENT: onLoad for initialization and validation
protected onLoad(): void {
// Validate required references
if (!this.playerNode) {
throw new Error('GameManager: playerNode is required');
}
if (!this.uiRoot) {
throw new Error('GameManager: uiRoot is required');
}
// Initialize component state
this.initializeGameState();
// Cache references (DO NOT reference other components yet)
this.cacheNodeReferences();
}
private initializeGameState(): void {
// Setup initial state
}
private cacheNodeReferences(): void {
// Cache child nodes for faster access
}
}
// ❌ WRONG: Accessing other components in onLoad
protected onLoad(): void {
// Don't do this - other components may not be loaded yet
const playerController = this.playerNode!.getComponent(PlayerController);
playerController.initialize(); // May be undefined!
}
// ❌ WRONG: Heavy operations in onLoad
protected onLoad(): void {
// Avoid expensive operations - onLoad should be fast
this.loadAllLevelData(); // Should be async in start()
this.generateProceduralContent(); // Too expensive for onLoad
}
```
### 2. start() - Post-Initialization
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PlayerController')
export class PlayerController extends Component {
@property(Node)
private readonly enemyManagerNode: Node | null = null;
private enemyManager!: EnemyManager;
protected onLoad(): void {
// Validate references
if (!this.enemyManagerNode) {
throw new Error('PlayerController: enemyManagerNode is required');
}
}
// ✅ EXCELLENT: start() for referencing other components
protected start(): void {
// Safe to get components from other nodes now
const enemyManager = this.enemyManagerNode!.getComponent(EnemyManager);
if (!enemyManager) {
throw new Error('EnemyManager component not found');
}
this.enemyManager = enemyManager;
// Initialize based on other components
this.setupPlayerBasedOnEnemies();
// Start async operations
this.loadPlayerDataAsync();
}
private setupPlayerBasedOnEnemies(): void {
const enemyCount = this.enemyManager.getEnemyCount();
this.adjustDifficultyBasedOnEnemies(enemyCount);
}
private async loadPlayerDataAsync(): Promise<void> {
// Async loading is safe in start()
}
}
// ❌ WRONG: Using start() instead of onLoad for validation
protected start(): void {
// Too late - might be used before start() is called
if (!this.playerNode) {
throw new Error('playerNode is required');
}
}
```
### 3. onEnable() - Activation
```typescript
import { _decorator, Component, Node, EventTouch } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('InputHandler')
export class InputHandler extends Component {
@property(Node)
private readonly buttonNode: Node | null = null;
// ✅ EXCELLENT: onEnable() for registering listeners
protected onEnable(): void {
// Register event listeners
if (this.buttonNode) {
this.buttonNode.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
this.buttonNode.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
}
// Subscribe to global events
EventManager.on(GameEvent.LEVEL_COMPLETE, this.onLevelComplete, this);
// Resume component operations
this.resumeGameLogic();
}
protected onDisable(): void {
// ✅ CRITICAL: Always unregister in onDisable
if (this.buttonNode) {
this.buttonNode.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
this.buttonNode.off(Node.EventType.TOUCH_END, this.onTouchEnd, this);
}
EventManager.off(GameEvent.LEVEL_COMPLETE, this.onLevelComplete, this);
// Pause component operations
this.pauseGameLogic();
}
private onTouchStart(event: EventTouch): void {
// Handle touch
}
private onTouchEnd(event: EventTouch): void {
// Handle touch end
}
private onLevelComplete(): void {
// Handle level complete
}
}
// ❌ WRONG: Registering listeners in onLoad
protected onLoad(): void {
// Don't register here - won't be unregistered properly when disabled
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
}
// ❌ WRONG: Not unregistering in onDisable
protected onEnable(): void {
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
}
protected onDisable(): void {
// Missing unregistration - memory leak!
}
```
### 4. update(dt) - Per-Frame Logic
```typescript
import { _decorator, Component, Node, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PlayerMovement')
export class PlayerMovement extends Component {
@property(Number)
private readonly moveSpeed: number = 100;
private readonly tempVec3: Vec3 = new Vec3();
private inputDirection: Vec3 = new Vec3(1, 0, 0);
// ✅ EXCELLENT: Efficient update implementation
protected update(dt: number): void {
// Reuse preallocated vector
this.node.getPosition(this.tempVec3);
// Calculate movement
this.tempVec3.x += this.inputDirection.x * this.moveSpeed * dt;
this.tempVec3.y += this.inputDirection.y * this.moveSpeed * dt;
// Apply new position
this.node.setPosition(this.tempVec3);
}
}
// Throttled expensive operations
@ccclass('AIController')
export class AIController extends Component {
private frameCount: number = 0;
private static readonly AI_UPDATE_INTERVAL: number = 10;
// ✅ EXCELLENT: Throttle expensive operations
protected update(dt: number): void {
this.frameCount++;
// Cheap operations every frame
this.moveTowardsTarget(dt);
// Expensive AI decisions every 10 frames
if (this.frameCount % AIController.AI_UPDATE_INTERVAL === 0) {
this.updateAIDecision();
}
}
private moveTowardsTarget(dt: number): void {
// Simple movement calculation
}
private updateAIDecision(): void {
// Complex AI logic
}
}
// ❌ WRONG: Allocations in update
protected update(dt: number): void {
const currentPos = this.node.position.clone(); // Allocates every frame!
currentPos.x += this.moveSpeed * dt;
this.node.setPosition(currentPos);
}
// ❌ WRONG: Expensive operations every frame
protected update(dt: number): void {
this.recalculatePathfinding(); // A* algorithm 60 times per second!
this.updateComplexAI(); // Too expensive for every frame
}
// ❌ WRONG: Component lookups in update
protected update(dt: number): void {
const sprite = this.node.getComponent(Sprite); // Cache this in onLoad!
sprite?.doSomething();
}
```
### 5. lateUpdate(dt) - Post-Update Logic
```typescript
import { _decorator, Component, Node, Camera } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('CameraFollow')
export class CameraFollow extends Component {
@property(Node)
private readonly target: Node | null = null;
@property(Camera)
private readonly camera: Camera | null = null;
// ✅ EXCELLENT: lateUpdate for camera following
// Runs after all update() calls, ensuring target has moved
protected lateUpdate(dt: number): void {
if (!this.target || !this.camera) return;
// Follow target position after target has been updated
const targetPos = this.target.position;
this.camera.node.setPosition(targetPos.x, targetPos.y, this.camera.node.position.z);
}
}
// ✅ GOOD: lateUpdate for UI that depends on game state
@ccclass('HealthBarUpdater')
export class HealthBarUpdater extends Component {
@property(Node)
private readonly healthBar: Node | null = null;
private playerHealth: number = 100;
// Health is updated in PlayerController.update()
// UI is updated in lateUpdate() to reflect final health value
protected lateUpdate(dt: number): void {
if (!this.healthBar) return;
const healthPercentage = this.playerHealth / 100;
this.healthBar.scale = new Vec3(healthPercentage, 1, 1);
}
}
// ❌ WRONG: Using lateUpdate for regular logic
protected lateUpdate(dt: number): void {
// This should be in update(), not lateUpdate()
this.movePlayer(dt);
}
```
### 6. onDestroy() - Cleanup
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('ResourceManager')
export class ResourceManager extends Component {
private readonly loadedAssets: Map<string, Asset> = new Map();
private readonly eventListeners: Set<Function> = new Set();
private readonly scheduledCallbacks: Set<Function> = new Set();
// ✅ EXCELLENT: Complete cleanup in onDestroy
protected onDestroy(): void {
// Unregister all event listeners
this.node.off(Node.EventType.TOUCH_START);
EventManager.off(GameEvent.LEVEL_COMPLETE, this.onLevelComplete, this);
// Clear collections
this.eventListeners.clear();
this.scheduledCallbacks.clear();
// Release loaded assets
for (const [id, asset] of this.loadedAssets) {
asset.decRef();
}
this.loadedAssets.clear();
// Unschedule all callbacks
this.unscheduleAllCallbacks();
// Clear any references to prevent memory leaks
this.clearReferences();
}
private clearReferences(): void {
// Clear any cached references
}
}
// ❌ WRONG: Missing cleanup
protected onDestroy(): void {
// Forgot to unregister events - memory leak!
// Forgot to release assets - memory leak!
// Forgot to unschedule callbacks - may cause errors!
}
// ❌ WRONG: Incomplete cleanup
protected onDestroy(): void {
this.loadedAssets.clear(); // Cleared map but didn't decRef assets!
}
```
## Component Execution Order
```typescript
// Execution order when scene loads:
// 1. All components: onLoad() (in hierarchy order)
// 2. All components: start() (in hierarchy order)
// 3. All components: onEnable() (if not already enabled)
// 4. Begin frame loop:
// - All components: update(dt)
// - All components: lateUpdate(dt)
// 5. When component disabled:
// - Component: onDisable()
// 6. When component destroyed:
// - Component: onDestroy()
// ✅ EXCELLENT: Lifecycle method organization
@ccclass('CompleteLifecycle')
export class CompleteLifecycle extends Component {
// 1. INITIALIZATION PHASE
protected onLoad(): void {
// Initialize component
// Validate required references
// Cache node references
// DO NOT access other components yet
}
protected start(): void {
// Access other components (safe now)
// Start async operations
// Initialize based on other components
}
// 2. ACTIVATION PHASE
protected onEnable(): void {
// Register event listeners
// Subscribe to global events
// Resume operations
}
// 3. UPDATE PHASE
protected update(dt: number): void {
// Per-frame game logic
// Movement, input, AI
// Keep allocations zero
}
protected lateUpdate(dt: number): void {
// Logic that depends on update()
// Camera follow, UI updates
}
// 4. DEACTIVATION PHASE
protected onDisable(): void {
// Unregister event listeners
// Unsubscribe from events
// Pause operations
}
// 5. CLEANUP PHASE
protected onDestroy(): void {
// Release resources
// Clear collections
// Unschedule callbacks
// Final cleanup
}
}
```
## Required Reference Validation
```typescript
import { _decorator, Component, Node, Sprite } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('RequiredReferences')
export class RequiredReferences extends Component {
@property(Node)
private readonly targetNode: Node | null = null;
@property(Sprite)
private readonly spriteComponent: Sprite | null = null;
@property([Node])
private readonly enemyNodes: Node[] = [];
// ✅ EXCELLENT: Validate all required references in onLoad
protected onLoad(): void {
if (!this.targetNode) {
throw new Error('RequiredReferences: targetNode is required');
}
if (!this.spriteComponent) {
throw new Error('RequiredReferences: spriteComponent is required');
}
if (this.enemyNodes.length === 0) {
throw new Error('RequiredReferences: at least one enemy node is required');
}
// All references validated - safe to use
this.initialize();
}
private initialize(): void {
// Can safely use all references here
this.targetNode!.setPosition(0, 0, 0);
this.spriteComponent!.sizeMode = Sprite.SizeMode.CUSTOM;
}
}
// ❌ WRONG: No validation
protected onLoad(): void {
// Assuming references exist - may crash at runtime
this.targetNode!.setPosition(0, 0, 0);
}
// ❌ WRONG: Silent validation
protected onLoad(): void {
if (!this.targetNode) {
console.error('targetNode is missing'); // Don't just log
return; // Silent failure
}
}
```
## Summary: Component System Checklist
**Component Structure:**
- [ ] @ccclass decorator on class
- [ ] Extends Component base class
- [ ] @property decorator for Inspector-exposed fields
- [ ] readonly for properties that aren't reassigned
- [ ] Access modifiers (public/private/protected)
**Lifecycle Implementation:**
- [ ] onLoad() - Validate required references, initialize state
- [ ] start() - Access other components, start async operations
- [ ] onEnable() - Register event listeners
- [ ] update(dt) - Per-frame logic (zero allocations)
- [ ] lateUpdate(dt) - Post-update logic (camera, UI)
- [ ] onDisable() - Unregister event listeners
- [ ] onDestroy() - Release resources, clear references
**Best Practices:**
- [ ] Validate required @property references in onLoad()
- [ ] Throw exceptions for missing required references
- [ ] Cache component references (don't lookup in update)
- [ ] Zero allocations in update/lateUpdate
- [ ] Always unregister listeners in onDisable/onDestroy
- [ ] Use readonly for @property fields when appropriate
**The component lifecycle is the foundation of Cocos Creator architecture.**
```
### references/framework/event-patterns.md
```markdown
# Cocos Creator Event Patterns
## EventDispatcher Pattern (Custom Events)
```typescript
import { _decorator, Component, EventTarget } from 'cc';
const { ccclass } = _decorator;
// ✅ EXCELLENT: Centralized event system
export enum GameEvent {
SCORE_CHANGED = 'score_changed',
LEVEL_COMPLETE = 'level_complete',
PLAYER_DIED = 'player_died',
ENEMY_SPAWNED = 'enemy_spawned',
}
export interface ScoreChangedEvent {
oldScore: number;
newScore: number;
combo: number;
}
export interface LevelCompleteEvent {
level: number;
stars: number;
time: number;
}
@ccclass('EventManager')
export class EventManager extends Component {
private static instance: EventManager | null = null;
private readonly eventTarget: EventTarget = new EventTarget();
protected onLoad(): void {
if (EventManager.instance) {
throw new Error('EventManager: instance already exists');
}
EventManager.instance = this;
}
protected onDestroy(): void {
this.eventTarget.clear();
EventManager.instance = null;
}
// ✅ EXCELLENT: Type-safe emit
public static emit<T>(event: GameEvent, data?: T): void {
if (!EventManager.instance) {
throw new Error('EventManager: instance not initialized');
}
EventManager.instance.eventTarget.emit(event, data);
}
// ✅ EXCELLENT: Type-safe subscribe
public static on<T>(event: GameEvent, callback: (data: T) => void, target?: any): void {
if (!EventManager.instance) {
throw new Error('EventManager: instance not initialized');
}
EventManager.instance.eventTarget.on(event, callback, target);
}
// ✅ EXCELLENT: Type-safe unsubscribe
public static off<T>(event: GameEvent, callback: (data: T) => void, target?: any): void {
if (!EventManager.instance) {
throw new Error('EventManager: instance not initialized');
}
EventManager.instance.eventTarget.off(event, callback, target);
}
// ✅ EXCELLENT: Once (auto-unsubscribe after first call)
public static once<T>(event: GameEvent, callback: (data: T) => void, target?: any): void {
if (!EventManager.instance) {
throw new Error('EventManager: instance not initialized');
}
EventManager.instance.eventTarget.once(event, callback, target);
}
}
// Usage in component
@ccclass('ScoreManager')
export class ScoreManager extends Component {
private currentScore: number = 0;
public addScore(points: number): void {
const oldScore = this.currentScore;
this.currentScore += points;
// ✅ EXCELLENT: Emit typed event
EventManager.emit<ScoreChangedEvent>(GameEvent.SCORE_CHANGED, {
oldScore,
newScore: this.currentScore,
combo: 1,
});
}
}
// Subscriber component
@ccclass('ScoreDisplay')
export class ScoreDisplay extends Component {
protected onEnable(): void {
// ✅ EXCELLENT: Subscribe in onEnable
EventManager.on<ScoreChangedEvent>(GameEvent.SCORE_CHANGED, this.onScoreChanged, this);
}
protected onDisable(): void {
// ✅ CRITICAL: Always unsubscribe in onDisable
EventManager.off<ScoreChangedEvent>(GameEvent.SCORE_CHANGED, this.onScoreChanged, this);
}
private onScoreChanged(data: ScoreChangedEvent): void {
console.log(`Score: ${data.oldScore} → ${data.newScore}`);
this.updateDisplay(data.newScore);
}
private updateDisplay(score: number): void {
// Update UI
}
}
// ❌ WRONG: No unsubscription (memory leak)
protected onEnable(): void {
EventManager.on<ScoreChangedEvent>(GameEvent.SCORE_CHANGED, this.onScoreChanged, this);
}
// Missing onDisable - memory leak!
// ❌ WRONG: String-based events (not type-safe)
EventManager.emit('score_changed', { score: 100 }); // Typo-prone
```
## Node Event System (Built-in Events)
```typescript
import { _decorator, Component, Node, EventTouch, EventKeyboard } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('TouchHandler')
export class TouchHandler extends Component {
@property(Node)
private readonly buttonNode: Node | null = null;
// ✅ EXCELLENT: Touch event handling
protected onEnable(): void {
if (!this.buttonNode) return;
this.buttonNode.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
this.buttonNode.on(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
this.buttonNode.on(Node.EventType.TOUCH_END, this.onTouchEnd, this);
this.buttonNode.on(Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this);
}
protected onDisable(): void {
if (!this.buttonNode) return;
this.buttonNode.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
this.buttonNode.off(Node.EventType.TOUCH_MOVE, this.onTouchMove, this);
this.buttonNode.off(Node.EventType.TOUCH_END, this.onTouchEnd, this);
this.buttonNode.off(Node.EventType.TOUCH_CANCEL, this.onTouchCancel, this);
}
private onTouchStart(event: EventTouch): void {
const location = event.getUILocation();
console.log(`Touch start at: ${location.x}, ${location.y}`);
}
private onTouchMove(event: EventTouch): void {
const delta = event.getUIDelta();
console.log(`Touch delta: ${delta.x}, ${delta.y}`);
}
private onTouchEnd(event: EventTouch): void {
console.log('Touch ended');
}
private onTouchCancel(event: EventTouch): void {
console.log('Touch cancelled');
}
}
// ✅ EXCELLENT: Keyboard event handling
@ccclass('KeyboardHandler')
export class KeyboardHandler extends Component {
protected onEnable(): void {
this.node.on(Node.EventType.KEY_DOWN, this.onKeyDown, this);
this.node.on(Node.EventType.KEY_UP, this.onKeyUp, this);
}
protected onDisable(): void {
this.node.off(Node.EventType.KEY_DOWN, this.onKeyDown, this);
this.node.off(Node.EventType.KEY_UP, this.onKeyUp, this);
}
private onKeyDown(event: EventKeyboard): void {
switch (event.keyCode) {
case macro.KEY.w:
case macro.KEY.up:
this.moveUp();
break;
case macro.KEY.s:
case macro.KEY.down:
this.moveDown();
break;
}
}
private onKeyUp(event: EventKeyboard): void {
this.stopMovement();
}
}
```
## Event Cleanup Patterns
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
// ✅ EXCELLENT: Comprehensive cleanup pattern
@ccclass('CompleteEventCleanup')
export class CompleteEventCleanup extends Component {
@property(Node)
private readonly targetNode: Node | null = null;
// Track registered listeners for complete cleanup
private readonly registeredListeners: Array<{
node: Node;
eventType: string;
callback: Function;
}> = [];
protected onEnable(): void {
if (!this.targetNode) return;
// Register and track listeners
this.registerListener(
this.targetNode,
Node.EventType.TOUCH_START,
this.onTouchStart
);
this.registerListener(
this.node,
Node.EventType.CHILD_ADDED,
this.onChildAdded
);
// Subscribe to global events
EventManager.on(GameEvent.LEVEL_COMPLETE, this.onLevelComplete, this);
}
protected onDisable(): void {
// Unregister all tracked listeners
for (const { node, eventType, callback } of this.registeredListeners) {
node.off(eventType, callback, this);
}
this.registeredListeners.length = 0;
// Unsubscribe from global events
EventManager.off(GameEvent.LEVEL_COMPLETE, this.onLevelComplete, this);
}
private registerListener(node: Node, eventType: string, callback: Function): void {
node.on(eventType, callback, this);
this.registeredListeners.push({ node, eventType, callback });
}
private onTouchStart(event: EventTouch): void {
// Handle touch
}
private onChildAdded(child: Node): void {
// Handle child added
}
private onLevelComplete(): void {
// Handle level complete
}
}
// ✅ EXCELLENT: Automatic cleanup with disposable pattern
interface IDisposable {
dispose(): void;
}
class EventSubscription implements IDisposable {
constructor(
private readonly eventManager: EventManager,
private readonly event: GameEvent,
private readonly callback: Function,
private readonly target: any
) {}
public dispose(): void {
EventManager.off(this.event, this.callback as any, this.target);
}
}
@ccclass('DisposablePattern')
export class DisposablePattern extends Component {
private readonly subscriptions: IDisposable[] = [];
protected onEnable(): void {
// ✅ EXCELLENT: Track subscriptions for auto-cleanup
this.subscriptions.push(
new EventSubscription(
EventManager.instance!,
GameEvent.SCORE_CHANGED,
this.onScoreChanged,
this
)
);
}
protected onDisable(): void {
// ✅ EXCELLENT: Dispose all subscriptions
for (const subscription of this.subscriptions) {
subscription.dispose();
}
this.subscriptions.length = 0;
}
private onScoreChanged(data: ScoreChangedEvent): void {
// Handle score change
}
}
```
## Event Performance Best Practices
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('PerformanceOptimizedEvents')
export class PerformanceOptimizedEvents extends Component {
// ✅ EXCELLENT: Throttle frequent events
private lastEmitTime: number = 0;
private static readonly EMIT_THROTTLE_MS: number = 100; // Max 10 events/second
public emitThrottled(event: GameEvent, data: any): void {
const now = Date.now();
if (now - this.lastEmitTime >= PerformanceOptimizedEvents.EMIT_THROTTLE_MS) {
EventManager.emit(event, data);
this.lastEmitTime = now;
}
}
// ✅ EXCELLENT: Batch events to reduce overhead
private readonly pendingEvents: Array<{ event: GameEvent; data: any }> = [];
private batchEmitScheduled: boolean = false;
public emitBatched(event: GameEvent, data: any): void {
this.pendingEvents.push({ event, data });
if (!this.batchEmitScheduled) {
this.batchEmitScheduled = true;
this.scheduleOnce(() => {
this.flushBatchedEvents();
}, 0);
}
}
private flushBatchedEvents(): void {
for (const { event, data } of this.pendingEvents) {
EventManager.emit(event, data);
}
this.pendingEvents.length = 0;
this.batchEmitScheduled = false;
}
// ✅ EXCELLENT: Debounce events (emit only after quiet period)
private debounceTimer: number | null = null;
private static readonly DEBOUNCE_MS: number = 300;
public emitDebounced(event: GameEvent, data: any): void {
if (this.debounceTimer !== null) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(() => {
EventManager.emit(event, data);
this.debounceTimer = null;
}, PerformanceOptimizedEvents.DEBOUNCE_MS) as any;
}
}
// ❌ WRONG: Emitting events in update loop
protected update(dt: number): void {
// Emits 60 events per second!
EventManager.emit(GameEvent.PLAYER_MOVED, this.node.position);
}
// ✅ BETTER: Throttle or emit only on significant changes
private lastPosition: Vec3 = new Vec3();
private static readonly MOVE_THRESHOLD: number = 1.0;
protected update(dt: number): void {
const distance = Vec3.distance(this.node.position, this.lastPosition);
if (distance >= PerformanceOptimizedEvents.MOVE_THRESHOLD) {
EventManager.emit(GameEvent.PLAYER_MOVED, this.node.position.clone());
this.lastPosition.set(this.node.position);
}
}
```
## Summary: Event Pattern Checklist
**EventDispatcher (Custom Events):**
- [ ] Use centralized EventManager with EventTarget
- [ ] Define event names as enum (not strings)
- [ ] Use typed event data interfaces
- [ ] Subscribe in onEnable(), unsubscribe in onDisable()
- [ ] Always pass `this` as target parameter for proper cleanup
**Node Events (Built-in):**
- [ ] Use Node.EventType constants (TOUCH_START, KEY_DOWN, etc.)
- [ ] Register listeners in onEnable()
- [ ] Unregister listeners in onDisable() with same parameters
- [ ] Handle EventTouch and EventKeyboard properly
**Event Cleanup:**
- [ ] Track all registered listeners for complete cleanup
- [ ] Unregister in both onDisable() and onDestroy()
- [ ] Use disposable pattern for automatic cleanup
- [ ] Clear event collections in onDestroy()
**Performance:**
- [ ] Throttle frequent events (max 10/second)
- [ ] Batch events to reduce overhead
- [ ] Debounce events for user input
- [ ] Never emit events in update() without throttling
**Always unsubscribe from events to prevent memory leaks.**
```
### references/framework/playable-optimization.md
```markdown
# Playable Ads Performance Optimization
## DrawCall Batching (Critical for Playables)
**Target: <10 DrawCalls for smooth 60fps playables**
```typescript
import { _decorator, Component, Sprite, SpriteAtlas, SpriteFrame } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('SpriteAtlasManager')
export class SpriteAtlasManager extends Component {
// ✅ EXCELLENT: Use sprite atlas for DrawCall batching
@property(SpriteAtlas)
private readonly characterAtlas: SpriteAtlas | null = null;
@property(SpriteAtlas)
private readonly uiAtlas: SpriteAtlas | null = null;
private readonly spriteFrameCache: Map<string, SpriteFrame> = new Map();
protected onLoad(): void {
if (!this.characterAtlas || !this.uiAtlas) {
throw new Error('SpriteAtlasManager: atlases are required');
}
// ✅ EXCELLENT: Prewarm sprite frames from atlas
this.prewarmAtlas(this.characterAtlas, 'character');
this.prewarmAtlas(this.uiAtlas, 'ui');
}
private prewarmAtlas(atlas: SpriteAtlas, prefix: string): void {
const spriteFrames = atlas.getSpriteFrames();
for (const frame of spriteFrames) {
const key = `${prefix}_${frame.name}`;
this.spriteFrameCache.set(key, frame);
}
}
// ✅ EXCELLENT: Get sprite frame from cache (batched in same DrawCall)
public getSpriteFrame(atlasName: string, frameName: string): SpriteFrame | null {
const key = `${atlasName}_${frameName}`;
return this.spriteFrameCache.get(key) ?? null;
}
}
// Usage: All sprites from same atlas = single DrawCall
@ccclass('CharacterSprite')
export class CharacterSprite extends Component {
@property(Sprite)
private readonly sprite: Sprite | null = null;
private atlasManager!: SpriteAtlasManager;
protected start(): void {
const manager = this.node.parent?.getComponent(SpriteAtlasManager);
if (!manager) throw new Error('SpriteAtlasManager not found');
this.atlasManager = manager;
// ✅ GOOD: Set sprite frame from atlas (batched)
const frame = this.atlasManager.getSpriteFrame('character', 'idle_01');
if (frame && this.sprite) {
this.sprite.spriteFrame = frame;
}
}
}
// ❌ WRONG: Individual textures (multiple DrawCalls)
@property(SpriteFrame)
private characterIdleFrame: SpriteFrame | null = null; // DrawCall 1
@property(SpriteFrame)
private characterWalkFrame: SpriteFrame | null = null; // DrawCall 2
@property(SpriteFrame)
private characterJumpFrame: SpriteFrame | null = null; // DrawCall 3
// Result: 3 DrawCalls for 3 sprites!
// ✅ BETTER: Single atlas
@property(SpriteAtlas)
private characterAtlas: SpriteAtlas | null = null; // DrawCall 1 for all frames
```
## GPU Skinning (Skeletal Animations)
```typescript
import { _decorator, Component, SkeletalAnimation } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('AnimationController')
export class AnimationController extends Component {
@property(SkeletalAnimation)
private readonly skeleton: SkeletalAnimation | null = null;
protected onLoad(): void {
if (!this.skeleton) {
throw new Error('AnimationController: skeleton is required');
}
// ✅ EXCELLENT: Enable GPU skinning for better performance
// GPU handles bone transformations instead of CPU
this.skeleton.useBakedAnimation = true; // Baked animation data
}
public playAnimation(animName: string, loop: boolean = false): void {
if (!this.skeleton) return;
const state = this.skeleton.getState(animName);
if (state) {
state.wrapMode = loop ? SkeletalAnimation.WrapMode.Loop : SkeletalAnimation.WrapMode.Normal;
this.skeleton.play(animName);
}
}
}
// ❌ WRONG: CPU skinning (default, slower)
// Don't set useBakedAnimation to false for playables
```
## Object Pooling for Playables
```typescript
import { _decorator, Component, Node, Prefab, instantiate, NodePool } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('PlayableObjectPool')
export class PlayableObjectPool extends Component {
@property(Prefab)
private readonly bulletPrefab: Prefab | null = null;
@property(Prefab)
private readonly effectPrefab: Prefab | null = null;
private readonly bulletPool: NodePool = new NodePool();
private readonly effectPool: NodePool = new NodePool();
private static readonly PREWARM_COUNT: number = 20;
// ✅ EXCELLENT: Prewarm pools to avoid allocations during gameplay
protected onLoad(): void {
if (!this.bulletPrefab || !this.effectPrefab) {
throw new Error('PlayableObjectPool: prefabs are required');
}
// Prewarm bullet pool
for (let i = 0; i < PlayableObjectPool.PREWARM_COUNT; i++) {
const bullet = instantiate(this.bulletPrefab);
this.bulletPool.put(bullet);
}
// Prewarm effect pool
for (let i = 0; i < PlayableObjectPool.PREWARM_COUNT; i++) {
const effect = instantiate(this.effectPrefab);
this.effectPool.put(effect);
}
}
// ✅ EXCELLENT: Get from pool (zero allocations in gameplay)
public getBullet(): Node {
if (this.bulletPool.size() > 0) {
const bullet = this.bulletPool.get()!;
bullet.active = true;
return bullet;
}
// Fallback: create new (should be rare if prewarmed correctly)
if (!this.bulletPrefab) {
throw new Error('bulletPrefab is null');
}
return instantiate(this.bulletPrefab);
}
public returnBullet(bullet: Node): void {
bullet.active = false;
this.bulletPool.put(bullet);
}
protected onDestroy(): void {
this.bulletPool.clear();
this.effectPool.clear();
}
}
// ❌ WRONG: Creating/destroying objects during gameplay
public shoot(): void {
const bullet = instantiate(this.bulletPrefab!); // Allocates every time
this.scheduleOnce(() => {
bullet.destroy(); // Triggers GC
}, 2.0);
}
```
## Update Loop Optimization for Playables
```typescript
import { _decorator, Component, Node, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('OptimizedUpdate')
export class OptimizedUpdate extends Component {
@property([Node])
private readonly enemies: Node[] = [];
// ✅ EXCELLENT: Preallocate to avoid allocations in update
private readonly tempVec3: Vec3 = new Vec3();
private readonly activeEnemies: Node[] = [];
private activeEnemiesDirty: boolean = true;
private frameCount: number = 0;
// ✅ EXCELLENT: Update expensive operations every N frames
protected update(dt: number): void {
this.frameCount++;
// Cheap operations: every frame
this.updateMovement(dt);
// Expensive operations: every 10 frames (6 times/second at 60fps)
if (this.frameCount % 10 === 0) {
this.updateAI();
}
// Very expensive: every 60 frames (once per second at 60fps)
if (this.frameCount % 60 === 0) {
this.updatePathfinding();
}
}
private updateMovement(dt: number): void {
// Use cached active enemies list
const activeEnemies = this.getActiveEnemies();
for (const enemy of activeEnemies) {
// Reuse preallocated vector
enemy.getPosition(this.tempVec3);
this.tempVec3.y += 10 * dt;
enemy.setPosition(this.tempVec3);
}
}
private getActiveEnemies(): Node[] {
if (this.activeEnemiesDirty) {
this.activeEnemies.length = 0;
for (const enemy of this.enemies) {
if (enemy.active) {
this.activeEnemies.push(enemy);
}
}
this.activeEnemiesDirty = false;
}
return this.activeEnemies;
}
private updateAI(): void {
// Expensive AI logic
}
private updatePathfinding(): void {
// Very expensive pathfinding
}
}
// ❌ WRONG: All logic in update, allocations everywhere
protected update(dt: number): void {
// Allocates array every frame
const activeEnemies = this.enemies.filter(e => e.active);
for (const enemy of activeEnemies) {
// Allocates vector every frame
const pos = enemy.position.clone();
pos.y += 10 * dt;
enemy.setPosition(pos);
}
// Expensive operations every frame
this.updatePathfinding(); // 60 times/second!
this.updateAI(); // 60 times/second!
}
```
## Resource Loading and Preloading
```typescript
import { _decorator, Component, resources, SpriteFrame, AudioClip } from 'cc';
const { ccclass } = _decorator;
@ccclass('ResourcePreloader')
export class ResourcePreloader extends Component {
private readonly loadedResources: Map<string, Asset> = new Map();
// ✅ EXCELLENT: Preload all resources at game start
protected async start(): Promise<void> {
await this.preloadAllResources();
}
private async preloadAllResources(): Promise<void> {
const resourcePaths = [
'sprites/character',
'sprites/enemies',
'audio/bgm',
'audio/sfx',
];
const promises = resourcePaths.map(path => this.preloadResource(path));
await Promise.all(promises);
console.log('All resources preloaded');
}
private async preloadResource(path: string): Promise<void> {
return new Promise((resolve, reject) => {
resources.load(path, (err, asset) => {
if (err) {
console.error(`Failed to load ${path}:`, err);
reject(err);
return;
}
this.loadedResources.set(path, asset);
resolve();
});
});
}
public getResource<T extends Asset>(path: string): T | null {
return (this.loadedResources.get(path) as T) ?? null;
}
protected onDestroy(): void {
// ✅ EXCELLENT: Release all loaded resources
for (const [path, asset] of this.loadedResources) {
asset.decRef();
}
this.loadedResources.clear();
}
}
// ❌ WRONG: Loading resources during gameplay
protected update(dt: number): void {
if (this.shouldSpawnEnemy()) {
// Loading during gameplay causes frame drops!
resources.load('sprites/enemy', SpriteFrame, (err, sprite) => {
this.spawnEnemy(sprite);
});
}
}
// ✅ BETTER: Preload and reuse
protected start(): void {
resources.load('sprites/enemy', SpriteFrame, (err, sprite) => {
this.enemySprite = sprite;
});
}
protected update(dt: number): void {
if (this.shouldSpawnEnemy() && this.enemySprite) {
this.spawnEnemy(this.enemySprite); // Instant, no loading
}
}
```
## Memory Management for Playables
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass } = _decorator;
@ccclass('MemoryOptimized')
export class MemoryOptimized extends Component {
// ✅ EXCELLENT: Use typed arrays for large datasets
private positions: Float32Array = new Float32Array(300); // 100 Vec3s
private velocities: Float32Array = new Float32Array(300);
// ✅ EXCELLENT: Reuse arrays instead of creating new ones
private readonly tempArray: number[] = [];
protected update(dt: number): void {
// Reuse array, don't allocate
this.tempArray.length = 0;
for (let i = 0; i < 100; i++) {
this.tempArray.push(i * dt);
}
}
// ✅ EXCELLENT: WeakMap for caches (automatic cleanup)
private readonly nodeCache: WeakMap<Node, CachedData> = new WeakMap();
public getCachedData(node: Node): CachedData | undefined {
return this.nodeCache.get(node);
}
protected onDestroy(): void {
// ✅ EXCELLENT: Clear references
this.tempArray.length = 0;
// WeakMap entries are auto-cleared when nodes are destroyed
}
}
```
## Summary: Playable Optimization Checklist
**DrawCall Batching (<10 target):**
- [ ] Use sprite atlases for all sprites (not individual textures)
- [ ] Prewarm sprite frame cache in onLoad()
- [ ] Group UI elements into single atlas
- [ ] Use same material for similar objects
**Animation Performance:**
- [ ] Enable GPU skinning (useBakedAnimation = true)
- [ ] Bake skeletal animations
- [ ] Limit simultaneous animations
**Object Pooling:**
- [ ] Pool bullets, effects, enemies (anything spawned frequently)
- [ ] Prewarm pools in onLoad() (at least 20 objects)
- [ ] Never instantiate/destroy during gameplay
**Update Loop:**
- [ ] Zero allocations in update()
- [ ] Throttle expensive operations (every 10-60 frames)
- [ ] Cache active object lists
- [ ] Reuse preallocated vectors/arrays
**Resource Management:**
- [ ] Preload all resources at game start
- [ ] Never load resources during gameplay
- [ ] Release resources in onDestroy()
- [ ] Use WeakMap for auto-cleanup caches
**Target: 60fps with <10 DrawCalls and <5MB bundle size for playable ads.**
```
### references/language/performance.md
```markdown
# TypeScript Performance Optimization
## Zero Allocations in update()
**Critical Rule**: Never allocate objects in `update()`, `lateUpdate()`, or any method called every frame.
```typescript
import { _decorator, Component, Node, Vec3, Quat } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('OptimizedController')
export class OptimizedController extends Component {
@property(Node)
private readonly targetNode: Node | null = null;
// ✅ EXCELLENT: Preallocated reusable objects
private readonly tempVec3: Vec3 = new Vec3();
private readonly tempQuat: Quat = new Quat();
private readonly tempVec3Array: Vec3[] = [];
// ✅ EXCELLENT: No allocations in update
protected update(dt: number): void {
if (!this.targetNode) return;
// Reuse preallocated vector
this.targetNode.getPosition(this.tempVec3);
this.tempVec3.y += 10 * dt;
this.targetNode.setPosition(this.tempVec3);
// Reuse preallocated quaternion
this.targetNode.getRotation(this.tempQuat);
Quat.rotateY(this.tempQuat, this.tempQuat, dt);
this.targetNode.setRotation(this.tempQuat);
}
// ✅ EXCELLENT: Reuse array instead of creating new one
public updateMultipleNodes(nodes: Node[]): void {
this.tempVec3Array.length = 0; // Clear without allocating
for (const node of nodes) {
node.getPosition(this.tempVec3);
this.tempVec3Array.push(this.tempVec3.clone());
}
}
}
// ❌ WRONG: Allocating in update
protected update(dt: number): void {
if (!this.targetNode) return;
// Creates new Vec3 every frame (60 allocations/second)
const currentPos = this.targetNode.position.clone();
currentPos.y += 10 * dt;
this.targetNode.setPosition(currentPos);
// Creates new array every frame
const positions = this.nodes.map(n => n.position.clone());
}
// ❌ WRONG: String concatenation in update
protected update(dt: number): void {
// Creates new string every frame
const debugInfo = `Position: ${this.node.position.x}, ${this.node.position.y}`;
console.log(debugInfo);
}
```
## Object Pooling Pattern
```typescript
import { _decorator, Component, Node, Prefab, instantiate, NodePool } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('BulletPool')
export class BulletPool extends Component {
@property(Prefab)
private readonly bulletPrefab: Prefab | null = null;
private readonly pool: NodePool = new NodePool();
private static readonly INITIAL_POOL_SIZE: number = 20;
// ✅ EXCELLENT: Prewarm pool on initialization
protected onLoad(): void {
if (!this.bulletPrefab) {
throw new Error('BulletPool: bulletPrefab is required');
}
for (let i = 0; i < BulletPool.INITIAL_POOL_SIZE; i++) {
const bullet = instantiate(this.bulletPrefab);
this.pool.put(bullet);
}
}
// ✅ EXCELLENT: Get from pool (no allocation if available)
public getBullet(): Node {
let bullet: Node;
if (this.pool.size() > 0) {
bullet = this.pool.get()!;
} else {
// Only allocate if pool is empty
if (!this.bulletPrefab) {
throw new Error('BulletPool: bulletPrefab is required');
}
bullet = instantiate(this.bulletPrefab);
}
bullet.active = true;
return bullet;
}
// ✅ EXCELLENT: Return to pool (no deallocation)
public returnBullet(bullet: Node): void {
bullet.active = false;
this.pool.put(bullet);
}
// ✅ EXCELLENT: Clear pool on cleanup
protected onDestroy(): void {
this.pool.clear();
}
}
// Usage in game
@ccclass('Gun')
export class Gun extends Component {
private readonly bulletPool!: BulletPool;
public shoot(): void {
// ✅ GOOD: Get from pool instead of instantiate
const bullet = this.bulletPool.getBullet();
bullet.setPosition(this.node.position);
// Set up bullet with timeout to return to pool
this.scheduleOnce(() => {
this.bulletPool.returnBullet(bullet);
}, 3.0);
}
}
// ❌ WRONG: Creating new instances every time
public shoot(): void {
// Allocates and deallocates constantly
const bullet = instantiate(this.bulletPrefab!);
bullet.setPosition(this.node.position);
this.scheduleOnce(() => {
bullet.destroy(); // Triggers garbage collection
}, 3.0);
}
```
## Caching Expensive Operations
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('EnemyManager')
export class EnemyManager extends Component {
@property([Node])
private readonly enemyNodes: Node[] = [];
// ✅ EXCELLENT: Cache component references
private readonly enemyControllers: EnemyController[] = [];
private cachedActiveEnemies: EnemyController[] = [];
private activeEnemiesDirty: boolean = true;
protected onLoad(): void {
// Cache component references on initialization
for (const node of this.enemyNodes) {
const controller = node.getComponent(EnemyController);
if (controller) {
this.enemyControllers.push(controller);
}
}
}
// ✅ EXCELLENT: Mark cache as dirty instead of recalculating
public onEnemyStateChanged(): void {
this.activeEnemiesDirty = true;
}
// ✅ EXCELLENT: Lazy recalculation only when needed
public getActiveEnemies(): EnemyController[] {
if (this.activeEnemiesDirty) {
this.cachedActiveEnemies = this.enemyControllers.filter(e => e.isActive);
this.activeEnemiesDirty = false;
}
return this.cachedActiveEnemies;
}
protected update(dt: number): void {
// ✅ GOOD: Use cached active enemies
const activeEnemies = this.getActiveEnemies();
for (const enemy of activeEnemies) {
enemy.update(dt);
}
}
}
// ❌ WRONG: Finding components every frame
protected update(dt: number): void {
for (const node of this.enemyNodes) {
const controller = node.getComponent(EnemyController); // Expensive lookup!
if (controller?.isActive) {
controller.update(dt);
}
}
}
// ❌ WRONG: Filtering every frame
protected update(dt: number): void {
const activeEnemies = this.enemyControllers.filter(e => e.isActive); // Allocates array every frame!
for (const enemy of activeEnemies) {
enemy.update(dt);
}
}
```
## Throttling Expensive Operations
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('AIController')
export class AIController extends Component {
private frameCount: number = 0;
private static readonly AI_UPDATE_INTERVAL: number = 10; // Every 10 frames
private static readonly PATHFINDING_INTERVAL: number = 60; // Every 60 frames (1 second at 60fps)
// ✅ EXCELLENT: Update AI every N frames, not every frame
protected update(dt: number): void {
this.frameCount++;
// Run expensive AI logic every 10 frames instead of every frame
if (this.frameCount % AIController.AI_UPDATE_INTERVAL === 0) {
this.updateAIDecision();
}
// Run very expensive pathfinding every 60 frames (1 second)
if (this.frameCount % AIController.PATHFINDING_INTERVAL === 0) {
this.recalculatePath();
}
// Cheap operations can run every frame
this.moveTowardsTarget(dt);
}
private updateAIDecision(): void {
// Expensive: Check all enemies, evaluate threats, etc.
}
private recalculatePath(): void {
// Very expensive: A* pathfinding
}
private moveTowardsTarget(dt: number): void {
// Cheap: Simple movement
}
}
// ❌ WRONG: Expensive operations every frame
protected update(dt: number): void {
this.recalculatePath(); // A* pathfinding 60 times per second!
this.updateAIDecision(); // Complex AI logic 60 times per second!
this.moveTowardsTarget(dt);
}
```
## Time-Based Throttling
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('PerformanceMonitor')
export class PerformanceMonitor extends Component {
private lastUpdateTime: number = 0;
private static readonly UPDATE_INTERVAL: number = 1.0; // 1 second
// ✅ EXCELLENT: Time-based throttling
protected update(dt: number): void {
const currentTime = Date.now() / 1000;
if (currentTime - this.lastUpdateTime >= PerformanceMonitor.UPDATE_INTERVAL) {
this.performExpensiveOperation();
this.lastUpdateTime = currentTime;
}
}
private performExpensiveOperation(): void {
// Expensive operation that runs once per second
}
}
// Alternative using scheduleOnce
@ccclass('TimerBased')
export class TimerBased extends Component {
private static readonly CHECK_INTERVAL: number = 2.0; // 2 seconds
protected start(): void {
this.scheduleCheckRecurring();
}
private scheduleCheckRecurring(): void {
this.performCheck();
this.scheduleOnce(this.scheduleCheckRecurring, TimerBased.CHECK_INTERVAL);
}
private performCheck(): void {
// Expensive check operation
}
}
```
## Avoid Expensive Lookups
```typescript
import { _decorator, Component, Node, find } from 'cc';
const { ccclass } = _decorator;
@ccclass('GameManager')
export class GameManager extends Component {
// ✅ EXCELLENT: Cache references in onLoad
private uiRootNode!: Node;
private playerNode!: Node;
private enemyNodes: Node[] = [];
protected onLoad(): void {
// Cache node references once
const uiRoot = find('Canvas/UI');
if (!uiRoot) {
throw new Error('GameManager: UI root not found');
}
this.uiRootNode = uiRoot;
const player = find('Canvas/Player');
if (!player) {
throw new Error('GameManager: Player not found');
}
this.playerNode = player;
// Cache array of enemy nodes
const enemyParent = find('Canvas/Enemies');
if (enemyParent) {
this.enemyNodes = enemyParent.children.slice();
}
}
protected update(dt: number): void {
// ✅ GOOD: Use cached references
this.updatePlayer(this.playerNode, dt);
this.updateEnemies(this.enemyNodes, dt);
}
}
// ❌ WRONG: Finding nodes every frame
protected update(dt: number): void {
const player = find('Canvas/Player'); // Expensive search every frame!
const enemies = find('Canvas/Enemies')?.children; // Expensive search every frame!
if (player) {
this.updatePlayer(player, dt);
}
if (enemies) {
this.updateEnemies(enemies, dt);
}
}
// ❌ WRONG: getComponent every frame
protected update(dt: number): void {
const playerController = this.playerNode.getComponent(PlayerController); // Expensive lookup!
playerController?.update(dt);
}
// ✅ BETTER: Cache component reference
private playerController!: PlayerController;
protected onLoad(): void {
const controller = this.playerNode.getComponent(PlayerController);
if (!controller) {
throw new Error('PlayerController not found');
}
this.playerController = controller;
}
protected update(dt: number): void {
this.playerController.update(dt);
}
```
## String Concatenation Performance
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('DebugDisplay')
export class DebugDisplay extends Component {
// ✅ EXCELLENT: Use template literals for readability
public getDebugInfo(player: Player): string {
return `Player: ${player.name}, HP: ${player.health}/${player.maxHealth}, Level: ${player.level}`;
}
// ✅ EXCELLENT: Build strings efficiently with array join (for large strings)
public generateReport(players: Player[]): string {
const lines: string[] = [];
lines.push('=== Player Report ===');
for (const player of players) {
lines.push(`${player.name}: Level ${player.level}, HP ${player.health}`);
}
lines.push('=== End Report ===');
return lines.join('\n');
}
// ✅ EXCELLENT: Avoid string operations in update loop
private debugText: string = '';
private frameCount: number = 0;
protected update(dt: number): void {
this.frameCount++;
// Only update debug text every 30 frames
if (this.frameCount % 30 === 0) {
this.debugText = this.generateDebugText();
}
}
}
// ❌ WRONG: String concatenation in loop
public generateReport(players: Player[]): string {
let report = '=== Player Report ===\n';
for (const player of players) {
report += `${player.name}: Level ${player.level}\n`; // Allocates new string each iteration
}
report += '=== End Report ===';
return report;
}
// ❌ WRONG: Building strings in update
protected update(dt: number): void {
this.debugText = `FPS: ${1/dt}, Position: ${this.node.position}`; // Allocates every frame
}
```
## Number Operations Performance
```typescript
import { _decorator, Component } from 'cc';
const { ccclass } = _decorator;
@ccclass('MathOptimizations')
export class MathOptimizations extends Component {
// ✅ EXCELLENT: Use multiplication instead of division
private static readonly INV_FRAME_RATE: number = 1 / 60;
public calculateTimedValue(value: number): number {
return value * MathOptimizations.INV_FRAME_RATE; // Faster than value / 60
}
// ✅ EXCELLENT: Use bitwise operations for integer math
public fastFloor(value: number): number {
return value | 0; // Faster than Math.floor for positive numbers
}
public isPowerOfTwo(value: number): boolean {
return (value & (value - 1)) === 0; // Faster than logarithm check
}
// ✅ EXCELLENT: Cache expensive math operations
private readonly sinCache: Map<number, number> = new Map();
public getCachedSin(angle: number): number {
if (!this.sinCache.has(angle)) {
this.sinCache.set(angle, Math.sin(angle));
}
return this.sinCache.get(angle)!;
}
// ✅ EXCELLENT: Use squared distance to avoid sqrt
public isWithinRange(pos1: Vec3, pos2: Vec3, range: number): boolean {
const dx = pos2.x - pos1.x;
const dy = pos2.y - pos1.y;
const dz = pos2.z - pos1.z;
const distSquared = dx * dx + dy * dy + dz * dz;
const rangeSquared = range * range;
return distSquared <= rangeSquared; // No sqrt needed
}
}
// ❌ WRONG: Using expensive operations
public isWithinRange(pos1: Vec3, pos2: Vec3, range: number): boolean {
const distance = Vec3.distance(pos1, pos2); // Uses sqrt internally
return distance <= range;
}
// ❌ WRONG: Division in hot path
protected update(dt: number): void {
const value = this.baseValue / 60; // Division is slower than multiplication
}
```
## Memory Management Best Practices
```typescript
import { _decorator, Component, Node } from 'cc';
const { ccclass } = _decorator;
@ccclass('ResourceManager')
export class ResourceManager extends Component {
private readonly loadedAssets: Map<string, Asset> = new Map();
private readonly nodeReferences: Set<Node> = new Set();
// ✅ EXCELLENT: Clear references on cleanup
protected onDestroy(): void {
// Clear maps and sets
this.loadedAssets.clear();
this.nodeReferences.clear();
// Remove event listeners
this.node.off(Node.EventType.TOUCH_START);
}
// ✅ EXCELLENT: Remove unused assets
public unloadAsset(assetId: string): void {
const asset = this.loadedAssets.get(assetId);
if (asset) {
asset.decRef(); // Release reference
this.loadedAssets.delete(assetId);
}
}
// ✅ EXCELLENT: Weak references for caches
private readonly weakNodeCache: WeakMap<Node, CachedData> = new WeakMap();
public getCachedData(node: Node): CachedData | undefined {
return this.weakNodeCache.get(node);
}
public setCachedData(node: Node, data: CachedData): void {
this.weakNodeCache.set(node, data);
// Node is garbage collected → cache entry is automatically removed
}
}
// ❌ WRONG: Memory leaks
protected onDestroy(): void {
// Forgot to clear references - memory leak!
// this.loadedAssets.clear();
// this.nodeReferences.clear();
}
// ❌ WRONG: Strong references prevent garbage collection
private readonly nodeCache: Map<Node, CachedData> = new Map();
// Nodes are never garbage collected even when destroyed
```
## Summary: Performance Checklist
**Critical for playable ads (<5MB, <10 DrawCalls):**
- [ ] Zero allocations in update() (preallocate and reuse)
- [ ] Object pooling for frequently created/destroyed objects
- [ ] Cache component and node references (no getComponent in update)
- [ ] Throttle expensive operations (every N frames, not every frame)
- [ ] Avoid string operations in hot paths
- [ ] Use multiplication instead of division
- [ ] Use squared distance instead of distance (avoid sqrt)
- [ ] Clear references in onDestroy() to prevent memory leaks
- [ ] Use WeakMap for caches that should be garbage collected
- [ ] Array.length = 0 to clear arrays (don't create new arrays)
**Performance is critical for smooth 60fps playable ads.**
```
### references/framework/size-optimization.md
```markdown
# Bundle Size Optimization (<5MB Target)
## Texture Compression (Biggest Impact)
**Target: <5MB total bundle size for playable ads**
Texture compression is the single biggest factor in bundle size. Enable compression for all platforms.
### Build Settings Configuration
```json
// Project Settings → Build → Web Mobile
{
"textureCompression": {
"web-mobile": "auto", // Auto-select best compression
"web-desktop": "auto",
"android": "etc1", // ETC1 for Android
"ios": "pvrtc" // PVRTC for iOS
},
"packAutoAtlas": true, // Auto-generate atlases
"md5Cache": false, // Disable for smaller output
"inlineSpriteFrames": true // Reduce file count
}
```
### Texture Size Guidelines
```typescript
// ✅ EXCELLENT: Optimal texture sizes for playables
// Character sprites: 512x512 max (often 256x256 is enough)
// UI elements: 256x256 max
// Backgrounds: 1024x1024 max (or use tiled smaller textures)
// Effects: 128x128 or 256x256
// Icons: 64x64 or 128x128
// ❌ WRONG: Oversized textures
// - 2048x2048 for small character sprites
// - High-res images that won't be seen at that scale
// Use appropriate sizes for display resolution
```
## Asset Optimization Priority
### 1. Textures (50-60% of bundle)
```typescript
// ✅ EXCELLENT: Sprite atlas configuration
// Combine multiple small textures into single atlas
// - Character animations: single atlas
// - UI elements: single atlas
// - Effects: single atlas
// Auto-atlas settings (Project Settings):
// - Max Width: 2048
// - Max Height: 2048
// - Padding: 2
// - Allow Rotation: true
// - Force Square: false
// ❌ WRONG: Individual texture files
// Each separate texture = separate HTTP request + worse compression
```
### 2. Audio (20-30% of bundle)
```typescript
// ✅ EXCELLENT: Audio optimization
// - Format: MP3 or OGG (not WAV)
// - Background music: 128kbps max, short loops (<30 seconds)
// - Sound effects: 64kbps, very short (<2 seconds)
// ❌ WRONG: Uncompressed audio
// - WAV files: 10-20x larger than compressed
// - Long music tracks: use short loops
// - High bitrate: 320kbps unnecessary for playables
```
### 3. Code (5-10% of bundle)
```typescript
// ✅ EXCELLENT: Code minification
// rollup.config.js or webpack.config.js
export default {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true, // Remove console.log
drop_debugger: true, // Remove debugger
dead_code: true, // Remove unreachable code
unused: true // Remove unused variables
},
mangle: { toplevel: true } // Shorten variable names
}
})
]
}
};
// ✅ EXCELLENT: Import only what you need
import { Vec3, Node } from 'cc'; // Specific imports
// ❌ WRONG: Import entire module
import * as cc from 'cc'; // Imports everything (larger bundle)
```
### 4. Fonts (5-10% of bundle)
```typescript
// ✅ EXCELLENT: Bitmap fonts for playables
// - Pre-render characters to texture
// - Include only needed characters: "0123456789,."
// - Much smaller than TTF fonts
// Create bitmap font:
// 1. Use BMFont tool or online generator
// 2. Include only needed characters
// 3. Export as .fnt + .png
// 4. Import to Cocos Creator as BitmapFont
// ❌ WRONG: TTF fonts
// - Large file size (hundreds of KB)
// - System fonts vary by platform
// - Use bitmap fonts for playables
```
## Build Configuration for Minimum Size
```json
// Project Settings → Build → Web Mobile
{
// Bundle settings
"inlineSpriteFrames": true, // Reduce file count
"md5Cache": false, // Disable MD5 in filenames
"mainBundleCompressionType": "default",
"mainBundleIsRemote": false,
// Code optimization
"debug": false, // Disable debug mode
"sourceMaps": false, // Disable source maps
"separateEngine": false, // Include engine in bundle
// Texture optimization
"packAutoAtlas": true, // Auto-generate atlases
"textureCompression": "auto", // Enable compression
// Feature exclusions
"excludeScenes": [], // Remove unused scenes
"useBuiltinServer": false // Playables don't need server
}
```
## Removing Unused Assets
```typescript
// ✅ EXCELLENT: Regular asset cleanup
// 1. Use Cocos Creator's "Find References" feature
// - Right-click asset → Find References
// - Delete if no references found
// 2. Check build output
// - Review build folder size after each build
// - Identify largest files
// - Remove unused assets
// 3. Remove debug assets before build
// - Test levels
// - Debug sprites and textures
// - Development-only tools
// - Temporary assets
// ❌ WRONG: Keep all assets "just in case"
// - Unused textures add unnecessary size
// - Clean up regularly during development
```
## Real-World Example: Size Breakdown
```typescript
// Target: <5MB playable bundle
// Typical optimized breakdown:
// Textures: 2.5MB (50%)
// - Character sprites: 800KB (sprite atlas, ETC1 compressed)
// - UI elements: 600KB (sprite atlas, ETC1 compressed)
// - Background: 700KB (1024x1024, compressed, or tiled)
// - Effects: 400KB (sprite atlas, compressed)
// Code: 400KB (8%)
// - Cocos engine: 200KB (minified, tree-shaken)
// - Game logic: 200KB (minified, dead code removed)
// Audio: 1.5MB (30%)
// - Background music: 1MB (MP3, 128kbps, 60s loop)
// - Sound effects: 500KB (MP3, 64kbps, 10 short clips)
// Other: 600KB (12%)
// - Bitmap fonts: 200KB (only needed characters)
// - Config files: 100KB (JSON, minified)
// - Misc assets: 300KB
// Total: 5.0MB (within ad network limit)
// ❌ BAD EXAMPLE: Unoptimized (12MB+)
// - Textures: 8MB (no compression, individual files)
// - Audio: 3MB (WAV files, long tracks)
// - Code: 800KB (no minification, dev mode)
// - Fonts: 400KB (TTF fonts)
// Total: 12.2MB (rejected by ad networks!)
```
## Monitoring Bundle Size
```bash
# ✅ EXCELLENT: Monitor size regularly
# 1. Check build output size
du -sh build/web-mobile/
# 2. Break down by asset type
du -sh build/web-mobile/assets/
du -sh build/web-mobile/src/
# 3. Find largest files
find build/web-mobile -type f -exec du -h {} \; | sort -rh | head -20
# 4. Set size budget in CI/CD
# Fail build if bundle >5MB
# Alert if bundle >4.5MB (warning threshold)
```
## Lazy Loading Pattern (Optional)
```typescript
import { _decorator, Component, resources, Prefab } from 'cc';
const { ccclass } = _decorator;
@ccclass('LazyLoader')
export class LazyLoader extends Component {
// ✅ EXCELLENT: Load levels on demand
// For playables with multiple levels, load only current level
private levelPrefabs: Map<number, Prefab> = new Map();
public async loadLevel(levelId: number): Promise<void> {
if (this.levelPrefabs.has(levelId)) {
return; // Already loaded
}
const path = `levels/level_${levelId}`;
return new Promise((resolve, reject) => {
resources.load(path, Prefab, (err, prefab) => {
if (err) {
reject(err);
return;
}
this.levelPrefabs.set(levelId, prefab);
resolve();
});
});
}
// ✅ GOOD: Unload previous level
public async switchLevel(fromLevel: number, toLevel: number): Promise<void> {
const prevPrefab = this.levelPrefabs.get(fromLevel);
if (prevPrefab) {
prevPrefab.decRef();
this.levelPrefabs.delete(fromLevel);
}
await this.loadLevel(toLevel);
}
}
// ❌ WRONG: Loading all levels at start
// - Increases initial bundle size
// - Longer load time
// - Only load what's needed for first level
```
## Size Optimization Checklist
**🔴 Critical (Biggest Impact):**
- [ ] Enable texture compression (auto or platform-specific)
- [ ] Use sprite atlases (combine textures)
- [ ] Reduce texture dimensions (512x512 max for characters)
- [ ] Compress audio (MP3/OGG, 64-128kbps)
- [ ] Remove unused assets
**🟡 Important:**
- [ ] Enable code minification (drop_console, dead_code removal)
- [ ] Use bitmap fonts (not TTF)
- [ ] Disable source maps in production
- [ ] Import specific modules (tree shaking)
- [ ] Remove debug/test assets
**🟢 Nice to Have:**
- [ ] Lazy load levels (if multiple levels)
- [ ] Monitor bundle size in CI/CD
- [ ] Set size budget alerts (<5MB hard limit)
- [ ] Track size trends over time
**Target: <5MB total bundle size for playable ad approval.**
```
### references/review/architecture-review.md
```markdown
# Cocos Creator Architecture Review
This review focuses on Cocos Creator-specific architectural issues including component lifecycle violations, event management problems, and performance issues specific to playable ads.
## Component Lifecycle Violations
### Accessing Components in onLoad
```typescript
// ❌ CRITICAL: Accessing other components in onLoad
@ccclass('BadLifecycle')
export class BadLifecycle extends Component {
@property(Node)
private playerNode: Node | null = null;
protected onLoad(): void {
// WRONG: Other components may not be loaded yet
const controller = this.playerNode!.getComponent(PlayerController);
controller.initialize(); // May be undefined!
}
}
// ✅ CORRECT: Access components in start()
@ccclass('GoodLifecycle')
export class GoodLifecycle extends Component {
@property(Node)
private readonly playerNode: Node | null = null;
private playerController!: PlayerController;
protected onLoad(): void {
if (!this.playerNode) {
throw new Error('GoodLifecycle: playerNode is required');
}
}
protected start(): void {
const controller = this.playerNode!.getComponent(PlayerController);
if (!controller) {
throw new Error('PlayerController not found');
}
this.playerController = controller;
this.playerController.initialize();
}
}
// Severity: 🔴 Critical
// Impact: Undefined behavior, crashes
// Fix: Move component access from onLoad() to start()
```
### Event Listener Memory Leaks
```typescript
// ❌ CRITICAL: Not unregistering event listeners
@ccclass('EventLeakBad')
export class EventLeakBad extends Component {
protected onEnable(): void {
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
EventManager.on(GameEvent.SCORE_CHANGED, this.onScoreChanged, this);
}
// MISSING: onDisable() - memory leak!
}
// ✅ CORRECT: Always unregister in onDisable
@ccclass('EventLeakGood')
export class EventLeakGood extends Component {
protected onEnable(): void {
this.node.on(Node.EventType.TOUCH_START, this.onTouchStart, this);
EventManager.on(GameEvent.SCORE_CHANGED, this.onScoreChanged, this);
}
protected onDisable(): void {
this.node.off(Node.EventType.TOUCH_START, this.onTouchStart, this);
EventManager.off(GameEvent.SCORE_CHANGED, this.onScoreChanged, this);
}
private onTouchStart(event: EventTouch): void {}
private onScoreChanged(data: ScoreChangedEvent): void {}
}
// Severity: 🔴 Critical
// Impact: Memory leaks, performance degradation
// Fix: Always implement onDisable() to unregister listeners
```
### Missing Required Reference Validation
```typescript
// ❌ CRITICAL: No validation of required references
@ccclass('NoValidation')
export class NoValidation extends Component {
@property(Node)
private targetNode: Node | null = null;
protected onLoad(): void {
this.targetNode!.setPosition(0, 0, 0); // Will crash if null
}
}
// ✅ CORRECT: Validate in onLoad
@ccclass('WithValidation')
export class WithValidation extends Component {
@property(Node)
private readonly targetNode: Node | null = null;
protected onLoad(): void {
if (!this.targetNode) {
throw new Error('WithValidation: targetNode is required');
}
this.targetNode.setPosition(0, 0, 0);
}
}
// Severity: 🔴 Critical
// Impact: Runtime crashes with unhelpful errors
// Fix: Validate all required @property references in onLoad()
```
### Resource Cleanup Violations
```typescript
// ❌ CRITICAL: Not releasing resources
@ccclass('ResourceLeakBad')
export class ResourceLeakBad extends Component {
private readonly loadedAssets: Map<string, Asset> = new Map();
protected onDestroy(): void {
// MISSING: decRef() and clear()
}
}
// ✅ CORRECT: Complete cleanup
@ccclass('ResourceLeakGood')
export class ResourceLeakGood extends Component {
private readonly loadedAssets: Map<string, Asset> = new Map();
protected onDestroy(): void {
for (const [id, asset] of this.loadedAssets) {
asset.decRef();
}
this.loadedAssets.clear();
this.unscheduleAllCallbacks();
}
}
// Severity: 🔴 Critical
// Impact: Memory leaks
// Fix: Release resources and clear collections in onDestroy()
```
## Performance Violations (Playable-Specific)
### Allocations in Update Loop
```typescript
// ❌ CRITICAL: Allocating every frame
@ccclass('UpdateAllocationsBad')
export class UpdateAllocationsBad extends Component {
protected update(dt: number): void {
const pos = this.node.position.clone(); // 60 allocations/second
pos.y += 10 * dt;
this.node.setPosition(pos);
}
}
// ✅ CORRECT: Preallocate and reuse
@ccclass('UpdateAllocationsGood')
export class UpdateAllocationsGood extends Component {
private readonly tempVec3: Vec3 = new Vec3();
protected update(dt: number): void {
this.node.getPosition(this.tempVec3);
this.tempVec3.y += 10 * dt;
this.node.setPosition(this.tempVec3);
}
}
// Severity: 🔴 Critical
// Impact: Frame drops, GC pauses
// Fix: Preallocate objects, reuse in update
```
### Component Lookup in Update
```typescript
// ❌ IMPORTANT: getComponent in update
@ccclass('ComponentLookupBad')
export class ComponentLookupBad extends Component {
@property(Node)
private playerNode: Node | null = null;
protected update(dt: number): void {
const controller = this.playerNode!.getComponent(PlayerController); // Expensive!
controller?.update(dt);
}
}
// ✅ CORRECT: Cache component reference
@ccclass('ComponentLookupGood')
export class ComponentLookupGood extends Component {
@property(Node)
private readonly playerNode: Node | null = null;
private playerController!: PlayerController;
protected start(): void {
if (!this.playerNode) {
throw new Error('playerNode is required');
}
const controller = this.playerNode.getComponent(PlayerController);
if (!controller) {
throw new Error('PlayerController not found');
}
this.playerController = controller;
}
protected update(dt: number): void {
this.playerController.update(dt);
}
}
// Severity: 🟡 Important
// Impact: Significant performance overhead
// Fix: Cache component references in start()
```
## Summary: Architecture Review Checklist
**🔴 Critical (Must Fix):**
- [ ] No component access in onLoad() (use start())
- [ ] All event listeners unregistered in onDisable()
- [ ] Required @property references validated in onLoad()
- [ ] Resources released in onDestroy()
- [ ] Zero allocations in update() loop
- [ ] readonly used for @property fields not reassigned
**🟡 Important (Should Fix):**
- [ ] Component references cached (not getComponent in update)
- [ ] Expensive operations throttled (every N frames)
- [ ] Node references cached (not find() in update)
- [ ] Arrays cleared with .length = 0 (not new array)
**🟢 Nice to Have:**
- [ ] Object pooling for frequent spawn/despawn
- [ ] WeakMap for auto-cleanup caches
- [ ] Disposable pattern for subscription management
**Always fix lifecycle and event cleanup issues - they cause crashes and memory leaks.**
```
### references/review/quality-review.md
```markdown
# TypeScript Quality Review
This review focuses on TypeScript code quality issues including access modifiers, strict mode compliance, error handling, and code hygiene.
## TypeScript Strict Mode Violations
```typescript
// ❌ CRITICAL: Strict mode disabled
// tsconfig.json
{
"compilerOptions": {
"strict": false // Bad!
}
}
// ✅ CORRECT: Enable strict mode
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true
}
}
// Severity: 🔴 Critical
// Fix: Enable strict mode in tsconfig.json
```
## Access Modifier Violations
```typescript
// ❌ CRITICAL: Missing access modifiers
@ccclass('NoModifiers')
export class NoModifiers extends Component {
playerNode: Node | null = null; // Implicitly public!
currentHealth: number = 100; // Implicitly public!
updateHealth(value: number) { // Implicitly public!
this.currentHealth = value;
}
}
// ✅ CORRECT: Explicit modifiers
@ccclass('WithModifiers')
export class WithModifiers extends Component {
@property(Node)
private readonly playerNode: Node | null = null;
private currentHealth: number = 100;
public updateHealth(value: number): void {
this.currentHealth = value;
}
}
// Severity: 🔴 Critical
// Fix: Add access modifiers (public/private/protected) to all members
```
## Silent Error Handling
```typescript
// ❌ CRITICAL: Silent failures
@ccclass('SilentErrors')
export class SilentErrors extends Component {
public getPlayer(id: string): Player | undefined {
const player = this.players.get(id);
return player; // Caller doesn't know why it failed
}
}
// ✅ CORRECT: Throw exceptions
@ccclass('ThrowExceptions')
export class ThrowExceptions extends Component {
public getPlayer(id: string): Player {
const player = this.players.get(id);
if (!player) {
throw new Error(`Player not found: ${id}`);
}
return player;
}
}
// Severity: 🔴 Critical
// Fix: Throw exceptions for errors, not silent failures
```
## console.log in Production
```typescript
// ❌ CRITICAL: Unconditional console.log
@ccclass('ConsoleLogBad')
export class ConsoleLogBad extends Component {
protected update(dt: number): void {
console.log('Update'); // In production build!
}
}
// ✅ CORRECT: Conditional or removed
@ccclass('ConsoleLogGood')
export class ConsoleLogGood extends Component {
protected update(dt: number): void {
if (CC_DEBUG) {
console.log('Update');
}
}
}
// Severity: 🔴 Critical (for playables)
// Impact: Bundle size increase, performance
// Fix: Wrap in CC_DEBUG or remove entirely
```
## Inline Comments Instead of Descriptive Names
```typescript
// ❌ IMPORTANT: Comments explaining unclear code
@ccclass('InlineCommentsBad')
export class InlineCommentsBad extends Component {
private h: number = 100; // health
public td(a: number): void { // take damage
this.h = this.h - a; // subtract
if (this.h <= 0) { // dead
this.hd(); // handle death
}
}
}
// ✅ CORRECT: Self-explanatory names
@ccclass('InlineCommentsGood')
export class InlineCommentsGood extends Component {
private currentHealth: number = 100;
public takeDamage(amount: number): void {
this.currentHealth -= amount;
if (this.isDead()) {
this.handleDeath();
}
}
private isDead(): boolean {
return this.currentHealth <= 0;
}
private handleDeath(): void {
// Implementation
}
}
// Severity: 🟡 Important
// Fix: Use descriptive names, remove inline comments
```
## Missing readonly/const
```typescript
// ❌ IMPORTANT: Mutable when should be immutable
@ccclass('MissingReadonly')
export class MissingReadonly extends Component {
@property(Node)
private targetNode: Node | null = null; // Should be readonly
private maxHealth: number = 100; // Should be static readonly
}
// ✅ CORRECT: Use readonly/const
@ccclass('WithReadonly')
export class WithReadonly extends Component {
@property(Node)
private readonly targetNode: Node | null = null;
private static readonly MAX_HEALTH: number = 100;
}
// Severity: 🟡 Important
// Fix: Add readonly to fields not reassigned, use static readonly for constants
```
## Using `any` Type
```typescript
// ❌ IMPORTANT: Using any without justification
@ccclass('UsingAny')
export class UsingAny extends Component {
private data: any = {}; // Type safety lost
public processData(input: any): any {
return input; // No type checking
}
}
// ✅ CORRECT: Use proper types
interface PlayerData {
id: string;
name: string;
level: number;
}
@ccclass('WithTypes')
export class WithTypes extends Component {
private data: Map<string, PlayerData> = new Map();
public processData(input: PlayerData): PlayerData {
return input;
}
}
// Severity: 🟡 Important
// Fix: Define proper types and interfaces, avoid `any`
```
## Summary: Quality Review Checklist
**🔴 Critical (Must Fix):**
- [ ] TypeScript strict mode enabled in tsconfig.json
- [ ] All members have access modifiers (public/private/protected)
- [ ] Exceptions thrown for errors (no silent failures)
- [ ] console.log removed or wrapped in CC_DEBUG
- [ ] No nullable warnings (proper null handling)
**🟡 Important (Should Fix):**
- [ ] readonly used for non-reassigned fields
- [ ] const used for constants (not let)
- [ ] No inline comments (self-explanatory code)
- [ ] Optional chaining (?.) for safe access
- [ ] Nullish coalescing (??) for defaults
- [ ] No `any` types without justification
**🟢 Nice to Have:**
- [ ] Arrow functions for callbacks
- [ ] Destructuring for cleaner code
- [ ] Type guards for type safety
- [ ] Utility types (Partial, Required, etc.)
**Code quality is the foundation - fix these issues before performance optimization.**
```
### references/review/performance-review.md
```markdown
# Playable Ads Performance Review
This review focuses on performance issues specific to playable ads including DrawCall optimization, bundle size, update loop performance, and resource management.
## DrawCall Explosion (Critical for Playables)
**Target: <10 DrawCalls for 60fps playables**
```typescript
// ❌ CRITICAL: Individual textures (multiple DrawCalls)
@ccclass('DrawCallBad')
export class DrawCallBad extends Component {
@property(SpriteFrame)
private sprite1: SpriteFrame | null = null; // DrawCall 1
@property(SpriteFrame)
private sprite2: SpriteFrame | null = null; // DrawCall 2
@property(SpriteFrame)
private sprite3: SpriteFrame | null = null; // DrawCall 3
// 10 sprites = 10 DrawCalls! (BAD)
}
// ✅ CORRECT: Sprite atlas (single DrawCall)
@ccclass('DrawCallGood')
export class DrawCallGood extends Component {
@property(SpriteAtlas)
private readonly characterAtlas: SpriteAtlas | null = null; // 1 DrawCall for all
}
// Severity: 🔴 Critical
// Impact: Frame drops, poor performance
// Target: <10 DrawCalls total
// Fix: Use sprite atlases for all sprites
```
## Update Loop Allocations
```typescript
// ❌ CRITICAL: Allocating in update
@ccclass('UpdateAllocationsBad')
export class UpdateAllocationsBad extends Component {
protected update(dt: number): void {
// Creates new Vec3 every frame
const pos = this.node.position.clone(); // 60 allocations/second!
pos.y += 10 * dt;
this.node.setPosition(pos);
// Creates array every frame
const enemies = this.getAllEnemies().filter(e => e.active); // 60 arrays/second!
}
}
// ✅ CORRECT: Zero allocations
@ccclass('UpdateAllocationsGood')
export class UpdateAllocationsGood extends Component {
private readonly tempVec3: Vec3 = new Vec3();
private readonly activeEnemies: Enemy[] = [];
private cacheRDirty: boolean = true;
protected update(dt: number): void {
// Reuse preallocated vector
this.node.getPosition(this.tempVec3);
this.tempVec3.y += 10 * dt;
this.node.setPosition(this.tempVec3);
// Use cached array
const enemies = this.getActiveEnemies();
}
private getActiveEnemies(): Enemy[] {
if (this.cacheDirty) {
this.activeEnemies.length = 0;
// Rebuild cache
this.cacheDirty = false;
}
return this.activeEnemies;
}
}
// Severity: 🔴 Critical
// Impact: Frame drops, GC pauses
// Fix: Preallocate objects, reuse in update
```
## No Object Pooling
```typescript
// ❌ IMPORTANT: instantiate/destroy in gameplay
@ccclass('NoPoolingBad')
export class NoPoolingBad extends Component {
public shoot(): void {
const bullet = instantiate(this.bulletPrefab!); // Allocates
this.scheduleOnce(() => {
bullet.destroy(); // GC overhead
}, 2.0);
}
}
// ✅ CORRECT: Object pooling
@ccclass('NoPoolingGood')
export class NoPoolingGood extends Component {
private readonly bulletPool: NodePool = new NodePool();
protected onLoad(): void {
// Prewarm pool
for (let i = 0; i < 20; i++) {
const bullet = instantiate(this.bulletPrefab!);
this.bulletPool.put(bullet);
}
}
public shoot(): void {
const bullet = this.bulletPool.get() ?? instantiate(this.bulletPrefab!);
this.scheduleOnce(() => {
this.bulletPool.put(bullet);
}, 2.0);
}
}
// Severity: 🟡 Important
// Impact: Allocations, GC pauses
// Fix: Implement object pooling for frequent spawn/despawn
```
## Unthrottled Expensive Operations
```typescript
// ❌ IMPORTANT: Expensive operations every frame
@ccclass('UnthrottledBad')
export class UnthrottledBad extends Component {
protected update(dt: number): void {
this.recalculatePathfinding(); // A* every frame (60 times/second)!
this.updateComplexAI(); // Expensive every frame!
}
}
// ✅ CORRECT: Throttle expensive operations
@ccclass('UnthrottledGood')
export class UnthrottledGood extends Component {
private frameCount: number = 0;
protected update(dt: number): void {
this.frameCount++;
// Pathfinding once per second
if (this.frameCount % 60 === 0) {
this.recalculatePathfinding();
}
// AI 6 times per second
if (this.frameCount % 10 === 0) {
this.updateComplexAI();
}
// Cheap operations every frame
this.moveTowardsTarget(dt);
}
}
// Severity: 🟡 Important
// Impact: Poor performance, frame drops
// Fix: Throttle to every N frames (10-60)
```
## Bundle Size >5MB
```typescript
// ❌ CRITICAL: Bundle exceeds playable limit
// Build output: 7.2MB (too large for most ad networks!)
// Common causes:
// 1. Uncompressed textures → Enable compression
// 2. Oversized textures → Reduce to 512x512 max
// 3. Uncompressed audio → Use MP3/OGG at 64-128kbps
// 4. Unused assets → Remove from project
// 5. No code minification → Enable in build settings
// ✅ CORRECT: Optimized to <5MB
// - Enable texture compression (Project Settings)
// - Use sprite atlases (combine textures)
// - Compress audio (64-128kbps)
// - Remove unused assets
// - Enable code minification (drop_console, dead_code)
// Severity: 🔴 Critical
// Impact: Playable rejected by ad networks
// Target: <5MB total bundle
// Fix: Apply size optimization techniques
```
## Loading Resources During Gameplay
```typescript
// ❌ IMPORTANT: Loading during gameplay
@ccclass('LoadingInGameplayBad')
export class LoadingInGameplayBad extends Component {
protected update(dt: number): void {
if (this.shouldSpawnEnemy()) {
// Loading causes frame drop!
resources.load('sprites/enemy', SpriteFrame, (err, sprite) => {
this.spawnEnemy(sprite);
});
}
}
}
// ✅ CORRECT: Preload at startup
@ccclass('LoadingInGameplayGood')
export class LoadingInGameplayGood extends Component {
private enemySprite: SpriteFrame | null = null;
protected start(): void {
// Preload once
resources.load('sprites/enemy', SpriteFrame, (err, sprite) => {
if (!err) {
this.enemySprite = sprite;
}
});
}
protected update(dt: number): void {
if (this.shouldSpawnEnemy() && this.enemySprite) {
this.spawnEnemy(this.enemySprite); // Instant, no loading
}
}
}
// Severity: 🟡 Important
// Impact: Frame drops during loading
// Fix: Preload all resources at startup
```
## GPU Skinning Disabled
```typescript
// ❌ IMPORTANT: CPU skinning (slower)
@ccclass('CPUSkinningBad')
export class CPUSkinningBad extends Component {
@property(SkeletalAnimation)
private skeleton: SkeletalAnimation | null = null;
protected onLoad(): void {
// Using default CPU skinning (slower)
}
}
// ✅ CORRECT: Enable GPU skinning
@ccclass('GPUSkinningGood')
export class GPUSkinningGood extends Component {
@property(SkeletalAnimation)
private readonly skeleton: SkeletalAnimation | null = null;
protected onLoad(): void {
if (this.skeleton) {
// GPU handles bone transformations (faster)
this.skeleton.useBakedAnimation = true;
}
}
}
// Severity: 🟢 Nice to Have
// Impact: Better performance for skeletal animations
// Fix: Enable useBakedAnimation for GPU skinning
```
## Summary: Performance Review Checklist
**🔴 Critical (Must Fix):**
- [ ] DrawCall count <10 (use sprite atlases)
- [ ] Zero allocations in update() loop
- [ ] Bundle size <5MB total
- [ ] No loading resources during gameplay
**🟡 Important (Should Fix):**
- [ ] Object pooling for bullets, effects, enemies
- [ ] Expensive operations throttled (every 10-60 frames)
- [ ] Component references cached (not getComponent in update)
- [ ] Node references cached (not find() in update)
**🟢 Nice to Have:**
- [ ] GPU skinning enabled (useBakedAnimation = true)
- [ ] Texture dimensions optimized (512x512 max)
- [ ] Audio compressed (64-128kbps)
- [ ] WeakMap for auto-cleanup caches
**Performance targets: 60fps, <10 DrawCalls, <5MB bundle for playable ads.**
```