一、为什么必须懂装饰器
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);
}
}
加了这个装饰器,update、onLoad 等生命周期在编辑器环境也会触发。适合做实时预览效果,比如调整参数立即看到结果。
注意 :不要在 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 { }
多个装饰器从下往上执行(最靠近类的先生效)。建议顺序:
@ccclass(必须最上面)- 行为类装饰器(
@executeInEditMode、@disallowMultiple) - 菜单和元数据(
@menu、@help) - 执行控制(
@executionOrder)