t1k:cocos:base:code-conventions
| Field | Value |
|---|---|
| Module | base |
| Version | 3.3.2 |
| Effort | low |
| Tools | — |
Keywords: ccclass, code conventions, coding standards, collider, collision group, decorator, lifecycle order, naming, typescript
How to invoke
Section titled “How to invoke”/t1k:cocos:base:code-conventionsCocos Creator TypeScript Code Conventions
Section titled “Cocos Creator TypeScript Code Conventions”Cocos Creator 3.x + TypeScript. Same game-dev mindset as the Unity C# conventions — the syntax differs, the discipline does not: self-documenting names, no hardcoded values, one responsibility per file, decouple via signals, clean up on teardown.
Triggers
Section titled “Triggers”- TypeScript code, naming, convention, style, constant, hardcoded, magic number
- new class, new file, new component, new service, new controller, refactor
- code review, code quality, code standards,
@ccclass, SignalBus
Naming Conventions (TypeScript + Cocos)
Section titled “Naming Conventions (TypeScript + Cocos)”Casing Rules
Section titled “Casing Rules”| Element | Casing | Example |
|---|---|---|
| Class, Component, Service | PascalCase + semantic suffix | ColorService, TimerComponent |
| Interface | PascalCase, NO I prefix | HealthProvider (not IHealthProvider) |
| Type alias | PascalCase | SpawnOptions |
| Enum | PascalCase name | AttackType |
| Enum member | PascalCase name, lowercase string value | Brown = 'brown' |
| Public method / property | camelCase | calculateDamage(), maxHealth |
| Private method | camelCase | applyKnockback() |
| Private field | _camelCase (underscore prefix) | _attackTimer |
| Parameter / local | camelCase | targetNode, closestEnemy |
| Constant (module/config) | UPPER_SNAKE_CASE | MAX_STACK_COUNT |
readonly config value | UPPER_SNAKE_CASE or camelCase | readonly SPAWN_RATE = 2 |
| Generic type parameter | TPascalCase | TComponent |
| Callback method | on* or handle* prefix | onWeaponPlaced, handleTimerEnd |
Note — deviates from some TS style guides on purpose: private fields use the underscore prefix (
_camelCase). This is the studio standard for Cocos/TS and is mandatory. Enum string values are lowercase for stable serialization (BROWN = 'brown').
Cocos-Specific Naming
Section titled “Cocos-Specific Naming”| Element | Pattern | Example |
|---|---|---|
| Component (UI / scene-attached) | Noun + Component/View/Controller | PlayerController, HpBarView |
| Service (business logic singleton) | Noun + Service | ColorService, AudioService |
| Signal class | Noun + Signal | WinSignal, ColorPickedSignal |
| Config / ScriptableObject-like | Noun + Config | GameConfig, ArenaConfig |
| File | {Feature}{Suffix}.ts (matches type) | ColorService.ts, TimerComponent.ts |
Reusable Framework vs Game-Specific Code (CRITICAL)
Section titled “Reusable Framework vs Game-Specific Code (CRITICAL)”- Framework / shared packages (e.g.
PLAGameFoundation, sharedutils/): NEVER bake a game/demo name into a type. Names must read correctly in a completely different game.- BAD:
ColorFitSignalBus,BeadsOutPool - GOOD:
SignalBus,ObjectPool<T> - Test: “Would this name make sense in another game?” If no → rename.
- BAD:
- Game-specific code (
assets/scripts/{Game}/): game names are fine here (ColorFitBoard,BeadsOutCanvasUI).
No Hardcoded Values (CRITICAL)
Section titled “No Hardcoded Values (CRITICAL)”Strings — NEVER inline literals for:
Section titled “Strings — NEVER inline literals for:”- Node / prefab names:
find('Canvas/HpBar')→ extract the path to aconst - Bundle / asset keys, event names, scene names → named
constor config - Pool keys → reuse the type name, not a magic string
Acceptable inline strings: console.log(...), @ccclass('ClassName') decorator arg, error messages thrown once.
Numbers — NEVER inline numeric literals:
Section titled “Numbers — NEVER inline numeric literals:”-1sentinel →const INVALID_INDEX = -1;0.1threshold →const DAMAGE_THRESHOLD = 0.1;- Tween durations, spawn counts, sizes →
GameConfigor a file-localconst
→ See references/constants-patterns.md for the GameConfig object pattern, module constants, and file-local const usage.
Code Organization
Section titled “Code Organization”File Size
Section titled “File Size”- One type per file; filename matches the type:
ColorService.ts. - Keep files under 200 lines — split into helper classes, extract utilities, compose over inherit.
Class Structure Order
Section titled “Class Structure Order”const { ccclass, property } = _decorator;(top of file)- Constants (
static readonly/ moduleconst) @propertyserialized fields, then private fields- Lifecycle methods in order (see below)
- Public → private methods
- Callbacks (
on*/handle*)
Folder Layout
Section titled “Folder Layout”assets/scripts/{Game}/├── services/ # Business-logic singletons├── UI/ # Components, views, controllers├── Data/ # Models, enums, configs├── Signal/ # Signal classes + shared signalBus export├── utils/ # Static helpers└── constant/ # Named constantsCocos-Specific Conventions
Section titled “Cocos-Specific Conventions”Decorators
Section titled “Decorators”- Destructure once at top:
const { ccclass, property } = _decorator; @ccclass('ClassName')on every component class.@property(Type)only for editor-exposed fields — not every private field.
Lifecycle Order (do not reorder)
Section titled “Lifecycle Order (do not reorder)”onLoad → onEnable → start → update → onDisable → onDestroy
- Lazy init:
this._field ??= new Foo();(inonLoad/start, notupdate). - Subscribe in
onLoad/onEnable; unsubscribe inonDestroy/onDisable— always pair.
SignalBus (CRITICAL)
Section titled “SignalBus (CRITICAL)”- Decouple systems with
signalBus.subscribe/unsubscribe()— NOT direct cross-component calls. - MUST use named methods (arrow-function class fields), NOT inline lambdas.
- Reason:
unsubscribe()usesindexOf()on the handler ref; a fresh inline lambda has a new ref every call → it can never be removed → leak.
private onColorPicked = (s: ColorPickedSignal): void => { /* ... */ };onLoad() { signalBus.subscribe(ColorPickedSignal, this.onColorPicked); }onDestroy() { signalBus.unsubscribe(ColorPickedSignal, this.onColorPicked); }Architecture Roles
Section titled “Architecture Roles”- Service — business logic, reusable, singleton (
private static _instance+static get instance(), set inonLoad). - Component — UI / scene-attached, extends
Component. - Controller — orchestration + state management.
TypeScript-Specific Conventions
Section titled “TypeScript-Specific Conventions”- Guard clauses for null safety — early return, not nested
ifs. readonlyfor config values — prevents runtime mutation.- Prefer explicit types on public surfaces; avoid
any(useunknown+ narrowing). enumvalues are lowercase strings for serialization stability.- Extract magic numbers to
GameConfigor a localconst.
→ See references/anti-patterns.md for the full anti-patterns table with correct alternatives.
Reference Files
Section titled “Reference Files”| File | Content |
|---|---|
references/constants-patterns.md | GameConfig object, module constants, file-local const, naming prefixes |
references/anti-patterns.md | Common Cocos/TS anti-patterns table with correct alternatives |
references/physics-collision-groups.md | Where a collider’s collision group actually lives (per-node shared body), and when a group bit test may stand in for getComponent |
Gotchas
Section titled “Gotchas”- Inline lambdas in
signalBus.subscribeleak —unsubscribematches by reference viaindexOf; a new lambda each call never matches. Always bind a named arrow-function class field. Most common Cocos memory-leak source. - Forgetting
onDestroycleanup — timers (this.unschedule), tweens (Tween.stopAllByTarget), and signal handlers all leak across scene reloads if not torn down. Pair every subscribe/schedule with a teardown. node.active = falsedoes NOT fireonDestroy— it firesonDisable. Put reversible teardown inonDisable, permanent teardown inonDestroy. Confusing the two double-subscribes on re-enable.@propertywithout a type on a node ref serializes asnullsilently — a ref never dragged in the editor isnullat runtime with no error. Guard-clause every@propertynode ref before use._decoratormust be imported fromcc—import { _decorator, Component, Node } from 'cc';. Copy-pasting a class without the import gives a cryptic decorator error.- Enum numeric-vs-string drift — mixing numeric and string enum members breaks serialization round-trips. Keep an enum all-string (
= 'brown') or all-numeric, never mixed. - A collider’s collision group comes from its node, not from the collider —
Colliderserialises no group of its own;getGroup()forwards to the per-node shared body, whose group is that node’sRigidBody.group. Socollider.getGroup() & SOME_BITis a valid stand-in forgetComponent(SomeClass)only when theRigidBodyand the collider sit on the same node — a collider one level down with noRigidBodyof its own reportsPhysicsGroup.DEFAULT, and every group predicate goes quietly false with no error and no failing test. Seereferences/physics-collision-groups.mdbefore swapping a component test for a group test. - This is guidance, not an enforced gate — pair with a linter (
.eslintrc) + CI to catch drift; a convention skill alone does not block violations.
Living Document
Section titled “Living Document”If unsure about a convention not covered here, ask the user for their preference and update this skill. Conventions grow from real decisions.