Cocos Creator开发踩坑笔记
我在使用Cocos Creator开发项目时积累了一些经验,记录在这里供参考。
动画系统踩坑记录
动态创建AnimationClip
在实际开发中,我们经常需要动态创建动画而不是预先在编辑器中配置。Cocos Creator提供了灵活的API来实现这一需求。
从SpriteFrame序列创建动画:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| cc.resources.loadDir("clips/hit", cc.SpriteFrame, (err, sprites: cc.SpriteFrame[]) => { if (err || !sprites) { cc.error("加载动画资源失败:", err); return; }
let anim = this.node.getComponent(cc.Animation); anim.clear();
let clip = cc.AnimationClip.createWithSpriteFrames(sprites, sprites.length); clip.wrapMode = cc.WrapMode.Normal; clip.name = "hit";
anim.addClip(clip, "hit"); anim.play("hit"); });
|
从预制资源加载动画:
1 2 3 4 5 6 7 8 9 10
| cc.resources.load(path, (err, clip: cc.AnimationClip) => { if (err) { cc.error("动画加载失败:", err); return; }
let anim = node.getComponent(cc.Animation); anim.addClip(clip); anim.play('animation_name'); });
|
动画事件系统
Cocos Creator支持丰富的动画回调事件,让开发者能够精确控制动画播放流程。
支持的回调事件类型:
| 事件名称 |
触发时机 |
使用场景 |
| play |
开始播放时 |
动画开始前的初始化操作 |
| stop |
停止播放时 |
清理动画相关资源 |
| pause |
暂停播放时 |
暂停游戏逻辑 |
| resume |
恢复播放时 |
恢复游戏逻辑 |
| lastframe |
播放到最后一帧(循环时每次触发) |
循环动画的每轮处理 |
| finished |
动画播放完成时 |
动画结束后的回调处理 |
单个动画状态注册回调:
1 2 3 4 5 6 7 8 9 10 11 12 13
| this.animCtrl = this.node.getComponent(cc.Animation); this.animCtrl.play('run');
var animState = this.animCtrl.getAnimationState('run'); if (animState) { animState.on('stop', (event) => { let clip = event.detail; cc.log("动画停止:", clip.name); }, this); }
|
全局动画组件注册回调:
1 2 3 4 5 6 7 8 9 10
| this.animCtrl.on('stop', this.onAnimStop, this);
onAnimStop: function(event) { let animState = event.detail; if (animState.name === 'run' && event.type === 'stop') { this.animCtrl.off('stop', this.onAnimStop, this); } }
|
两种注册方式的区别:
- AnimationState注册:仅针对特定动画剪辑生效,适合对不同动画做差异化处理
- Animation组件注册:对所有动画剪辑生效,适合做全局统一处理
动画资源释放
动态创建的动画需要手动释放,避免内存泄漏:
1 2 3 4 5 6 7 8
| for (let i = 0; i < this.anim.getClips().length; i++) { const element = this.anim.getClips()[i]; this.anim.removeClip(element, true); }
this.node.getComponent(cc.Sprite).spriteFrame = null;
|
注意: 清理 spriteFrame 是必不可少的步骤,否则会造成纹理引用计数无法归零。
粒子系统与拖尾效果
动态加载粒子系统
1 2 3 4 5 6 7 8 9 10 11 12
| cc.resources.load("effects/explosion", cc.ParticleAsset, (err, asset) => { if (err || !asset) { cc.log(err); return; }
let ps: cc.ParticleSystem = this.particle.addComponent(cc.ParticleSystem); ps.playOnLoad = true; ps.file = asset; ps.resetSystem(); });
|
动态加载拖尾效果
1 2 3 4 5 6 7 8 9 10 11
| cc.resources.load("textures/trail", cc.SpriteFrame, (err, sprite: cc.SpriteFrame) => { if (err) return;
let motionStreak = this.motionStrake.getComponent(cc.MotionStreak); motionStreak.fadeTime = 0.3; motionStreak.minSeg = 0; motionStreak.stroke = 20; motionStreak.texture = sprite.getTexture(); motionStreak.color = new cc.Color().fromHEX("#FFFFFF"); motionStreak.dstBlendFactor = cc.macro.BlendFactor.ONE; });
|
关键参数说明:
fadeTime:拖尾渐隐的时间,值越小拖尾越短
minSeg:最小分段长度,0表示无限制
stroke:拖尾线条宽度
dstBlendFactor:目标混合因子,设置为ONE可实现加亮效果
事件处理与交互踩坑
全屏覆盖点击穿透层
在游戏中经常需要实现点击特效层,同时让事件穿透到底层:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| onLoad() { this.coverNode.on(cc.Node.EventType.TOUCH_END, this.onClickTouchEnd, this, true); this.coverNode._touchListener.setSwallowTouches(false); }
onDestroy() { this.coverNode.off(cc.Node.EventType.TOUCH_END, this.onClickTouchEnd, this, true); }
onClickEffectTouchEnd(event) { let touchLoc = event.getTouches()[0].getLocation(); touchLoc = this.node.parent.convertToNodeSpaceAR(touchLoc); }
|
当节点包含cc.Button组件时,tween操作opacity会失效,需要特殊处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| cc.Tween.stopAllByTarget(item); if (item.getComponent(cc.Button)) { item.getComponent(cc.Button).enabled = false; }
item.opacity = 0; cc.tween(item) .to(0.02, { opacity: 255 }) .call(() => { item.getComponent(cc.Button).enabled = true; }) .start();
|
UI适配与SafeArea问题
SafeArea频繁切换适配问题
当应用频繁在后台和前台切换时,SafeArea适配可能出现问题。
解决方案:
将所有Widget组件的Align Mode设置为 Once:
1 2 3 4 5 6 7
| this.node.walk((child) => { let widget = child.getComponent(cc.Widget); if (widget) { widget.alignMode = cc.Widget.AlignMode.ONCE; } }, this);
|
注意: 必须是所有Widget组件都修改,否则适配效果不一致。
计算点到直线的距离
Cocos Creator提供了内置的几何计算函数:
1 2 3 4 5 6
| let distance = cc.Intersection.pointLineDistance( point, start, end, isSegment );
|
参数说明:
point:需要计算距离的点
start:直线的起始点或线段的起点
end:直线的结束点或线段的终点
isSegment:true表示计算到线段的距离,false表示计算到无限长直线的距离
TypeScript开发技巧
构造函数传参
在Cocos Creator中使用TypeScript时,可以通过扩展构造函数传递参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| const {ccclass, property} = cc._decorator;
@ccclass export default class Test extends cc.Component { private a: number; private b: number;
constructor(...params: any) { super(); this.a = params[0]; this.b = params[1]; console.log(this.a, this.b); } }
let test = new Test(1, 2);
|
可选参数与默认值
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function f(g: () => void = null) { if (g) { g(); } }
function f(g?: () => void) { g = g || null; if (g) { g(); } }
|
Promise与异步处理
在Cocos Creator项目中经常需要处理异步操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| async login(): Promise<Account> { return new Promise((resolve, reject) => { HttpManager.instance.get(`https://xxx.com/config.json`) .then(value => { try { let account = new Account(); account._config = JSON.parse(value); resolve(account); } catch (e) { reject(e); } }) .catch(reason => { reject(reason); }); }); }
async init() { try { let account = await this.login(); cc.log("登录成功:", account); } catch (e) { cc.error("登录失败:", e); } }
|
性能优化经验
资源加载优化
1 2 3 4 5 6 7
| cc.resources.preload("path/to/res", cc.SpriteFrame);
cc.resources.loadDir("textures/role", cc.SpriteFrame, (err, assets) => { });
|
节点池优化
对于频繁创建销毁的节点(如子弹、特效),使用对象池可以显著提升性能:
1 2 3 4 5 6 7 8
| this.bulletPool = new cc.NodePool("Bullet");
let bullet = this.bulletPool.get() || cc.instantiate(this.bulletPrefab);
this.bulletPool.put(bullet);
|
总结
以上是我在Cocos Creator开发中遇到的一些问题和解决方案。开发过程中遇到问题多查文档、多调试,积累经验最重要。
参考资源: