Cocos Creator开发踩坑笔记:动画、事件与性能优化

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);
// 执行后续逻辑
}
}

两种注册方式的区别:

  1. AnimationState注册:仅针对特定动画剪辑生效,适合对不同动画做差异化处理
  2. 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);
}

// 清理Sprite引用
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() {
// 注册触摸事件,useCapture设为true
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);
// 添加点击效果动画等
}

Tween动画与Button冲突处理

当节点包含cc.Button组件时,tween操作opacity会失效,需要特殊处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 解决方案:在动画期间禁用Button
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(() => {
// 动画结束后恢复Button
item.getComponent(cc.Button).enabled = true;
})
.start();

UI适配与SafeArea问题

SafeArea频繁切换适配问题

当应用频繁在后台和前台切换时,SafeArea适配可能出现问题。

解决方案:
将所有Widget组件的Align Mode设置为 Once

1
2
3
4
5
6
7
// 遍历所有Widget组件并修改
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, // 测试点 Vec2
start, // 线段起点 Vec2
end, // 线段终点 Vec2
isSegment // 是否为线段 boolean
);

参数说明:

  • 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); // 输出: 1, 2
}
}

// 使用自定义参数创建
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开发中遇到的一些问题和解决方案。开发过程中遇到问题多查文档、多调试,积累经验最重要。


参考资源: