Skip to content

t1k:cocos:base:code-conventions

FieldValue
Modulebase
Version3.3.2
Effortlow
Tools—

Keywords: ccclass, code conventions, coding standards, collider, collision group, decorator, lifecycle order, naming, typescript

/t1k:cocos:base: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.

  • 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
ElementCasingExample
Class, Component, ServicePascalCase + semantic suffixColorService, TimerComponent
InterfacePascalCase, NO I prefixHealthProvider (not IHealthProvider)
Type aliasPascalCaseSpawnOptions
EnumPascalCase nameAttackType
Enum memberPascalCase name, lowercase string valueBrown = 'brown'
Public method / propertycamelCasecalculateDamage(), maxHealth
Private methodcamelCaseapplyKnockback()
Private field_camelCase (underscore prefix)_attackTimer
Parameter / localcamelCasetargetNode, closestEnemy
Constant (module/config)UPPER_SNAKE_CASEMAX_STACK_COUNT
readonly config valueUPPER_SNAKE_CASE or camelCasereadonly SPAWN_RATE = 2
Generic type parameterTPascalCaseTComponent
Callback methodon* or handle* prefixonWeaponPlaced, 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').

ElementPatternExample
Component (UI / scene-attached)Noun + Component/View/ControllerPlayerController, HpBarView
Service (business logic singleton)Noun + ServiceColorService, AudioService
Signal classNoun + SignalWinSignal, ColorPickedSignal
Config / ScriptableObject-likeNoun + ConfigGameConfig, 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, shared utils/): 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.
  • Game-specific code (assets/scripts/{Game}/): game names are fine here (ColorFitBoard, BeadsOutCanvasUI).
  • Node / prefab names: find('Canvas/HpBar') → extract the path to a const
  • Bundle / asset keys, event names, scene names → named const or 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:”
  • -1 sentinel → const INVALID_INDEX = -1;
  • 0.1 threshold → const DAMAGE_THRESHOLD = 0.1;
  • Tween durations, spawn counts, sizes → GameConfig or a file-local const

→ See references/constants-patterns.md for the GameConfig object pattern, module constants, and file-local const usage.

  • One type per file; filename matches the type: ColorService.ts.
  • Keep files under 200 lines — split into helper classes, extract utilities, compose over inherit.
  1. const { ccclass, property } = _decorator; (top of file)
  2. Constants (static readonly / module const)
  3. @property serialized fields, then private fields
  4. Lifecycle methods in order (see below)
  5. Public → private methods
  6. Callbacks (on* / handle*)
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 constants
  • 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.

onLoad → onEnable → start → update → onDisable → onDestroy

  • Lazy init: this._field ??= new Foo(); (in onLoad/start, not update).
  • Subscribe in onLoad/onEnable; unsubscribe in onDestroy/onDisable — always pair.
  • Decouple systems with signalBus.subscribe/unsubscribe() — NOT direct cross-component calls.
  • MUST use named methods (arrow-function class fields), NOT inline lambdas.
  • Reason: unsubscribe() uses indexOf() 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); }
  • Service — business logic, reusable, singleton (private static _instance + static get instance(), set in onLoad).
  • Component — UI / scene-attached, extends Component.
  • Controller — orchestration + state management.
  • Guard clauses for null safety — early return, not nested ifs.
  • readonly for config values — prevents runtime mutation.
  • Prefer explicit types on public surfaces; avoid any (use unknown + narrowing).
  • enum values are lowercase strings for serialization stability.
  • Extract magic numbers to GameConfig or a local const.

→ See references/anti-patterns.md for the full anti-patterns table with correct alternatives.

FileContent
references/constants-patterns.mdGameConfig object, module constants, file-local const, naming prefixes
references/anti-patterns.mdCommon Cocos/TS anti-patterns table with correct alternatives
references/physics-collision-groups.mdWhere a collider’s collision group actually lives (per-node shared body), and when a group bit test may stand in for getComponent
  • Inline lambdas in signalBus.subscribe leak — unsubscribe matches by reference via indexOf; a new lambda each call never matches. Always bind a named arrow-function class field. Most common Cocos memory-leak source.
  • Forgetting onDestroy cleanup — 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 = false does NOT fire onDestroy — it fires onDisable. Put reversible teardown in onDisable, permanent teardown in onDestroy. Confusing the two double-subscribes on re-enable.
  • @property without a type on a node ref serializes as null silently — a ref never dragged in the editor is null at runtime with no error. Guard-clause every @property node ref before use.
  • _decorator must be imported from cc — 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 — Collider serialises no group of its own; getGroup() forwards to the per-node shared body, whose group is that node’s RigidBody.group. So collider.getGroup() & SOME_BIT is a valid stand-in for getComponent(SomeClass) only when the RigidBody and the collider sit on the same node — a collider one level down with no RigidBody of its own reports PhysicsGroup.DEFAULT, and every group predicate goes quietly false with no error and no failing test. See references/physics-collision-groups.md before 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.

If unsure about a convention not covered here, ask the user for their preference and update this skill. Conventions grow from real decisions.