Cocos Creator 3.x 装饰器实战:让你的代码优雅 10 倍

一、为什么必须懂装饰器

Cocos Creator 3.x 全面拥抱 TypeScript,最直接的红利就是装饰器(Decorator) 。从 2.x 的 cc.Class({ ... }) 到 3.x 的 @ccclass,看似只是语法糖,实际上是组件开发模式的彻底重构。

先看一组对比,你立刻就能感受到差距。

Cocos Creator 2.x 写法:

javascript 复制代码
cc.Class({
    extends: cc.Component,
    properties: {
        speed: {
            default: 100,
            type: cc.Float,
            tooltip: '移动速度',
            range: [0, 1000]
        },
        target: {
            default: null,
            type: cc.Node
        }
    },
    onLoad() { /* ... */ },
    start() { /* ... */ }
});

Cocos Creator 3.x 写法:

typescript 复制代码
@ccclass('PlayerController')
export class PlayerController extends Component {
    @property({ tooltip: '移动速度', range: [0, 1000] })
    speed: number = 100;

    @property(Node)
    target: Node = null!;

    onLoad() { /* ... */ }
    start() { /* ... */ }
}

差异点很明显:

  • 类型推断:3.x 直接用 TypeScript 类型,IDE 智能提示拉满
  • 代码结构:属性声明和方法声明位置一致,可读性远超 2.x
  • 关注点分离:装饰器是元数据,逻辑是逻辑,互不干扰
  • 可组合:多个装饰器可以叠加,灵活程度跃升一个量级

二、装饰器基础速通

2.1 什么是装饰器

装饰器(Decorator)是 TypeScript 提供的元编程能力,本质是一个函数,它能在运行时附加在类、属性、方法、参数上,修改其行为或添加元数据。

typescript 复制代码
// 装饰器就是一个函数
function MyDecorator(target: any) {
    console.log('我被附加到了', target.name);
}

@MyDecorator
class MyClass { }
// 输出:我被附加到了 MyClass

2.2 装饰器的四种形态

类型 附加位置 Cocos 典型代表
类装饰器 @ccclass
属性装饰器 属性 @property
方法装饰器 方法 自定义节流、日志等
参数装饰器 参数 依赖注入框架常用

2.3 在 Cocos Creator 3.x 中导入装饰器

所有内置装饰器都从 cc 模块导入:

typescript 复制代码
import { _decorator, Component, Node } from 'cc';
const { ccclass, property, executeInEditMode, menu, requireComponent } = _decorator;

这是 3.x 项目的标配头部。_decorator 是命名空间,里面装了所有官方装饰器。

小贴士 :用 Cocos Creator 编辑器创建脚本时,会自动生成 _decorator 导入语句,无需手动写。


三、@ccclass 类装饰器深度解析

@ccclass 是 Cocos Creator 3.x 最基础也最重要的装饰器,任何继承 Component 的脚本必须使用它,否则引擎无法识别该类。

3.1 基础用法

typescript 复制代码
@ccclass('PlayerController')
export class PlayerController extends Component {
    // ...
}

参数是一个字符串,作为类在引擎中的注册名称。这个名称用于序列化,一旦使用就不要随意修改,否则旧的预制体或场景会丢失引用。

3.2 命名空间策略

对于大型项目,建议加上命名空间避免冲突:

typescript 复制代码
@ccclass('Game.UI.MainMenu')
export class MainMenu extends Component { }

@ccclass('Game.Battle.PlayerController')
export class PlayerController extends Component { }

这样在编辑器组件菜单和资源面板中,会有清晰的层级显示。

3.3 不传参数的简写

typescript 复制代码
@ccclass
export class MyComponent extends Component { }

不传参数时,类名会自动作为注册名。生产环境不推荐这种写法,因为 minify 之后类名会变,导致序列化失效。


四、@property 属性装饰器全攻略

@property 是 Cocos Creator 3.x 中使用频率最高的装饰器,决定属性是否序列化、是否显示在编辑器、显示样式如何。

4.1 七种核心用法

用法 1:纯类型推断

typescript 复制代码
@property
hp: number = 100;

TypeScript 自动推断为 number,序列化也按 number 处理。简单字段推荐这种写法

用法 2:显式类型声明

typescript 复制代码
@property(Node)
target: Node = null!;

@property(Prefab)
bulletPrefab: Prefab = null!;

当属性是引用类型(Node、Prefab、SpriteFrame 等),必须显式声明类型,编辑器才能在面板显示正确的拖拽控件。

用法 3:完整 options 配置

typescript 复制代码
@property({
    type: Node,
    tooltip: '主摄像机节点',
    displayName: '相机',
    visible: true,
    serializable: true,
})
mainCamera: Node = null!;

options 对象支持非常多配置项,下面逐一展开。

用法 4:数组类型

typescript 复制代码
@property({ type: [Node] })
enemies: Node[] = [];

@property({ type: [Prefab] })
weaponPrefabs: Prefab[] = [];

数组类型必须用方括号包裹元素类型,否则编辑器无法识别。

用法 5:枚举类型

typescript 复制代码
enum WeaponType {
    Sword = 0,
    Bow = 1,
    Staff = 2,
}

@ccclass('Weapon')
export class Weapon extends Component {
    @property({ type: Enum(WeaponType) })
    weaponType: WeaponType = WeaponType.Sword;
}

枚举必须用 Enum() 包装,编辑器会展示为下拉框。

用法 6:自定义类(非 Component)

typescript 复制代码
@ccclass('SkillData')
export class SkillData {
    @property
    name: string = '';

    @property
    damage: number = 0;
}

@ccclass('Player')
export class Player extends Component {
    @property({ type: SkillData })
    skill: SkillData = new SkillData();

    @property({ type: [SkillData] })
    skills: SkillData[] = [];
}

数据类不继承 Component,但只要加上 @ccclass + @property,就能在编辑器面板里编辑。

用法 7:getter/setter

typescript 复制代码
@ccclass('HealthBar')
export class HealthBar extends Component {
    private _hp: number = 100;

    @property
    get hp(): number {
        return this._hp;
    }

    set hp(value: number) {
        this._hp = clamp(value, 0, 100);
        this.updateUI();
    }

    private updateUI() { /* ... */ }
}

通过 getter/setter 可以在属性变更时自动触发逻辑 ,比如更新 UI、播放动画等,远比手动调用 setHP() 优雅。

4.2 options 完整字段速查

字段 类型 作用
type Constructor 显式声明类型
default any 默认值(推荐直接初始化字段,不用此项)
serializable boolean 是否序列化保存,默认 true
visible boolean / Function 编辑器是否可见,可以传函数动态决定
displayName string 编辑器显示的名字(覆盖字段名)
displayOrder number 编辑器面板中的显示顺序
tooltip string 鼠标悬停时的提示文字
readonly boolean 是否只读
range [min, max, step] 数值范围(带步长)
slide boolean 是否显示为滑动条
group string / object 属性分组
min / max number 数值上下限(不带步长)
step number 数值步长
unit string 单位(显示在值后面)
multiline boolean 字符串多行编辑

4.3 高级技巧:visible 函数

typescript 复制代码
@ccclass('Weapon')
export class Weapon extends Component {
    @property({ type: Enum(WeaponType) })
    weaponType: WeaponType = WeaponType.Sword;

    // 仅当武器类型是 Bow 时显示这个字段
    @property({
        visible: function (this: Weapon) {
            return this.weaponType === WeaponType.Bow;
        }
    })
    arrowSpeed: number = 500;
}

这种条件显示特性可以让编辑器面板更整洁,根据当前配置动态展示相关字段。

4.4 高级技巧:分组管理

typescript 复制代码
@ccclass('Player')
export class Player extends Component {
    @property({ group: { name: '基础属性', id: '1' } })
    hp: number = 100;

    @property({ group: { name: '基础属性', id: '1' } })
    mp: number = 50;

    @property({ group: { name: '战斗属性', id: '2' } })
    attack: number = 10;

    @property({ group: { name: '战斗属性', id: '2' } })
    defense: number = 5;
}

通过 group 字段,可以把相关属性聚合在一起,编辑器面板呈现折叠分组,对于大型组件至关重要


五、编辑器行为装饰器

这一类装饰器决定组件在编辑器中的行为,是提升团队协作效率的利器。

5.1 @executeInEditMode:编辑器实时预览

typescript 复制代码
@ccclass('PreviewBox')
@executeInEditMode
export class PreviewBox extends Component {
    @property
    size: number = 100;

    update() {
        // 编辑器中也会运行
        this.node.setScale(this.size, this.size, 1);
    }
}

加了这个装饰器,updateonLoad 等生命周期在编辑器环境也会触发。适合做实时预览效果,比如调整参数立即看到结果。

注意 :不要在 executeInEditMode 的组件里访问运行时才存在的资源(比如远端加载的图片)。

5.2 @menu:自定义菜单分类

typescript 复制代码
@ccclass('SmokeEffect')
@menu('特效/烟雾效果')
export class SmokeEffect extends Component { }

@ccclass('ExplosionEffect')
@menu('特效/爆炸效果')
export class ExplosionEffect extends Component { }

在编辑器 添加组件 菜单里,组件会按 菜单路径 分类显示。大项目必备,避免菜单一堆杂乱的组件。

5.3 @requireComponent:依赖声明

typescript 复制代码
@ccclass('Mover')
@requireComponent(RigidBody2D)
export class Mover extends Component {
    private rb: RigidBody2D = null!;

    onLoad() {
        // 一定能拿到,不需要判空
        this.rb = this.getComponent(RigidBody2D)!;
    }
}

Mover 被添加到节点时,如果节点上没有 RigidBody2D,引擎会自动添加。同时移除时如果有其他组件依赖它,会阻止移除。

5.4 @disallowMultiple:禁止重复添加

typescript 复制代码
@ccclass('UniqueController')
@disallowMultiple
export class UniqueController extends Component { }

