生命周期回调
Cocos Creator 为组件脚本提供了生命周期的回调函数。用户只要定义特定的回调函数,Creator 就会在特定的时期自动执行相关脚本,用户不需要手工调用它们。
目前提供给用户的生命周期回调函数主要有:
- onLoad
- start
- update
- lateUpdate
- onDestroy
- onEnable
- onDisable
onLoad
组件脚本的初始化阶段,我们提供了 onLoad
回调函数。onLoad
回调会在节点首次激活时触发,比如所在的场景被载入,或者所在节点被激活的情况下。在 onLoad
阶段,保证了你可以获取到场景中的其他节点,以及节点关联的资源数据。onLoad
总是会在任何 start
方法调用前执行,这能用于安排脚本的初始化顺序。通常我们会在 onLoad
阶段去做一些初始化相关的操作。例如:
cc.Class({
extends: cc.Component,
properties: {
bulletSprite: cc.SpriteFrame,
gun: cc.Node,
},
onLoad: function () {
this._bulletRect = this.bulletSprite.getRect();
this.gun = cc.find('hand/weapon', this.node);
},
});
start
start
回调函数会在组件第一次激活前,也就是第一次执行 update
之前触发。start
通常用于初始化一些需要经常修改的数据,这些数据可能在 update 时会发生改变。
cc.Class({
extends: cc.Component,
start: function () {
this._timer = 0.0;
},
update: function (dt) {
this._timer += dt;
if ( this._timer >= 10.0 ) {
console.log('I am done!');
this.enabled = false;
}
},
});
update
游戏开发的一个关键点是在每一帧渲染前更新物体的行为,状态和方位。这些更新操作通常都放在 update
回调中。
cc.Class({
extends: cc.Component,
update: function (dt) {
this.node.setPosition( 0.0, 40.0 * dt );
}
});
lateUpdate
update
会在所有动画更新前执行,但如果我们要在动效(如动画、粒子、物理等)更新之后才进行一些额外操作,或者希望在所有组件的 update
都执行完之后才进行其它操作,那就需要用到 lateUpdate
回调。
cc.Class({
extends: cc.Component,
lateUpdate: function (dt) {
this.node.rotation = 20;
}
});
onEnable
当组件的 enabled
属性从 false
变为 true
时,或者所在节点的 active
属性从 false
变为 true
时,会激活 onEnable
回调。倘若节点第一次被创建且 enabled
为 true
,则会在 onLoad
之后,start
之前被调用。
onDisable
当组件的 enabled
属性从 true
变为 false
时,或者所在节点的 active
属性从 true
变为 false
时,会激活 onDisable
回调。
onDestroy
当组件或者所在节点调用了 destroy()
,则会调用 onDestroy
回调,并在当帧结束时统一回收组件。当同时声明了 onLoad
和 onDestroy
时,它们将总是被成对调用。也就是说从组件初始化到销毁的过程中,它们要么就都会被调用,要么就都不会被调用。
Tips
一个组件从初始化到激活,再到最终销毁的完整生命周期函数调用顺序为:onLoad
-> onEnable
-> start
-> update
-> lateUpdate
-> onDisable
-> onDestroy
。
其中,onLoad
和 start
常常用于组件的初始化,只有在节点 activeInHierarchy
的情况下才能调用,并且最多只会被调用一次。除了上文提到的内容以及调用顺序的不同,它们还有以下区别:
节点激活时 | 组件 enabled 时才会调用? | |
---|---|---|
onLoad | 立即调用 | 否 |
start | 延迟调用 | 是 |
创建和销毁节点
创建新节点
除了通过场景编辑器创建节点外,我们也可以在脚本中动态创建节点。通过 new cc.Node()
并将它加入到场景中,可以实现整个创建过程。
以下是一个简单的例子:
cc.Class({
extends: cc.Component,
properties: {
sprite: {
default: null,
type: cc.SpriteFrame,
},
},
start: function () {
var node = new cc.Node('Sprite');
var sp = node.addComponent(cc.Sprite);
sp.spriteFrame = this.sprite;
node.parent = this.node;
},
});
克隆已有节点
有时我们希望动态的克隆场景中的已有节点,我们可以通过 cc.instantiate
方法完成。使用方法如下:
cc.Class({
extends: cc.Component,
properties: {
target: {
default: null,
type: cc.Node,
},
},
start: function () {
var scene = cc.director.getScene();
var node = cc.instantiate(this.target);
node.parent = scene;
node.setPosition(0, 0);
},
});
创建预制节点
和克隆已有节点相似,你可以设置一个预制(Prefab)并通过 cc.instantiate
生成节点。使用方法如下:
cc.Class({
extends: cc.Component,
properties: {
target: {
default: null,
type: cc.Prefab,
},
},
start: function () {
var scene = cc.director.getScene();
var node = cc.instantiate(this.target);
node.parent = scene;
node.setPosition(0, 0);
},
});
销毁节点
通过 node.destroy()
函数,可以销毁节点。值得一提的是,销毁节点并不会立刻被移除,而是在当前帧逻辑更新结束后,统一执行。当一个节点销毁后,该节点就处于无效状态,可以通过 cc.isValid
判断当前节点是否已经被销毁。
使用方法如下:
cc.Class({
extends: cc.Component,
properties: {
target: cc.Node,
},
start: function () {
// 5 秒后销毁目标节点
setTimeout(function () {
this.target.destroy();
}.bind(this), 5000);
},
update: function (dt) {
if (cc.isValid(this.target)) {
this.target.rotation += dt * 10.0;
}
},
});
destroy 和 removeFromParent 的区别
调用一个节点的 removeFromParent
后,它不一定就能完全从内存中释放,因为有可能由于一些逻辑上的问题,导致程序中仍然引用到了这个对象。因此如果一个节点不再使用了,请直接调用它的 destroy
而不是 removeFromParent
。destroy
不但会激活组件上的 onDestroy
,还会降低内存泄露的几率,同时减轻内存泄露时的后果。
总之,如果一个节点不再使用,destroy
就对了,不需要 removeFromParent
也不需要设置 parent
为 null
。
加载和切换场景
在 Cocos Creator 中,我们使用场景文件名(不包含扩展名)来索引指代场景。并通过以下接口进行加载和切换操作:
cc.director.loadScene("MyScene");
除此之外,从 v2.4 开始 Asset Bundle 还增加了一种新的加载方式:
bundle.loadScene('MyScene', function (err, scene) {
cc.director.runScene(scene);
});
Asset Bundle 提供的 loadScene
只会加载指定 bundle 中的场景,并不会自动运行场景,还需要使用 cc.director.runScene
来运行场景。
loadScene
还提供了更多参数来控制加载流程,开发者可以自行控制加载参数或者在加载完场景后做一些处理。
更多关于加载 Asset Bundle 中的场景,可参考文档 Asset Bundle。
通过常驻节点进行场景资源管理和参数传递
引擎同时只会运行一个场景,当切换场景时,默认会将场景内所有节点和其他实例销毁。如果我们需要用一个组件控制所有场景的加载,或在场景之间传递参数数据,就需要将该组件所在节点标记为「常驻节点」,使它在场景切换时不被自动销毁,常驻内存。我们使用以下接口:
cc.game.addPersistRootNode(myNode);
上面的接口会将 myNode
变为常驻节点,这样挂在上面的组件都可以在场景之间持续作用,我们可以用这样的方法来储存玩家信息,或下一个场景初始化时需要的各种数据。
如果要取消一个节点的常驻属性:
cc.game.removePersistRootNode(myNode);
需要注意的是上面的 API 并不会立即销毁指定节点,只是将节点还原为可在场景切换时销毁的节点。
监听和发射事件
监听事件
事件处理是在节点(cc.Node
)中完成的。对于组件,可以通过访问节点 this.node
来注册和监听事件。监听事件可以通过 this.node.on()
函数来注册,方法如下:
cc.Class({
extends: cc.Component,
properties: {
},
onLoad: function () {
this.node.on('mousedown', function ( event ) {
console.log('Hello!');
});
},
});
值得一提的是,事件监听函数 on
可以传第三个参数 target,用于绑定响应函数的调用者。以下两种调用方式,效果上是相同的:
// 使用函数绑定
this.node.on('mousedown', function ( event ) {
this.enabled = false;
}.bind(this));
// 使用第三个参数
this.node.on('mousedown', function (event) {
this.enabled = false;
}, this);
除了使用 on
监听,我们还可以使用 once
方法。once
监听在监听函数响应后就会关闭监听事件。
关闭监听
当我们不再关心某个事件时,我们可以使用 off
方法关闭对应的监听事件。需要注意的是,off
方法的参数必须和 on
方法的参数一一对应,才能完成关闭。
我们推荐的书写方法如下:
cc.Class({
extends: cc.Component,
_sayHello: function () {
console.log('Hello World');
},
onEnable: function () {
this.node.on('foobar', this._sayHello, this);
},
onDisable: function () {
this.node.off('foobar', this._sayHello, this);
},
});
发射事件
发射事件有两种方式:emit
和 dispatchEvent
。两者的区别在于,后者可以做事件传递。我们先通过一个简单的例子来了解 emit
事件:
cc.Class({
extends: cc.Component,
onLoad () {
// args are optional param.
this.node.on('say-hello', function (msg) {
console.log(msg);
});
},
start () {
// At most 5 args could be emit.
this.node.emit('say-hello', 'Hello, this is Cocos Creator');
},
});
事件参数说明
在 2.0 之后,我们优化了事件的参数传递机制。 在发射事件时,我们可以在 emit
函数的第二个参数开始传递我们的事件参数。同时,在 on
注册的回调里,可以获取到对应的事件参数。
cc.Class({
extends: cc.Component,
onLoad () {
this.node.on('foo', function (arg1, arg2, arg3) {
console.log(arg1, arg2, arg3); // print 1, 2, 3
});
},
start () {
let arg1 = 1, arg2 = 2, arg3 = 3;
// At most 5 args could be emit.
this.node.emit('foo', arg1, arg2, arg3);
},
});
需要说明的是,出于底层事件派发的性能考虑,这里最多只支持传递 5 个事件参数。所以在传参时需要注意控制参数的传递个数。
派送事件
上文提到了 dispatchEvent
方法,通过该方法发射的事件,会进入事件派送阶段。在 Cocos Creator 的事件派送系统中,我们采用冒泡派送的方式。冒泡派送会将事件从事件发起节点,不断地向上传递给他的父级节点,直到到达根节点或者在某个节点的响应函数中做了中断处理 event.stopPropagation()
。
如上图所示,当我们从节点 c 发送事件 “foobar”,倘若节点 a,b 均做了 “foobar” 事件的监听,则 事件会经由 c 依次传递给 b,a 节点。如:
// 节点 c 的组件脚本中
this.node.dispatchEvent( new cc.Event.EventCustom('foobar', true) );
如果我们希望在 b 节点截获事件后就不再将事件传递,我们可以通过调用 event.stopPropagation()
函数来完成。具体方法如下:
// 节点 b 的组件脚本中
this.node.on('foobar', function (event) {
event.stopPropagation();
});
请注意,在发送用户自定义事件的时候,请不要直接创建 cc.Event
对象,因为它是一个抽象类,请创建 cc.Event.EventCustom
对象来进行派发。
缓动系统(cc.tween)介绍
Cocos Creator 在 v2.0.9 提供了一套新的 API —— cc.tween
。cc.tween
能够对对象的任意属性进行缓动,功能类似于 cc.Action(动作系统)。但是 cc.tween
会比 cc.Action
更加简洁易用,因为 cc.tween
提供了链式创建的方法,可以对任何对象进行操作,并且可以对对象的任意属性进行缓动。
动作系统 是从 Cocos2d-x 迁移到 Cocos Creator 的,提供的 API 比较繁琐,只支持在节点属性上使用,并且如果要支持新的属性就需要再添加一个新的动作。为了提供更好的 API,cc.tween
在 动作系统 的基础上做了一层 API 封装。下面是 cc.Action
与 cc.tween
在使用上的对比:
-
cc.Action:
this.node.runAction( cc.sequence( cc.spawn( cc.moveTo(1, 100, 100), cc.rotateTo(1, 360), ), cc.scale(1, 2) ) )
-
cc.tween:
cc.tween(this.node) .to(1, { position: cc.v2(100, 100), rotation: 360 }) .to(1, { scale: 2 }) .start()
链式 API
cc.tween
的每一个 API 都会在内部生成一个 action,并将这个 action 添加到内部队列中,在 API 调用完后会再返回自身实例,这样就可以通过链式调用的方式来组织代码。
cc.tween
在调用 start 时会将之前生成的 action 队列重新组合生成一个 cc.sequence 队列,所以 cc.tween
的链式结构是依次执行每一个 API 的,也就是会执行完一个 API 再执行下一个 API。
cc.tween(this.node)
// 0s 时,node 的 scale 还是 1
.to(1, { scale: 2 })
// 1s 时,执行完第一个 action,scale 为 2
.to(1, { scale: 3 })
// 2s 时,执行完第二个 action,scale 为 3
.start()
// 调用 start 开始执行 cc.tween
设置缓动属性
cc.tween
提供了两个设置属性的 API:
-
to
:对属性进行绝对值计算,最终的运行结果即是设置的属性值 -
by
:对属性进行相对值计算,最终的运行结果是设置的属性值加上开始运行时节点的属性值
cc.tween(node)
.to(1, {scale: 2}) // node.scale === 2
.by(1, {scale: 2}) // node.scale === 4 (2+2)
.by(1, {scale: 1}) // node.scale === 5
.to(1, {scale: 2}) // node.scale === 2
.start()
支持缓动任意对象的任意属性
let obj = { a: 0 }
cc.tween(obj)
.to(1, { a: 100 })
.start()
同时执行多个属性
cc.tween(this.node)
// 同时对 scale, position, rotation 三个属性缓动
.to(1, { scale: 2, position: cc.v2(100, 100), rotation: 90 })
.start()
easing
你可以使用 easing 来使缓动更生动,cc.tween
针对不同的情况提供了多种使用方式。
// 传入 easing 名字,直接使用内置 easing 函数
cc.tween().to(1, { scale: 2 }, { easing: 'sineOutIn'})
// 使用自定义 easing 函数
cc.tween().to(1, { scale: 2 }, { easing: t => t*t; })
// 只对单个属性使用 easing 函数
// value 必须与 easing 或者 progress 配合使用
cc.tween().to(1, { scale: 2, position: { value: cc.v3(100, 100, 100), easing: 'sineOutIn' } })
Easing 类型说明可参考 API 文档。
音频播放
音频的加载方式请参考:声音资源
使用 AudioSource 组件播放
在 层级管理器 上创建一个空节点
选中空节点,在 属性检查器 最下方点击 添加组件 -> 其他组件 -> AudioSource 来添加 AudioSource 组件
-
将 资源管理器 中所需的音频资源拖拽到 AudioSource 组件的 Clip 中,如下所示:
然后根据需要对 AudioSource 组件的其他参数项进行设置即可,参数详情可参考 AudioSource 组件参考。
-
通过脚本控制 AudioSource 组件
如果只需要在游戏加载完成后自动播放音频,那么勾选 AudioSource 组件的 Play On Load 即可。如果要更灵活的控制 AudioSource 的播放,可以在自定义脚本中获取 AudioSource 组件,然后调用相应的 API,如下所示:
// AudioSourceControl.js cc.Class({ extends: cc.Component, properties: { audioSource: { type: cc.AudioSource, default: null }, }, play: function () { this.audioSource.play(); }, pause: function () { this.audioSource.pause(); }, });
然后在编辑器的 属性检查器 中添加对应的用户脚本组件。选择相对应的节点,在 属性检查器 最下方点击 添加组件 -> 用户脚本组件 -> 用户脚本,即可添加脚本组件。然后将带有 AudioSource 组件的节点拖拽到脚本组件中的 Audio Source 上,如下所示:
使用 AudioEngine 播放
AudioEngine 与 AudioSource 都能播放音频,它们的区别在于 AudioSource 是组件,可以添加到场景中,由编辑器设置。而 AudioEngine 是引擎提供的纯 API,只能在脚本中进行调用。如下所示:
在脚本的 properties 中定义一个 AudioClip 资源对象
-
直接使用
cc.audioEngine.play(audio, loop, volume);
播放,如下所示:// AudioEngine.js cc.Class({ extends: cc.Component, properties: { audio: { default: null, type: cc.AudioClip } }, onLoad: function () { this.current = cc.audioEngine.play(this.audio, false, 1); }, onDestroy: function () { cc.audioEngine.stop(this.current); } });
目前建议使用 audioEngine.play 接口来统一播放音频。或者也可以使用 audioEngine.playEffect 和 audioEngine.playMusic 这两个接口,前者主要是用于播放音效,后者主要是用于播放背景音乐。具体可查看 API 文档。
AudioEngine 播放的时候,需要注意这里传入的是一个完整的 AudioClip 对象(而不是 url)。所以不建议在 play 接口内直接填写音频的 url 地址,而是希望用户在脚本的 properties 中先定义一个 AudioClip,然后在编辑器的 属性检查器 中添加对应的用户脚本组件,将音频资源拖拽到脚本组件的 audio-clip 上。如下所示:
通过常驻节点进行场景资源管理和参数传递
引擎同时只会运行一个场景,当切换场景时,默认会将场景内所有节点和其他实例销毁。如果我们需要用一个组件控制所有场景的加载,或在场景之间传递参数数据,就需要将该组件所在节点标记为「常驻节点」,使它在场景切换时不被自动销毁,常驻内存。我们使用以下接口:
cc.game.addPersistRootNode(myNode);
上面的接口会将 myNode
变为常驻节点,这样挂在上面的组件都可以在场景之间持续作用,我们可以用这样的方法来储存玩家信息,或下一个场景初始化时需要的各种数据。
如果要取消一个节点的常驻属性:
cc.game.removePersistRootNode(myNode);
需要注意的是上面的 API 并不会立即销毁指定节点,只是将节点还原为可在场景切换时销毁的节点。
要注意的是 : 在creator的引擎里面只有根节点才能够被成功的设置为常驻节点,这一点貌似官方文档是没用提到的
creator 设置常驻节点
cc.game.addPersistRootNode(this.scrollview.node);
节点必须是根节点,如果不是可以将其父节点设为scene;
this.scrollview.node.parent = cc.director.getScene();