بنقرة واحدة
cocos-creator
写 Cocos Creator(TypeScript)时使用。组件、节点、prefab、事件、资源管理规范。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
写 Cocos Creator(TypeScript)时使用。组件、节点、prefab、事件、资源管理规范。
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
做 Cocos Creator 多机型/多分辨率适配时使用。Canvas、Widget、安全区。
Cocos Creator 用 AssetBundle 做分包/远程资源时使用。加载、释放、依赖、缓存。
优化 Cocos Creator 渲染性能时使用。合批、图集、动静分离、Label。
给 Cocos Creator 原生包做热更新时使用。version manifest、增量、校验、回滚。
写 Cocos Creator 动效/动画时使用。tween、Animation、Spine、性能与清理。
做 Cocos Creator 大量条目列表时使用。虚拟列表、节点复用。
| name | cocos-creator |
| description | 写 Cocos Creator(TypeScript)时使用。组件、节点、prefab、事件、资源管理规范。 |
| category | gamedev |
| tags | ["cocoscreator","typescript","组件"] |
规则: 逻辑封装在继承自 Component 的脚本里,Inspector 可调属性用 @property 装饰器声明;onLoad 做自身初始化,start 做跨组件依赖初始化,onEnable/onDisable 管理激活态资源,onDestroy 清理一切外部引用。
为什么: AI 生成 Creator 组件时最常见的错误是在 onLoad 里调用另一个组件的方法——但另一个组件的 onLoad 还没执行,得到 undefined 或抛出"Cannot read property of undefined"。新手则把所有逻辑都堆进 start,onEnable/onDisable 完全不用,导致节点被反复激活时调度器和事件重复注册,表现为每激活一次组件,事件就多响应一次。正确的分层:onLoad = 自身组件引用缓存;start = 订阅事件、跨节点通信;onEnable = 恢复运行状态;onDisable = 暂停运行状态;onDestroy = 注销事件、释放资源。
怎么做:
@property(cc.Node) targetNode: cc.Node = null!; — Inspector 拖拽赋值,不在代码里 find。onLoad 里只缓存 this.getComponent、this.node.getChildByName 等本节点树内的引用。start 里获取,保证对方 onLoad 已完成。onDestroy 里注销所有 EventTarget 监听器,停止所有 tween,清空定时器。规则: 节点或组件引用在 onLoad 里缓存成私有字段,绝不在 update 或高频函数里调用 find、getChildByName、getComponent;频繁实例化销毁的对象(子弹、特效、列表项)用 NodePool 管理,不用 instantiate + destroy。
为什么: node.getChildByName("xxx") 是线性遍历子节点树,在 update 里每帧调用、节点树稍大时就能感受到 CPU 消耗。AI 写原型代码时习惯"需要用谁就 find 谁",在单个脚本里看起来没问题,但一旦场景有几十个同类组件并发运行,性能立刻崩塌。对象池的价值:移动端 instantiate 一个复杂 prefab 耗时可达数十毫秒(涉及资源解析、组件初始化),批量生成敌人或特效时直接造成卡顿帧;NodePool 复用已有节点,激活耗时降一个数量级。新手不用对象池的另一个原因是不知道如何处理"归还时重置状态"——容易留下上次使用的残留数据。
怎么做:
onLoad 里:this._hpBar = this.node.getChildByName("HPBar").getComponent(cc.ProgressBar);。private _bulletPool: cc.NodePool = new cc.NodePool("BulletController");(参数为组件名,归还时调用该组件的 unuse 钩子重置状态)。const node = this._bulletPool.size() > 0 ? this._bulletPool.get() : cc.instantiate(this.bulletPrefab);。this._bulletPool.put(node);,在 BulletController 的 unuse() 里重置速度、血量、特效状态。onDestroy 里:this._bulletPool.clear(); 释放池内所有节点。规则: 组件间通信优先用 EventTarget(全局事件中心或节点事件),不硬引用对方组件直接调方法;on 注册的所有监听器在 onDestroy 里用 off 注销,或使用 targetOff(target) 批量注销。
为什么: 组件硬引用是 Creator 项目里耦合度膨胀的最大来源:this.gameManager.addScore(10) 直接调用,GameManager 重构时所有引用方都要改。AI 倾向于生成这种"直接、简单"的代码,在小 demo 里无害,但项目稍大就变成一张网,谁都不敢动。事件系统的另一个高频 bug:onLoad 里注册了监听器,onDestroy 里没有 off,节点销毁后如果事件仍然触发,回调里访问 this 的字段——this 已销毁,轻则 undefined,重则引擎报 "js exception: Cannot set property of null",且报错栈指向事件调度处而不是注册处,极难追踪。
怎么做:
GameEvent 类继承 EventTarget,模块间通过它通信。GameEvent.instance.on("score_changed", this.onScoreChanged, this);(第三个参数 this 是 target,用于批量注销)。onDestroy() { GameEvent.instance.targetOff(this); } — 一句话注销该组件注册的所有监听器。node.on(cc.Node.EventType.TOUCH_END, handler, this),同样在 onDestroy 里 node.targetOff(this)。this._handler = () => ...; event.on("x", this._handler, this);。规则: 动态资源通过 resources.load 或 assetManager.loadBundle 加载,用完后调用 asset.decRef() 或 assetManager.releaseAsset(asset) 释放;图集(SpriteAtlas)释放时要同步释放包含的 SpriteFrame,否则图集卸载但纹理内存仍驻留;场景切换时通过 autoReleaseAssets 或手动 release 确保资源回收。
为什么: Creator 的资源系统是引用计数制,每次 load 引用计数 +1,只有计数归零才真正释放 GPU 纹理内存。AI 生成的代码几乎从不调用 release——"加载了用就完了"。结果:进入战斗场景加载了角色图集,退出后图集和所有 SpriteFrame 的引用计数仍是 1(load 时的那次),内存永不回收,多次进出战斗后内存涨到被系统杀掉。新手常见的另一个错误:assetManager.releaseAsset(atlas) 只释放了图集本身,但图集包含的每一个 SpriteFrame 有自己的引用计数,需要 atlas.getSpriteFrames().forEach(sf => sf.decRef()) 才能让纹理真正卸载。
怎么做:
resources.load("ui/atlas", cc.SpriteAtlas, (err, atlas) => { atlas.addRef(); this._atlas = atlas; });(手动 addRef 管理生命周期)。onDestroy() { if (this._atlas) { this._atlas.decRef(); this._atlas = null; } }。releaseAtlas(atlas: cc.SpriteAtlas) {
atlas.getSpriteFrames().forEach(sf => sf.decRef());
atlas.decRef();
}
cc.director.loadScene 前调用当前场景加载的资源的 release,或在 Scene 属性里勾选 autoReleaseAssets(适合一次性场景)。规则: update 里只放真正需要每帧平滑插值的逻辑(移动、跟随);状态轮询改为事件驱动;字符串拼接、对象创建、数组遍历从 update 移出;UI 动画用 cc.tween,不用 update 手写插值。
为什么: Creator 运行在单线程 JS 环境,update 是每帧同步执行的主线程代码,任何耗时操作都会直接掉帧。AI 写 UI 逻辑时惯用 update 轮询检查条件:if (this.node.position.x > 100) { this.doSomething(); }——这在逻辑简单时看起来无害,但同类组件多了就是几十次 position 读取和比较;更糟糕的是 update 里 this.label.string = "HP: " + this._hp,每帧生成一个新字符串,GC 压力在移动端会造成明显的帧率抖动。新手不了解 cc.tween 时会在 update 里手写 lerp,不仅代码冗长,还难以取消/链式组合。
怎么做:
update 轮询。setter 触发单次 label.string = ...,不在 update 每帧赋值。cc.tween(node).to(0.3, { position: targetPos }).call(() => onComplete()).start(); 比 update 插值少几十行代码且可随时 cc.Tween.stopAllByTarget(node) 取消。update 不可避免,把高频计算提到 onLoad 缓存(如固定的方向向量、边界值),不每帧重算。update 调用耗时,对照帧预算(16.6ms @ 60fps)确保 update 总耗时在预算 30% 以内。// 反例 — onLoad 访问未初始化的跨组件,update 里 find + 字符串拼接
@ccclass
export class PlayerHUD extends Component {
private _player: Player = null!;
onLoad() {
// ❌ Player 的 onLoad 可能还没执行,getComponent 可能返回 null
this._player = cc.find("Player").getComponent(Player);
this._player.init(); // ❌ 可能 TypeError: Cannot read property 'init' of null
}
update(dt: number) {
// ❌ 每帧 find + getComponent,性能杀手
const label = this.node.getChildByName("HpLabel").getComponent(Label);
// ❌ 每帧字符串拼接,GC Alloc
label.string = "HP: " + cc.find("Player").getComponent(Player).hp;
}
}
// 正例 — onLoad 缓存本地引用,start 获取跨组件引用,事件驱动更新
@ccclass
export class PlayerHUD extends Component {
@property(Label) hpLabel: Label = null!; // ✅ Inspector 直接拖拽,不在代码里 find
private _player: Player = null!;
onLoad() {
// ✅ onLoad 只缓存自身树内组件,不跨组件
}
start() {
// ✅ start 里跨组件引用已安全
this._player = cc.find("Player").getComponent(Player)!;
// ✅ 事件驱动更新,不在 update 轮询
GameEvent.instance.on("hp_changed", this.onHpChanged, this);
}
private onHpChanged(newHp: number) {
this.hpLabel.string = `HP: ${newHp}`; // ✅ 只在变化时执行一次
}
onDestroy() {
GameEvent.instance.targetOff(this); // ✅ 批量注销所有监听器
}
}
// 反例 — 高频 instantiate/destroy,图集加载不释放
spawnBullet() {
const bullet = cc.instantiate(this.bulletPrefab); // ❌ 每次生成都解析 prefab,耗时
this.node.addChild(bullet);
this.scheduleOnce(() => {
bullet.destroy(); // ❌ 触发 GC,高频调用帧率抖动
}, 3);
}
loadAtlas() {
resources.load("ui/battle_atlas", cc.SpriteAtlas, (err, atlas) => {
this._atlas = atlas;
// ❌ 没有 addRef,引用计数不受控;退出时也不 release
});
}
// 正例 — NodePool 复用,资源引用计数管理
private _bulletPool: cc.NodePool = new cc.NodePool("BulletController");
private _atlas: cc.SpriteAtlas = null!;
spawnBullet() {
// ✅ 优先从池里取,取不到才 instantiate
const node = this._bulletPool.size() > 0
? this._bulletPool.get()!
: cc.instantiate(this.bulletPrefab);
this.node.addChild(node);
node.getComponent(BulletController)!.launch(this._bulletPool); // 飞行结束后自行归还
}
loadAtlas() {
resources.load("ui/battle_atlas", cc.SpriteAtlas, (err, atlas) => {
atlas.addRef(); // ✅ 手动管理引用计数
this._atlas = atlas;
});
}
onDestroy() {
this._bulletPool.clear(); // ✅ 释放池内节点
if (this._atlas) {
// ✅ 释放图集及所有 SpriteFrame
this._atlas.getSpriteFrames().forEach(sf => sf.decRef());
this._atlas.decRef();
this._atlas = null!;
}
GameEvent.instance.targetOff(this);
}
onLoad 里只缓存自身节点树内的组件引用,跨节点引用全部移到 start。update 里没有 find、getChildByName、getComponent 调用,全部已在 onLoad 缓存。NodePool,onDestroy 里 pool.clear()。onDestroy 里用 targetOff(this) 批量注销。addRef,onDestroy 里有对应的 decRef 和置 null。decRef 了包含的 SpriteFrame,不仅仅释放图集本身。cc.tween,update 里无手写插值和每帧字符串拼接。