防止一个节点上添加多个相同组件。所有单例职责的组件都应该加这个,防止策划误操作。

5.5 @executionOrder:执行顺序

typescript 复制代码
@ccclass('GameManager')
@executionOrder(-1000)
export class GameManager extends Component {
    onLoad() {
        // 在所有默认组件之前执行
    }
}

@ccclass('UIManager')
@executionOrder(1000)
export class UIManager extends Component {
    onLoad() {
        // 在所有默认组件之后执行
    }
}

数字越小,执行越靠前。默认是 0,负数提前,正数延后。常用于全局管理类组件,确保它们先于业务组件初始化。

5.6 @help:帮助文档链接

typescript 复制代码
@ccclass('ComplexComponent')
@help('https://docs.your-game.com/components/complex')
export class ComplexComponent extends Component { }

编辑器面板右上角会出现帮助按钮,点击跳转到指定 URL。


六、自定义装饰器:通用能力封装

6.1 单例模式装饰器

typescript 复制代码
function singleton<T extends new (...args: any[]) => any>(constructor: T) {
    let instance: InstanceType<T> | null = null;

    return class extends constructor {
        constructor(...args: any[]) {
            if (instance) {
                return instance;
            }
            super(...args);
            instance = this as InstanceType<T>;
        }
    };
}

@singleton
class GameManager {
    private _data: any = {};

    setData(key: string, value: any) {
        this._data[key] = value;
    }

    getData(key: string) {
        return this._data[key];
    }
}

const a = new GameManager();
const b = new GameManager();
console.log(a === b); // true

注意 :单例装饰器不适合 Component 类,Component 必须挂在节点上。这种模式更适合纯数据管理类。

6.2 节流装饰器

防止某些方法被频繁调用(比如按钮点击、网络请求):

typescript 复制代码
function throttle(delay: number = 1000) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        const original = descriptor.value;
        const lastCallMap = new WeakMap<object, number>();

        descriptor.value = function (...args: any[]) {
            const now = Date.now();
            const last = lastCallMap.get(this) || 0;

            if (now - last >= delay) {
                lastCallMap.set(this, now);
                return original.apply(this, args);
            } else {
                console.log(`[throttle] ${propertyKey} 节流中`);
            }
        };

        return descriptor;
    };
}

@ccclass('SkillController')
export class SkillController extends Component {
    @throttle(500)
    castFireball() {
        console.log('火球术发射!');
        // 真实业务逻辑
    }

    @throttle(2000)
    castUltimate() {
        console.log('大招发动!');
    }
}

亮点@throttle(500) 一行注解,整个方法自动具备节流能力,业务代码零侵入。

6.3 性能监控装饰器

typescript 复制代码
function measureTime(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const original = descriptor.value;

    descriptor.value = function (...args: any[]) {
        const start = performance.now();
        const result = original.apply(this, args);
        const cost = performance.now() - start;

        if (cost > 16) {
            console.warn(`[Performance] ${propertyKey} 耗时 ${cost.toFixed(2)}ms(超过一帧)`);
        }

        return result;
    };

    return descriptor;
}

@ccclass('PathFinder')
export class PathFinder extends Component {
    @measureTime
    findPath(start: Vec3, end: Vec3) {
        // 复杂的 A* 寻路算法
    }

    @measureTime
    updateMap() {
        // 更新地图数据
    }
}

亮点:开发期发现性能瓶颈,无需手动加计时代码。


七、多装饰器叠加顺序

7.1 多装饰器叠加顺序

typescript 复制代码
@ccclass('GameCore')
@executeInEditMode
@disallowMultiple
@menu('系统/游戏核心')
@executionOrder(-9999)
export class GameCore extends Component { }

多个装饰器从下往上执行(最靠近类的先生效)。建议顺序:

  1. @ccclass(必须最上面)
  2. 行为类装饰器(@executeInEditMode@disallowMultiple
  3. 菜单和元数据(@menu@help
  4. 执行控制(@executionOrder
相关推荐
吃西瓜的年年8 小时前
TypeScript
javascript·ubuntu·typescript
Restart-AHTCM8 小时前
AI时代大前端Agent开发LangChain.js
typescript·langchain·memory·rag·tools
Edwardwu10 小时前
写了个y-mxgraph:给 draw.io 接上了 Yjs,顺便解决了部署在 iframe 里的一堆问题
前端·typescript
阿正的梦工坊12 小时前
【Typescript】14-高级实战-设计类型安全的-api
typescript
川冰ICE13 小时前
TypeScript装饰器与元编程实战
前端·javascript·typescript
Rain50916 小时前
mini-cc:一个轻量级 AI 编程助手的诞生
人工智能·typescript·ai编程
前端若水17 小时前
自定义消息组件:图片、文件附件与图表
前端·人工智能·react.js·typescript
晓杰'17 小时前
Balatro后端进阶(2):基于GitHub Actions的CI自动化验证实现
websocket·ci/cd·typescript·node.js·自动化·github·nestjs
阿正的梦工坊17 小时前
【Typescript】10-条件类型与-infer
前端·javascript·typescript