cocos creator游戏引擎浅析

生命周期回调

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 回调。倘若节点第一次被创建且 enabledtrue,则会在 onLoad 之后,start 之前被调用。

onDisable

当组件的 enabled 属性从 true 变为 false 时,或者所在节点的 active 属性从 true 变为 false 时,会激活 onDisable 回调。

onDestroy

当组件或者所在节点调用了 destroy(),则会调用 onDestroy 回调,并在当帧结束时统一回收组件。当同时声明了 onLoadonDestroy 时,它们将总是被成对调用。也就是说从组件初始化到销毁的过程中,它们要么就都会被调用,要么就都不会被调用。

Tips

一个组件从初始化到激活,再到最终销毁的完整生命周期函数调用顺序为:onLoad -> onEnable -> start -> update -> lateUpdate -> onDisable -> onDestroy

其中,onLoadstart 常常用于组件的初始化,只有在节点 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 而不是 removeFromParentdestroy 不但会激活组件上的 onDestroy,还会降低内存泄露的几率,同时减轻内存泄露时的后果。

总之,如果一个节点不再使用,destroy 就对了,不需要 removeFromParent 也不需要设置 parentnull


加载和切换场景

在 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);
  },
});

发射事件

发射事件有两种方式:emitdispatchEvent。两者的区别在于,后者可以做事件传递。我们先通过一个简单的例子来了解 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()

bubble-event

如上图所示,当我们从节点 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.tweencc.tween 能够对对象的任意属性进行缓动,功能类似于 cc.Action(动作系统)。但是 cc.tween 会比 cc.Action 更加简洁易用,因为 cc.tween 提供了链式创建的方法,可以对任何对象进行操作,并且可以对对象的任意属性进行缓动。

动作系统 是从 Cocos2d-x 迁移到 Cocos Creator 的,提供的 API 比较繁琐,只支持在节点属性上使用,并且如果要支持新的属性就需要再添加一个新的动作。为了提供更好的 API,cc.tween 在 动作系统 的基础上做了一层 API 封装。下面是 cc.Actioncc.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 组件播放

  1. 层级管理器 上创建一个空节点

  2. 选中空节点,在 属性检查器 最下方点击 添加组件 -> 其他组件 -> AudioSource 来添加 AudioSource 组件

  3. 资源管理器 中所需的音频资源拖拽到 AudioSource 组件的 Clip 中,如下所示:

    image

然后根据需要对 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 上,如下所示:

    image

使用 AudioEngine 播放

AudioEngine 与 AudioSource 都能播放音频,它们的区别在于 AudioSource 是组件,可以添加到场景中,由编辑器设置。而 AudioEngine 是引擎提供的纯 API,只能在脚本中进行调用。如下所示:

  1. 在脚本的 properties 中定义一个 AudioClip 资源对象

  2. 直接使用 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();

你可能感兴趣的:(cocos creator游戏引擎浅析)