安装Cocos Creator 下载 - 轻量高效的开发引擎
新建项目
打开其他项目
在 Dashboard 中,打开 新建项目 选项卡,选中 Hello World
项目模板。js
资源管理器
Scene 场景编辑器
Node Tree 层级管理器
属性检查器(Properties)
控件库
控制台(Console)
- 最左边选择预览窗口的比例大小,来模拟在不同移动设备上的显示效果
- Rotate 按钮决定显示横屏还是竖屏
- Debug Mode 里可以选择脚本中哪些级别的日志会输出到浏览器控制台中
- Show FPS 按钮可以选择是否显示每秒帧数和 Drawcall 数量等调试信息
- FPS 限制最高每秒帧数
- Pause 暂停游戏
- Recompile 重新编译项目脚本
脚本开发工作流程
节点 组件
创建组件脚本
将脚本添加到场景节点中,实际上就是为这个节点添加一份组件。我们先将刚刚创建出来的 NewScript.js 重命名为 say-hello.js。然后选中我们希望添加的场景节点,此时该节点的属性会显示在 属性检查器 中。在 属性检查器 的最下方有一个 添加组件 的按钮,点击按钮并选择 添加用户脚本 -> say-hello 来添加我们刚刚编写的脚本组件。
如果一切顺利,你将会看到你的脚本显示在 属性检查器 中:
注意:开发者也可以通过直接拖拽脚本资源到 属性检查器 的方式来添加脚本。
使用 cc.Class 声明类型
![](https://i-blog.csdnimg.cn/blog_migrate/c906daefd6a1de88e542b8dc2ecd94a6.png)
![](https://i-blog.csdnimg.cn/blog_migrate/fabdbba7b2d90b91ead9a7ce24716e1c.png)
//定义 CCClass
var Sprite = cc.Class({
name: "sprite",
//构造函数,使用 ctor 声明构造函数:
ctor: function () {
cc.log(this instanceof Sprite); // true
},
//实例方法 声明一个名叫 "print" 的实例方法
print: function () { },
// 继承,使用 extends 实现继承:CCClass 会统一自动调用父构造函数
extends: cc.Component,
//声明属性,可以将脚本组件中的字段可视化地展示在 属性检查器 中,从而方便地在场景中调整属性值。
properties: {
// 当声明的属性为基本 JavaScript 类型时,可以直接赋予默认值:
height: 20, // number
type: "actor", // string
loaded: false, // boolean
target: null, // object
//当声明的属性具备类型时(如:cc.Node,cc.Vec2 等),可以在声明处填写它们的构造函数来完成声明,如:
target: cc.Node,
pos: cc.Vec2,
//当声明属性的类型继承自 cc.ValueType 时(如:cc.Vec2,cc.Color 或 cc.Rect),除了上面的构造函数,还可以直接使用实例作为默认值:
poss: new cc.Vec2(10, 20),
color: new cc.Color(255, 255, 255, 128),
//当声明属性是一个数组时,可以在声明处填写它们的类型或构造函数来完成声明,如:
any: [], // 不定义具体类型的数组
bools: [cc.Boolean],
strings: [cc.String],
floats: [cc.Float],
ints: [cc.Integer],
values: [cc.Vec2],
nodes: [cc.Node],
frames: [cc.SpriteFrame],
//完整声明
score: {
//score 的默认值为 0
default: 0,
//在 属性检查器 面板中显示成指定名字
displayName: "Score (player)",
// 在属性检查器面板指定名字上方显示tooltip值
tooltip: "The score of player",
// type:限定属性的数据类型,详见 CCClass 进阶参考:type 参数
type:Number,
// visible:设为 false 则不在 属性检查器 面板中显示该属性
// visible:false,
// serializable:设为 false 则不序列化(保存)该属性
serializable:false
},
//数组声明,数组的 default 必须设置为 [],如果要在 属性检查器 中编辑,还需要设置 type 为构造函数,枚举,或者 cc.Integer,cc.Float,cc.Boolean 和 cc.String
names: {
default: [],
type: [cc.String] // 用 type 指定数组的每个元素都是字符串类型
},
enemies: {
default: [],
type: [cc.Node] // type 同样写成数组,提高代码可读性
},
//get/set 声明
foo: {
// ATTRIBUTES:
default: null, // The default value will be used only when the component attaching
// to a node for the first time
type: cc.SpriteFrame, // optional, default is typeof default
serializable: true, // optional, default is true
},
bar: {
get () {
return this._bar;
},
set (value) {
this._bar = value;
}
},
},
// LIFE-CYCLE CALLBACKS:
// onLoad () {},
start () {
},
// update (dt) {},
});
// 实例化
// Sprite 变量保存的是一个 JavaScript 构造函数,可以直接 new 出一个对象:
var obj = new Sprite();
// 判断类型
// 需要做类型判断时,可以用 JavaScript 原生的 instanceof:
cc.log(obj instanceof Sprite); // true
//继承后,CCClass 会统一自动调用父构造函数。
var Square = cc.Class({
extends: Sprite,
ctor: function () {
cc.log("Square"); // 再调用子构造函数
}
});
var square = new Square();// 实例化时,父构造函数会自动调用, 再调用子构造函数 以上代码将依次输出 true 和 Square
访问节点和组件
获得组件所在的节点
获得组件所在的节点很简单,只要在组件方法里访问 this.node
变量:
start () {
var node = this.node;
node.x = 100;
}
获得其它组件
获得同一个节点上的其它组件,用 getComponent
这个 API
start: function () {
var label = this.getComponent(cc.Label);
var text = this.name + 'started';
label.string = text;
}
为 getComponent
传入一个类名,SinRotate.js
里声明的组件,类名就是 SinRotate
var rotate = this.getComponent("SinRotate");
在节点上也有一个 getComponent
方法,它们的作用是一样的:获取挂载节点/组件
start: function () {
cc.log(this.node.getComponent(cc.Label) === this.getComponent(cc.Label)); // true
}
组件是否存在,节点上找不到你要的组件,getComponent
将返回 null
start: function () {
var label = this.getComponent(cc.Label);
if (label) {
label.string = "Hello";
}
else {
cc.error("Something wrong?");
}
}
获得其它节点及其组件
利用属性检查器设置节点
// Cannon.js
cc.Class({
extends: cc.Component,
properties: {
// 声明 player 属性
player: {
default: null,
type: cc.Node
}
}
});
接着你就可以将 层级管理器 中的任意一个节点拖到这个 Player 控件:
player 属性被设置成功,可以直接在 Cannon.js脚本里访问 player
// Cannon.js
cc.Class({
extends: cc.Component,
properties: {
// 声明 player 属性
player: {
default: null,
type: cc.Node
}
},
start: function () {
//访问 player
cc.log(this.player.name);
},
// ...
});
利用属性检查器设置组件
模块化 方式获取到脚本(例如 Player.js,
将 Cannon.js
中属性的 type 声明为 Player 脚本组件:
// Cannon.js
// 通过模块化方式获取脚本 “Player”
var Player = require("Player");
cc.Class({
extends: cc.Component,
properties: {
// 声明 player 属性,直接声明为组件类型
player: {
default: null,
type: Player
}
},
start: function () {
cc.log("The player is " + this.player.name);
},
// ...
});
这样 player
属性就相当于是这个节点上的 Player 脚本组件了,就不需要再自己手动调用 getComponent
来获取组件了。
还可以将属性的默认值由 null
改为数组 []
,这样你就能在 属性检查器 中同时设置多个对象
查找子节点
// CannonManager.js
cc.Class({
extends: cc.Component,
start: function () {
var cannons = this.node.children;
// ...
}
});
还可以使用 getChildByName
:
this.node.getChildByName("Cannon 01");
如果子节点的层次较深,还可以使用 cc.find
,cc.find
将根据传入的路径进行逐级查找:
cc.find("Cannon 01/Barrel/SFX", this.node);
全局名字查找
当 cc.find
只传入第一个参数时,将从场景根节点开始逐级查找:
this.backNode = cc.find("Canvas/Menu/Back");
通过全局变量访问
全局对象 window.Global
,这个对象里面包含了 backNode
和 backLabel
两个属性。
// Globals.js, this file can have any name
window.Global = {
backNode: null,
backLabel: null,
};
// Back.js
cc.Class({
extends: cc.Component,
onLoad: function () {
Global.backNode = this.node;
Global.backLabel = this.getComponent(cc.Label);
}
在任何地方访问到 Global
里的值:
// AnyScript.js
cc.Class({
extends: cc.Component,
// start 会在 onLoad 之后执行,所以这时 Global 已经初始化过了
start: function () {
var text = 'Back';
Global.backLabel.string = text;
}
});
通过模块访问
如果你不想用全局变量,你可以使用 require
来实现脚本的跨文件操作,让我们看个示例:
// Global.js, now the filename matters
module.exports = {
backNode: null,
backLabel: null,
};
每个脚本都能用 require
+ 文件名(不含路径) 来获取到对方 exports 的对象。
// Back.js
// this feels more safe since you know where the object comes from
var Global = require("Global");
cc.Class({
extends: cc.Component,
onLoad: function () {
Global.backNode = this.node;
Global.backLabel = this.getComponent(cc.Label);
}
});
// AnyScript.js
// this feels more safe since you know where the object comes from
var Global = require("Global");
cc.Class({
extends: cc.Component,
// start 会在 onLoad 之后执行,所以这时 Global 已经初始化过了
start: function () {
var text = "Back";
Global.backLabel.string = text;
}
});
常用节点和组件接口
激活/关闭节点
this.node.active = false;//节点默认是激活的
更改节点的父节点
假设父节点为 parentNode
,子节点为 this.node
将
子节点变为父节点
this.node.parent = parentNode;
或
this.node.removeFromParent(false);
parentNode.addChild(this.node);
索引节点的子节点
this.node.children
将返回节点的所有子节点数组。this.node.childrenCount
将返回节点的子节点数量。
更改节点的变换(位置、旋转、缩放、尺寸)
更改节点位置
分别对 x 轴和 y 轴坐标赋值:
this.node.x = 100;
this.node.y = 50;
还可以使用 setPosition
方法进行赋值:
this.node.setPosition(100, 50);
this.node.setPosition(cc.v2(100, 50));
或者通过设置 position
变量进行赋值:
this.node.position = cc.v2(100, 50);
更改节点旋转
this.node.rotation = 90;
或
this.node.setRotation(90);
更改节点缩放
this.node.scaleX = 2;
this.node.scaleY = 2;
或
this.node.setScale(2);
this.node.setScale(2, 2);
更改节点尺寸
this.node.setContentSize(100, 100);
this.node.setContentSize(cc.size(100, 100));
或
this.node.width = 100;
this.node.height = 100;
更改节点锚点位置
this.node.anchorX = 1;
this.node.anchorY = 0;
或
this.node.setAnchorPoint(1, 0);
颜色和不透明度
Sprite 的实例为 mySprite
,设置它的颜色:
mySprite.node.color = cc.Color.RED;
设置不透明度:
mySprite.node.opacity = 128;
生命周期回调
- onLoad 初始化
- start 初始化经常修改的数据
- update 每一帧渲染前更新物体的行为,状态和方位
- lateUpdate 在所有组件的
update
都执行完之后才进行其它操作 - onDestroy 所在节点的
active
属性从true
变为false
时 - onEnable 组件的或者所在节点的
enabled
/active
属性从true
变为false
时 - onDisable 当组件或者所在节点调用了
destroy()
创建和销毁节点
脚本中动态创建子节点
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;
}
},
加载和切换场景
使用场景文件名(不包含扩展名)来索引指代场景:
cc.director.loadScene("MyScene");
cc.director.runScene();
除此之外,从 v2.4 开始 Asset Bundle 还增加了一种新的加载方式:
bundle.loadScene('MyScene', function (err, scene) {
cc.director.runScene(scene);
});
通过常驻节点进行场景资源管理和参数传递
引擎同时只会运行一个场景,当切换场景时,默认会将场景内所有节点和其他实例销毁。如果我们需要用一个组件控制所有场景的加载,或在场景之间传递参数数据,就需要将该组件所在节点标记为「常驻节点」,使它在场景切换时不被自动销毁,常驻内存。储存玩家信息,或下一个场景初始化时需要的各种数据
cc.game.addPersistRootNode(myNode);
取消一个节点的常驻属性:
cc.game.removePersistRootNode(myNode);
场景加载回调
加载场景时,可以附加一个参数用来指定场景加载后的回调函数:
cc.director.loadScene("MyScene", onSceneLaunched);
预加载场景
cc.director.loadScene
会在加载场景之后自动切换运行新场景,有些时候我们需要在后台静默加载新场景,并在加载完成后手动进行切换。那就可以预先使用 cc.director.preloadScene
接口对场景进行预加载:
cc.director.preloadScene("table", function () {
cc.log("Next scene preloaded");
});
之后在合适的时间调用 loadScene
,就可以真正切换场景。
cc.director.loadScene("table");
加载资源
动态加载 resources
// 加载 Prefab
cc.resources.load("test assets/prefab", function (err, prefab) {
var newNode = cc.instantiate(prefab);
cc.director.getScene().addChild(newNode);
});
// 加载 AnimationClip
var self = this;
cc.resources.load("test assets/anim", function (err, clip) {
self.node.getComponent(cc.Animation).addClip(clip, "anim");
});
加载 SpriteFrame
图片设置为 Sprite 后,将会在 资源管理器 中生成一个对应的 SpriteFrame
// 加载 SpriteFrame
var self = this;
cc.resources.load("test assets/image", cc.SpriteFrame, function (err, spriteFrame) {
self.node.getComponent(cc.Sprite).spriteFrame = spriteFrame;
});
加载图集中的 SpriteFrame
对从 TexturePacker 等第三方工具导入的图集而言,如果要加载其中的 SpriteFrame,则只能先加载图集,再获取其中的 SpriteFrame。这是一种特殊情况。
// 加载 SpriteAtlas(图集),并且获取其中的一个 SpriteFrame // 注意 atlas 资源文件(plist)通常会和一个同名的图片文件(png)放在一个目录下, 所以需要在第二个参数指定资源类型 cc.resources.load("test assets/sheep", cc.SpriteAtlas, function (err, atlas) { var frame = atlas.getSpriteFrame('sheep_down_0'); //其中的一个 SpriteFrame sprite.spriteFrame = frame; });
资源释放
cc.resources.load
加载进来的单个资源如果需要释放,可以调用 cc.resources.release
,release
可以传入和 cc.resources.load
相同的路径和类型参数。
cc.resources.release("test assets/image", cc.SpriteFrame);
cc.resources.release("test assets/anim");
此外,你也可以使用 cc.assetManager.releaseAsset
来释放特定的 Asset 实例。
cc.assetManager.releaseAsset(spriteFrame);
cc.resources.loadDir
可以加载相同路径下的多个资源:
// 加载 test assets 目录下所有资源
cc.resources.loadDir("test assets", function (err, assets) {
// ...
});
// 加载 test assets 目录下所有 SpriteFrame,并且获取它们的路径
cc.resources.loadDir("test assets", cc.SpriteFrame, function (err, assets) {
// ...
});
预加载资源
从 v2.4 开始,除了场景能够预加载之外,其他资源也可以预加载。预加载的加载参数与正常加载时一样,不过预加载只会去下载必要的资源,并不会进行资源的反序列化和初始化工作,所以性能消耗更小,适合游戏运行中使用。
cc.resources
提供了 preload
和 preloadDir
用于预加载资源。
cc.resources.preload('test assets/image', cc.SpriteFrame);
// wait for while
cc.resources.load('test assets/image', cc.SpriteFrame, function (err, spriteFrame) {
self.node.getComponent(cc.Sprite).spriteFrame = spriteFrame;
});
加载远程资源和设备资源
在目前的 Cocos Creator 中,我们支持加载远程贴图资源,这对于加载用户头像等需要向服务器请求的贴图很友好,需要注意的是,这需要开发者直接调用 cc.assetManager.loadRemote
方法。同时,如果开发者用其他方式下载了资源到本地设备存储中,也需要用同样的 API 来加载,上文中的 cc.resources.load
等 API 只适用于应用包内的资源和热更新的本地资源。下面是这个 API 的用法:
// 远程 url 带图片后缀名
var remoteUrl = "http://unknown.org/someres.png";
cc.assetManager.loadRemote(remoteUrl, function (err, texture) {
// Use texture to create sprite frame
});
// 远程 url 不带图片后缀名,此时必须指定远程图片文件的类型
remoteUrl = "http://unknown.org/emoji?id=124982374";
cc.assetManager.loadRemote(remoteUrl, {ext: '.png'}, function () {
// Use texture to create sprite frame
});
// 用绝对路径加载设备存储内的资源,比如相册
var absolutePath = "/dara/data/some/path/to/image.png"
cc.assetManager.loadRemote(absolutePath, function () {
// Use texture to create sprite frame
});
// 远程音频
remoteUrl = "http://unknown.org/sound.mp3";
cc.assetManager.loadRemote(remoteUrl, function (err, audioClip) {
// play audio clip
});
// 远程文本
remoteUrl = "http://unknown.org/skill.txt";
cc.assetManager.loadRemote(remoteUrl, function (err, textAsset) {
// use string to do something
});
目前的此类手动资源加载还有一些限制,对开发者影响比较大的是:
- 这种加载方式只支持图片、声音、文本等原生资源类型,不支持 SpriteFrame、SpriteAtlas、Tilemap 等资源的直接加载和解析。(如需远程加载所有资源,可使用 Asset Bundle)
- Web 端的远程加载受到浏览器的 CORS 跨域策略限制,如果对方服务器禁止跨域访问,那么会加载失败,而且由于 WebGL 安全策略的限制,即便对方服务器允许 http 请求成功之后也无法渲染。
接下来要介绍问题的另一个核心:JavaScript 中无法跟踪对象引用。
在 JavaScript 这种脚本语言中,由于其弱类型特性,以及为了代码的便利,往往是不包含内存管理功能的,所有对象的内存都由垃圾回收机制来管理。这就导致 JS 层逻辑永远不知道一个对象会在什么时候被释放,这意味着引擎无法通过类似引用计数的机制来管理外部对象对资源的引用,也无法严谨地统计资源是否不再被需要了。
在 v2.4 之前,Creator 很长时间里选择让开发者控制所有资源的释放,包括资源本身和它的依赖项,你必须手动获取资源所有的依赖项并选择需要释放的依赖项,例如如下形式:
// 直接释放某个贴图 cc.loader.release(texture); // 释放一个 prefab 以及所有它依赖的资源 var deps = cc.loader.getDependsRecursively('prefabs/sample'); cc.loader.release(deps); // 如果在这个 prefab 中有一些和场景其他部分共享的资源,你不希望它们被释放,可以将这个资源从依赖列表中删除 var deps = cc.loader.getDependsRecursively('prefabs/sample'); var index = deps.indexOf(texture2d._uuid); if (index !== -1) deps.splice(index, 1); cc.loader.release(deps);
这种方案给予了开发者最大的控制权力,对于小型项目来说工作良好,但随着 Creator 的发展,项目的规模不断提升,场景所引用的资源不断增加,而其他场景可能也复用了这些资源,这会造成释放资源的复杂度越来越高,开发者需要掌握所有资源的使用非常困难。为了解决这个痛点,Asset Manager 提供了一套基于引用计数的资源释放机制,让开发者可以简单高效地释放资源,不用担心项目规模的急剧膨胀。
这一套方案所做的工作是通过 AssetManager 加载资源时,对资源的依赖资源进行分析记录,并增加引用。而在通过 AssetManager 释放资源时,拿到记录的依赖资源,取消引用,并根据依赖资源的引用数,尝试自动去释放依赖资源。所以这个方案引擎只对资源的静态引用进行了分析,也就是说如果开发者在游戏运行过程中动态加载了资源并设置给场景或其他资源,则这些动态加载出来的资源引擎是没有记录的,这些资源需要开发者进行管理。每一个资源对象都提供了两个方法
addRef
,decRef
,你可以使用这两个接口来对动态资源的引用进行控制,比如说:cc.resources.load('image', cc.SpriteFrame, (err, spriteFrame) => { this.spriteFrame = spriteFrame; spriteFrame.addRef(); });
因为 texture 是动态加载进来的,而不是一开始就被组件所引用,所以这个 texture 是没有记录的,他的引用计数是 0,为了避免这个 texture 被其他地方误释放,开发者需要手动执行
addRef
操作为其增加一个引用。而在你不再需要使用这个资源时,你需要执行decRef
为其减少一个引用:this.spriteFrame.decRef(); this.spriteFrame = null;
监听和发射事件
监听事件
事件处理是在节点(cc.Node
)中完成的。对于组件,可以通过访问节点 this.node
来注册和监听事件。监听事件可以通过 this.node.on()
函数来注册,方法如下:
cc.Class({
extends: cc.Component,
properties: {
},
onLoad: function () {
this.node.on('mousedown', function ( event ) {
console.log('Hello!');
});
/ 使用函数绑定
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
方法关闭对应的监听事件
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.Event
类型的事件对象 event
,stopPropagation
就是 cc.Event
的标准 API,其它重要的 API 包含:
API 名 | 类型 | 意义 |
---|---|---|
type | String | 事件的类型(事件名) |
target | cc.Node | 接收到事件的原始对象 |
currentTarget | cc.Node | 接收到事件的当前对象,事件在冒泡阶段当前对象可能与原始对象不同 |
getType | Function | 获取事件的类型 |
stopPropagation | Function | 停止冒泡阶段,事件将不会继续向父节点传递,当前节点的剩余监听器仍然会接收到事件 |
stopPropagationImmediate | Function | 立即停止事件的传递,事件将不会传给父节点以及当前节点的剩余监听器 |
getCurrentTarget | Function | 获取当前接收到事件的目标节点 |
detail | Function | 自定义事件的信息(属于 cc.Event.EventCustom ) |
setUserData | Function | 设置自定义事件的信息(属于 cc.Event.EventCustom ) |
getUserData | Function | 获取自定义事件的信息(属于 cc.Event.EventCustom ) |
完整的 API 列表可以参考 cc.Event
及其子类的 API 文档。
系统内置事件
以上是通用的事件监听和发射规则,在 Cocos Creator 中,我们默认支持了一些系统内置事件,可以参考我们后续的文档来查看如何使用:
节点系统事件
// 使用枚举类型来注册
node.on(cc.Node.EventType.MOUSE_DOWN, function (event) {
console.log('Mouse down');
}, this);
// 使用事件名来注册
node.on('mousedown', function (event) {
console.log('Mouse down');
}, this);
鼠标事件类型和事件对象
鼠标事件在桌面平台才会触发,系统提供的事件类型如下:
枚举对象定义 | 对应的事件名 | 事件触发的时机 |
---|---|---|
cc.Node.EventType.MOUSE_DOWN | mousedown | 当鼠标在目标节点区域按下时触发一次 |
cc.Node.EventType.MOUSE_ENTER | mouseenter | 当鼠标移入目标节点区域时,不论是否按下 |
cc.Node.EventType.MOUSE_MOVE | mousemove | 当鼠标在目标节点区域中移动时,不论是否按下 |
cc.Node.EventType.MOUSE_LEAVE | mouseleave | 当鼠标移出目标节点区域时,不论是否按下 |
cc.Node.EventType.MOUSE_UP | mouseup | 当鼠标从按下状态松开时触发一次 |
cc.Node.EventType.MOUSE_WHEEL | mousewheel | 当鼠标滚轮滚动时 |
鼠标事件(cc.Event.EventMouse
)的重要 API 如下(cc.Event
标准事件 API 除外):
函数名 | 返回值类型 | 意义 |
---|---|---|
getScrollY | Number | 获取滚轮滚动的 Y 轴距离,只有滚动时才有效 |
getLocation | Object | 获取鼠标位置对象,对象包含 x 和 y 属性 |
getLocationX | Number | 获取鼠标的 X 轴位置 |
getLocationY | Number | 获取鼠标的 Y 轴位置 |
getPreviousLocation | Object | 获取鼠标事件上次触发时的位置对象,对象包含 x 和 y 属性 |
getDelta | Object | 获取鼠标距离上一次事件移动的距离对象,对象包含 x 和 y 属性 |
getButton | Number | cc.Event.EventMouse.BUTTON_LEFT 或 cc.Event.EventMouse.BUTTON_RIGHT 或 cc.Event.EventMouse.BUTTON_MIDDLE |
触摸事件类型和事件对象
触摸事件在移动平台和桌面平台都会触发,这样做的目的是为了更好得服务开发者在桌面平台调试,只需要监听触摸事件即可同时响应移动平台的触摸事件和桌面端的鼠标事件。系统提供的触摸事件类型如下:
枚举对象定义 | 对应的事件名 | 事件触发的时机 |
---|---|---|
cc.Node.EventType.TOUCH_START | touchstart | 当手指触点落在目标节点区域内时 |
cc.Node.EventType.TOUCH_MOVE | touchmove | 当手指在屏幕上移动时 |
cc.Node.EventType.TOUCH_END | touchend | 当手指在目标节点区域内离开屏幕时 |
cc.Node.EventType.TOUCH_CANCEL | touchcancel | 当手指在目标节点区域外离开屏幕时 |
触摸事件(cc.Event.EventTouch
)的重要 API 如下(cc.Event
标准事件 API 除外):
API 名 | 类型 | 意义 |
---|---|---|
touch | cc.Touch | 与当前事件关联的触点对象 |
getID | Number | 获取触点的 ID,用于多点触摸的逻辑判断 |
getLocation | Object | 获取触点位置对象,对象包含 x 和 y 属性 |
getLocationX | Number | 获取触点的 X 轴位置 |
getLocationY | Number | 获取触点的 Y 轴位置 |
getPreviousLocation | Object | 获取触点上一次触发事件时的位置对象,对象包含 x 和 y 属性 |
getStartLocation | Object | 获取触点初始时的位置对象,对象包含 x 和 y 属性 |
getDelta | Object | 获取触点距离上一次事件移动的距离对象,对象包含 x 和 y 属性 |
调用 event.stopPropagation()
可以主动停止冒泡过程。
需要注意的是,触摸事件支持多点触摸,每个触点都会发送一次事件给事件监听器。
多点触摸事件
引擎在 v2.3 版本中新增了多点触摸事件的屏蔽开关,多点触摸事件默认为开启状态。对于有些类型的项目为了防止多点误触,需要屏蔽多点触摸事件,可以通过以下代码进行关闭:
cc.macro.ENABLE_MULTI_TOUCH = false;
暂停或恢复节点系统事件
暂停节点系统事件
// 暂停当前节点上注册的所有节点系统事件,节点系统事件包含触摸和鼠标事件。
// 如果传递参数 true,那么这个 API 将暂停本节点和它的所有子节点上的节点系统事件。
// example
this.node.pauseSystemEvents();
恢复节点系统事件
// 恢复当前节点上注册的所有节点系统事件,节点系统事件包含触摸和鼠标事件。
// 如果传递参数 true,那么这个 API 将恢复本节点和它的所有子节点上的节点系统事件。
// example
this.node.resumeSystemEvents();
全局系统事件
全局系统事件是指与节点树不相关的各种全局事件,由 cc.systemEvent
来统一派发,目前支持以下几种事件:
- 键盘事件
- 设备重力传感事件
除此之外,鼠标事件与触摸事件请参考 节点系统事件
注意:目前已经不建议直接使用
cc.eventManager
来注册任何事件,cc.eventManager
的用法也不保证持续性,有可能随时被修改。
如何定义输入事件
键盘、设备重力传感器此类全局事件是通过函数 cc.systemEvent.on(type, callback, target)
注册的。
可选的 type
类型有:
- cc.SystemEvent.EventType.KEY_DOWN (键盘按下)
- cc.SystemEvent.EventType.KEY_UP (键盘释放)
- cc.SystemEvent.EventType.DEVICEMOTION (设备重力传感)
键盘事件
- 事件监听器类型:
cc.SystemEvent.EventType.KEY_DOWN
和cc.SystemEvent.EventType.KEY_UP
- 事件触发后的回调函数:
- 自定义回调函数:
callback(event);
- 自定义回调函数:
- 回调参数:
cc.Class({
extends: cc.Component,
onLoad: function () {
// add key down and key up event
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
},
onDestroy () {
cc.systemEvent.off(cc.SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this);
cc.systemEvent.off(cc.SystemEvent.EventType.KEY_UP, this.onKeyUp, this);
},
onKeyDown: function (event) {
switch(event.keyCode) {
case cc.macro.KEY.a:
console.log('Press a key');
break;
}
},
onKeyUp: function (event) {
switch(event.keyCode) {
case cc.macro.KEY.a:
console.log('release a key');
break;
}
}
});
设备重力传感事件
- 事件监听器类型:
cc.SystemEvent.EventType.DEVICEMOTION
- 事件触发后的回调函数:
- 自定义回调函数:
callback(event);
- 自定义回调函数:
- 回调参数:
- Event:API 传送门
cc.Class({
extends: cc.Component,
onLoad () {
// open Accelerometer
cc.systemEvent.setAccelerometerEnabled(true);
cc.systemEvent.on(cc.SystemEvent.EventType.DEVICEMOTION, this.onDeviceMotionEvent, this);
},
onDestroy () {
cc.systemEvent.off(cc.SystemEvent.EventType.DEVICEMOTION, this.onDeviceMotionEvent, this);
},
onDeviceMotionEvent (event) {
cc.log(event.acc.x + " " + event.acc.y);
},
});
在 Cocos Creator 中使用缓动系统(cc.tween)
缓动系统(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 文档。
自定义 progress
相对于 easing,自定义 progress 函数可以更自由的控制缓动的过程。
// 对所有属性自定义 progress
cc.tween().to(1, { scale: 2, rotation: 90 }, {
progress: (start, end, current, ratio) => {
return start + (end - start) * ratio;
}
})
// 对单个属性自定义 progress
cc.tween().to(1, {
scale: 2,
position: {
value: cc.v3(),
progress: (start, end, current, t) => {
// 注意,传入的属性为 cc.Vec3,所以需要使用 Vec3.lerp 进行插值计算
return start.lerp(end, t, current);
}
}
})
复制缓动
clone 函数会克隆一个当前的缓动,并接受一个 target 作为参数。
// 先创建一个缓动作为模板
let tween = cc.tween().to(4, { scale: 2 })
// 复制 tween,并使用节点 Canvas/cocos 作为 target
tween.clone(cc.find('Canvas/cocos')).start()
// 复制 tween,并使用节点 Canvas/cocos2 作为 target
tween.clone(cc.find('Canvas/cocos2')).start()
插入其他的缓动到队列中
你可以事先创建一些固定的缓动,然后通过组合这些缓动形成新的缓动来减少代码的编写。
let scale = cc.tween().to(1, { scale: 2 })
let rotate = cc.tween().to(1, { rotation: 90})
let move = cc.tween().to(1, { position: cc.v3(100, 100, 100)})
// 先缩放再旋转
cc.tween(this.node).then(scale).then(rotate)
// 先缩放再移动
cc.tween(this.node).then(scale).then(move)
并行执行缓动
cc.tween
在链式执行时是按照 sequence 的方式来执行的,但是在编写复杂缓动的时候可能会需要同时并行执行多个队列,cc.tween
提供了 parallel 接口来满足这个需求。
let t = cc.tween;
t(this.node)
// 同时执行两个 cc.tween
.parallel(
t().to(1, { scale: 2 }),
t().to(2, { position: cc.v2(100, 100) })
)
.call(() => {
console.log('All tweens finished.')
})
.start()
回调
cc.tween(this.node)
.to(2, { rotation: 90})
.to(1, { scale: 2})
// 当前面的动作都执行完毕后才会调用这个回调函数
.call(() => { cc.log('This is a callback') })
.start()
重复执行
repeat/repeatForever 函数会将前一个 action 作为作用对象。但是如果有参数提供了其他的 action 或者 tween,则 repeat/repeatForever 函数会将传入的 action 或者 tween 作为作用对象。
cc.tween(this.node)
.by(1, { scale: 1 })
// 对前一个 by 重复执行 10次
.repeat(10)
// 最后 node.scale === 11
.start()
// 也可以这样用
cc.tween(this.node)
.repeat(10,
cc.tween().by(1, { scale: 1 })
)
.start()
// 一直重复执行下去
cc.tween(this.node)
.by(1, { scale: 1 })
.repeatForever()
.start()
延迟执行
cc.tween(this.node)
// 延迟 1s
.delay(1)
.to(1, { scale: 2 })
// 再延迟 1s
.delay(1)
.to(1, { scale: 3 })
.start()
在 Cocos Creator 中使用动作系统
动作系统目前已不推荐使用,未来将逐步移除,建议使用 缓动系统 做为替代。
动作系统简介
Cocos Creator 提供的动作系统源自 Cocos2d-x,API 和使用方法均一脉相承。动作系统可以在一定时间内对节点完成位移,缩放,旋转等各种动作。
需要注意的是,动作系统并不能取代 动画系统,动作系统提供的是面向程序员的 API 接口,而动画系统则是提供在编辑器中来设计的。同时,它们服务于不同的使用场景,动作系统比较适合来制作简单的形变和位移动画,而动画系统则强大许多,美术可以用编辑器制作支持各种属性,包含运动轨迹和缓动的复杂动画。
动作系统 API
动作系统的使用方式也很简单,在 cc.Node
中支持如下 API:
// 创建一个移动动作
var action = cc.moveTo(2, 100, 100);
// 执行动作
node.runAction(action);
// 停止一个动作
node.stopAction(action);
// 停止所有动作
node.stopAllActions();
开发者还可以给动作设置 tag,并通过 tag 来控制动作。
// 给 action 设置 tag
var ACTION_TAG = 1;
action.setTag(ACTION_TAG);
// 通过 tag 获取 action
node.getActionByTag(ACTION_TAG);
// 通过 tag 停止一个动作
node.stopActionByTag(ACTION_TAG);
动作类型
在 Cocos Creator 中支持非常丰富的各种动作,这些动作主要分为以下几大类。
(由于动作类型过多,在这里不展开描述每个动作的用法,开发者可以参考 动作系统 API 列表 来查看所有动作。)
基础动作
基础动作就是实现各种形变,位移动画的动作,比如 cc.moveTo
用来移动节点到某个位置;cc.rotateBy
用来旋转节点一定的角度;cc.scaleTo
用来缩放节点。
基础动作中分为时间间隔动作和即时动作,前者是在一定时间间隔内完成的渐变动作,前面提到的都是时间间隔动作,它们全部继承自 cc.ActionInterval。后者则是立即发生的,比如用来调用回调函数的 cc.callFunc
;用来隐藏节点的 cc.hide
,它们全部继承自 cc.ActionInstant。
容器动作
容器动作可以以不同的方式将动作组织起来,下面是几种容器动作的用途:
-
顺序动作
cc.sequence
顺序动作可以让一系列子动作按顺序一个个执行。示例:// 让节点左右来回移动 var seq = cc.sequence(cc.moveBy(0.5, 200, 0), cc.moveBy(0.5, -200, 0)); node.runAction(seq);
-
同步动作
cc.spawn
同步动作可以同步执行对一系列子动作,子动作的执行结果会叠加起来修改节点的属性。示例:// 让节点在向上移动的同时缩放 var spawn = cc.spawn(cc.moveBy(0.5, 0, 50), cc.scaleTo(0.5, 0.8, 1.4)); node.runAction(spawn);
-
重复动作
cc.repeat
重复动作用来多次重复一个动作。示例:// 让节点左右来回移动,并重复 5 次 var seq = cc.repeat( cc.sequence( cc.moveBy(2, 200, 0), cc.moveBy(2, -200, 0) ), 5); node.runAction(seq);
-
永远重复动作
cc.repeatForever
顾名思义,这个动作容器可以让目标动作一直重复,直到手动停止。// 让节点左右来回移动并一直重复 var seq = cc.repeatForever( cc.sequence( cc.moveBy(2, 200, 0), cc.moveBy(2, -200, 0) ));
-
速度动作
cc.speed
速度动作可以改变目标动作的执行速率,让动作更快或者更慢完成。// 让目标动作速度加快一倍,相当于原本 2 秒的动作在 1 秒内完成 var action = cc.speed( cc.spawn( cc.moveBy(2, 0, 50), cc.scaleTo(2, 0.8, 1.4) ), 2); node.runAction(action);
从上面的示例中可以看出,不同容器类型是可以复合的,除此之外,我们给容器类型动作提供了更为方便的链式 API,动作对象支持以下三个 API:repeat
、repeatForever
、speed
,这些 API 都会返回动作对象本身,支持继续链式调用。我们来看一个更复杂的动作示例:
// 一个复杂的跳跃动画
this.jumpAction = cc.sequence(
cc.spawn(
cc.scaleTo(0.1, 0.8, 1.2),
cc.moveTo(0.1, 0, 10)
),
cc.spawn(
cc.scaleTo(0.2, 1, 1),
cc.moveTo(0.2, 0, 0)
),
cc.delayTime(0.5),
cc.spawn(
cc.scaleTo(0.1, 1.2, 0.8),
cc.moveTo(0.1, 0, -10)
),
cc.spawn(
cc.scaleTo(0.2, 1, 1),
cc.moveTo(0.2, 0, 0)
)
// 以 1/2 的速度慢放动画,并重复 5 次
).speed(2).repeat(5);
动作回调
动作回调可以用以下的方式声明:
var finished = cc.callFunc(this.myMethod, this, opt);
cc.callFunc
第一个参数是处理回调的方法,即可以使用 CCClass 的成员方法,也可以声明一个匿名函数:
var finished = cc.callFunc(function () {
// doSomething
}, this, opt);
第二个参数指定了处理回调方法的 context(也就是绑定 this),第三个参数是向处理回调方法的传参。您可以这样使用传参:
var finished = cc.callFunc(function(target, score) {
this.score += score;
}, this, 100); //动作完成后会给玩家加 100 分
在声明了回调动作 finished
后,您可以配合 cc.sequence
来执行一整串动作并触发回调:
var myAction = cc.sequence(cc.moveBy(1, cc.v2(0, 100)), cc.fadeOut(1), finished);
在同一个 sequence 里也可以多次插入回调:
var myAction = cc.sequence(cc.moveTo(1, cc.v2(0, 0)), finished1, cc.fadeOut(1), finished2); // finished1、finished2 都是使用 cc.callFunc 定义的回调动作
注意:在 cc.callFunc
中不应该停止自身动作,由于动作是不能被立即删除,如果在动作回调中暂停自身动作会引发一系列遍历问题,导致更严重的 bug。
缓动动作
缓动动作不可以单独存在,它永远是为了修饰基础动作而存在的,它可以用来修改基础动作的时间曲线,让动作有快入、缓入、快出或其它更复杂的特效。需要注意的是,只有时间间隔动作才支持缓动:
var action = cc.scaleTo(0.5, 2, 2);
action.easing(cc.easeIn(3.0));
基础的缓动动作类是 cc.ActionEase。各种缓动动作的时间曲线可以参考下图:
图片源自 Tweener Documentation and Language Reference
使用计时器
在 Cocos Creator 中,我们为组件提供了方便的计时器,这个计时器源自于 Cocos2d-x 中的 cc.Scheduler
,我们将它保留在了 Cocos Creator 中并适配了基于组件的使用方式。
也许有人会认为 setTimeout
和 setInterval
就足够了,开发者当然可以使用这两个函数,不过我们更推荐使用计时器,因为它更加强大灵活,和组件也结合得更好!
下面来看看它的具体使用方式:
首先,先创建一个指向某个组件的变量,变量名为 component。
-
开始一个计时器
component.schedule(function() { // 这里的 this 指向 component this.doSomething(); }, 5);
上面这个计时器将每隔 5s 执行一次。
-
更灵活的计时器
// 以秒为单位的时间间隔 var interval = 5; // 重复次数 var repeat = 3; // 开始延时 var delay = 10; component.schedule(function() { // 这里的 this 指向 component this.doSomething(); }, interval, repeat, delay);
上面的计时器将在 10 秒后开始计时,每 5 秒执行一次回调,执行 3 + 1 次。
-
只执行一次的计时器(快捷方式)
component.scheduleOnce(function() { // 这里的 this 指向 component this.doSomething(); }, 2);
上面的计时器将在两秒后执行一次回调函数,之后就停止计时。
-
取消计时器
开发者可以使用回调函数本身来取消计时器:
this.count = 0; this.callback = function () { if (this.count === 5) { // 在第六次执行回调时取消这个计时器 this.unschedule(this.callback); } this.doSomething(); this.count++; } component.schedule(this.callback, 1);
注意:组件的计时器调用回调时,会将回调的 this
指定为组件本身,因此回调中可以直接使用 this
。
下面是 Component 中所有关于计时器的函数:
schedule
:开始一个计时器scheduleOnce
:开始一个只执行一次的计时器unschedule
:取消一个计时器unscheduleAllCallbacks
:取消这个组件的所有计时器
这些 API 的详细描述都可以在 Component API 文档中找到。
除此之外,如果需要每一帧都执行一个函数,请直接在 Component 中添加 update
函数,这个函数将默认被每帧调用,这在 生命周期文档 中有详细描述。
脚本执行顺序
使用统一的控制脚本来初始化其他脚本
我们先设置一个 Game.js
脚本作为总的控制脚本,还有另外的 Player.js
、Enemy.js
、Menu.js
三个脚本,那么它们的初始化过程如下:
// Game.js
const Player = require('Player');
const Enemy = require('Enemy');
const Menu = require('Menu');
cc.Class({
extends: cc.Component,
properties: {
player: Player,
enemy: Enemy,
menu: Menu
},
onLoad: function () {
this.player.init();
this.enemy.init();
this.menu.init();
}
});
其中在 Player.js
、Enemy.js
和 Menu.js
中需要实现 init
方法,并将初始化逻辑放进去。这样我们就可以保证 Player、Enemy 和 Menu 的初始化顺序。
在 Update 中用自定义方法控制更新顺序
同理如果要保证以上三个脚本的每帧更新顺序,我们也可以将分散在每个脚本里的 update
替换成自己定义的方法:
// Player.js
updatePlayer: function (dt) {
// do player update
}
然后在 Game.js
脚本的 update
里调用这些方法:
// Game.js
update: function (dt) {
this.player.updatePlayer(dt);
this.enemy.updateEnemy(dt);
this.menu.updateMenu(dt);
}
控制同一个节点上的组件执行顺序
在同一个节点上的组件脚本执行顺序,可以通过组件在 属性检查器 里的排列顺序来控制。排列在上的组件会先于排列在下的组件执行。我们可以通过组件右上角的齿轮按钮里的 Move Up
和 Move Down
菜单来调整组件的排列顺序和执行顺序。
假如我们有两个组件 CompA 和 CompB,它们的内容分别是:
// CompA.js
cc.Class({
extends: cc.Component,
onLoad: function () {
cc.log('CompA onLoad!');
},
start: function () {
cc.log('CompA start!');
},
update: function (dt) {
cc.log('CompA update!');
},
});
// CompB.js
cc.Class({
extends: cc.Component,
onLoad: function () {
cc.log('CompB onLoad!');
},
start: function () {
cc.log('CompB start!');
},
update: function (dt) {
cc.log('CompB update!');
},
});
组件顺序 CompA 在 CompB 上面时,输出:
CompA onLoad!
CompB onLoad!
CompA start!
CompB start!
CompA update!
CompB update!
在 属性检查器 里通过 CompA 组件右上角齿轮菜单里的 Move Down
将 CompA 移到 CompB 下面后,输出:
CompB onLoad!
CompA onLoad!
CompB start!
CompA start!
CompB update!
CompA update!
设置组件执行优先级
如果以上方法还是不能提供所需的控制粒度,还可以直接设置组件的 executionOrder
。executionOrder
会影响组件的生命周期回调的执行优先级。设置方法如下:
// Player.js
cc.Class({
extends: cc.Component,
editor: {
executionOrder: -1
},
onLoad: function () {
cc.log('Player onLoad!');
}
});
// Menu.js
cc.Class({
extends: cc.Component,
editor: {
executionOrder: 1
},
onLoad: function () {
cc.log('Menu onLoad!');
}
});
executionOrder
越小,该组件相对其它组件就会越先执行。executionOrder
默认为 0,因此设置为负数的话,就会在其它默认的组件之前执行。
executionOrder
只对 onLoad
、onEnable
、start
、update
和 lateUpdate
有效,对 onDisable
和 onDestroy
无效。
标准网络接口
在 Cocos Creator 中,我们支持 Web 平台上最广泛使用的标准网络接口:
- XMLHttpRequest:用于短连接
- WebSocket:用于长连接
当然,在 Web 平台,浏览器原生就支持这两个接口,之所以说 Cocos Creator 支持,是因为在发布原生版本时,用户使用这两个网络接口的代码也是可以运行的。也就是遵循 Cocos 一直秉承的 “一套代码,多平台运行” 原则。
注意:如果需要在原生平台使用
WebSocket
,请确保有在 项目 -> 项目设置 -> 模块设置 中勾选了 Native Socket 模块。
使用方法
-
XMLHttpRequest
简单示例:
let xhr = new XMLHttpRequest(); xhr.onreadystatechange = function () { if (xhr.readyState == 4 && (xhr.status >= 200 && xhr.status < 400)) { var response = xhr.responseText; console.log(response); } }; xhr.open("GET", url, true); xhr.send();
开发者可以直接使用
new XMLHttpRequest()
来创建一个连接对象。XMLHttpRequest
的标准文档请参考 MDN 中文文档。 -
WebSocket
简单示例:
let ws = new WebSocket("ws://echo.websocket.org"); ws.onopen = function (event) { console.log("Send Text WS was opened."); }; ws.onmessage = function (event) { console.log("response text msg: " + event.data); }; ws.onerror = function (event) { console.log("Send Text fired an error"); }; ws.onclose = function (event) { console.log("WebSocket instance closed."); }; setTimeout(function () { if (ws.readyState === WebSocket.OPEN) { ws.send("Hello WebSocket, I'm a text message."); } else { console.log("WebSocket instance wasn't ready..."); } }, 3);
WebSocket
的标准文档请参考文档 MDN。
SocketIO
很抱歉,Creator 并未在 Web 平台上提供 socket.io 的官方支持,需要用户自己在项目中添加,并且原生平台的 socket.io 也已废弃。之前原生平台的 socket.io 是第三方开发者自己实现的,已经很久没有维护了,所以也不推荐使用。
除此之外,SocketIO 提供一种基于 WebSocket API 的封装,可以用于 Node.js 服务端。如果需要使用这个库,开发者可以自己引用 SocketIO。
在脚本中引用 SocketIO:
-
下载 SocketIO:Socket.IO。
-
将下载后的文件放入拖入资源管理器中你希望保存的路径。
-
修改 SocketIO 脚本文件以避免在原生环境中被执行。
由于 Web 版本 SocketIO 不能够在 JSB 中被正确解析,因此 Cocos 在原生环境中自带了 SocketIO 实现。所以我们需要一点 hack 的手段让 Web 版本 SocketIO 的脚本在原生环境中不生效,方法就是在 SocketIO 脚本文件中做如下修改:
if (!cc.sys.isNative) { // SocketIO 原始代码 }
-
将 SocketIO 脚本文件设为 插件脚本,这样在组件中直接使用 window.io 就能访问到 SocketIO。
-
在组件中使用 SocketIO,可以参考 SocketIO 官方网站 查询 API 和文档等。
注意:如果你需要在原生使用
WebSocket
或SocketIO
请确保你勾选了Native Socket
模块:
使用对象池
在运行时进行节点的创建(cc.instantiate
)和销毁(node.destroy
)操作是非常耗费性能的,因此我们在比较复杂的场景中,通常只有在场景初始化逻辑(onLoad
)中才会进行节点的创建,在切换场景时才会进行节点的销毁。如果制作有大量敌人或子弹需要反复生成和被消灭的动作类游戏,我们要如何在游戏进行过程中随时创建和销毁节点呢?这里就需要对象池的帮助了。
对象池的概念
对象池就是一组可回收的节点对象,我们通过创建 cc.NodePool
的实例来初始化一种节点的对象池。通常当我们有多个 prefab 需要实例化时,应该为每个 prefab 创建一个 cc.NodePool
实例。当我们需要创建节点时,向对象池申请一个节点,如果对象池里有空闲的可用节点,就会把节点返回给用户,用户通过 node.addChild
将这个新节点加入到场景节点树中。
当我们需要销毁节点时,调用对象池实例的 put(node)
方法,传入需要销毁的节点实例,对象池会自动完成把节点从场景节点树中移除的操作,然后返回给对象池。这样就实现了少数节点的循环利用。假如玩家在一关中要杀死 100 个敌人,但同时出现的敌人不超过 5 个,那我们就只需要生成 5 个节点大小的对象池,然后循环使用就可以了。
关于 cc.NodePool
的详细 API 说明,请参考 cc.NodePool API 文档。
流程介绍
下面是使用对象池的一般工作流程
准备好 Prefab
把你想要创建的节点事先设置好并做成 Prefab 资源,方法请查看 预制资源工作流程。
初始化对象池
在场景加载的初始化脚本中,我们可以将需要数量的节点创建出来,并放进对象池:
//...
properties: {
enemyPrefab: cc.Prefab
},
onLoad: function () {
this.enemyPool = new cc.NodePool();
let initCount = 5;
for (let i = 0; i < initCount; ++i) {
let enemy = cc.instantiate(this.enemyPrefab); // 创建节点
this.enemyPool.put(enemy); // 通过 put 接口放入对象池
}
}
对象池里需要的初始节点数量可以根据游戏的需要来控制,即使我们对初始节点数量的预估不准确也不要紧,后面我们会进行处理。
从对象池请求对象
接下来在我们的运行时代码中就可以用下面的方式来获得对象池中储存的对象了:
// ...
createEnemy: function (parentNode) {
let enemy = null;
if (this.enemyPool.size() > 0) { // 通过 size 接口判断对象池中是否有空闲的对象
enemy = this.enemyPool.get();
} else { // 如果没有空闲对象,也就是对象池中备用对象不够时,我们就用 cc.instantiate 重新创建
enemy = cc.instantiate(this.enemyPrefab);
}
enemy.parent = parentNode; // 将生成的敌人加入节点树
enemy.getComponent('Enemy').init(); //接下来就可以调用 enemy 身上的脚本进行初始化
}
安全使用对象池的要点就是在 get
获取对象之前,永远都要先用 size
来判断是否有可用的对象,如果没有就使用正常创建节点的方法,虽然会消耗一些运行时性能,但总比游戏崩溃要好!另一个选择是直接调用 get
,如果对象池里没有可用的节点,会返回 null
,在这一步进行判断也可以。
将对象返回对象池
当我们杀死敌人时,需要将敌人节点退还给对象池,以备之后继续循环利用,我们用这样的方法:
// ...
onEnemyKilled: function (enemy) {
// enemy 应该是一个 cc.Node
this.enemyPool.put(enemy); // 和初始化时的方法一样,将节点放进对象池,这个方法会同时调用节点的 removeFromParent
}
返还节点时,对象池内部会调用结点的 removeFromParent(false)
方法,将对象从父节点中移除,但并不会执行 cleanup
操作。这样我们就完成了一个完整的循环,主角需要刷多少怪都不成问题了!将节点放入和从对象池取出的操作不会带来额外的内存管理开销,因此只要是可能,应该尽量去利用。
使用组件来处理回收和复用的事件
使用构造函数创建对象池时,可以指定一个组件类型或名称,作为挂载在节点上用于处理节点回收和复用事件的组件。假如我们有一组可点击的菜单项需要做成对象池,每个菜单项上有一个 MenuItem.js
组件:
// MenuItem.js
cc.Class({
extends: cc.Component,
onLoad: function () {
this.node.selected = false;
this.node.on(cc.Node.EventType.TOUCH_END, this.onSelect, this.node);
},
unuse: function () {
this.node.off(cc.Node.EventType.TOUCH_END, this.onSelect, this.node);
},
reuse: function () {
this.node.on(cc.Node.EventType.TOUCH_END, this.onSelect, this.node);
}
});
在创建对象池时可以用:
let menuItemPool = new cc.NodePool('MenuItem');
这样当使用 menuItemPool.get()
获取节点后,就会调用 MenuItem
里的 reuse
方法,完成点击事件的注册。当使用 menuItemPool.put(menuItemNode)
回收节点后,会调用 MenuItem
里的 unuse
方法,完成点击事件的反注册。
另外 cc.NodePool.get()
可以传入任意数量类型的参数,这些参数会被原样传递给 reuse
方法:
// BulletManager.js
let myBulletPool = new cc.NodePool('Bullet'); //创建子弹对象池
...
let newBullet = myBulletPool.get(this); // 传入 manager 的实例,用于之后在子弹脚本中回收子弹
// Bullet.js
reuse (bulletManager) {
this.bulletManager = bulletManager; // get 中传入的管理类实例
}
hit () {
// ...
this.bulletManager.put(this.node); // 通过之前传入的管理类实例回收子弹
}
清除对象池
如果对象池中的节点不再被需要,我们可以手动清空对象池,销毁其中缓存的所有节点:
myPool.clear(); // 调用这个方法就可以清空对象池
当对象池实例不再被任何地方引用时,引擎的垃圾回收系统会自动对对象池中的节点进行销毁和回收。但这个过程的时间点不可控,另外如果其中的节点有被其他地方所引用,也可能会导致内存泄露,所以最好在切换场景或其他不再需要对象池的时候手动调用 clear
方法来清空缓存节点。
使用 cc.NodePool 的优势
cc.NodePool
除了可以创建多个对象池实例,同一个 prefab 也可以创建多个对象池,每个对象池中用不同参数进行初始化,大大增强了灵活性;此外 cc.NodePool
针对节点事件注册系统进行了优化,用户可以根据自己的需要自由的在节点回收和复用的生命周期里进行事件的注册和反注册。
而之前的 cc.pool
接口是一个单例,无法正确处理节点回收和复用时的事件注册。不再推荐使用。
对象池的基本功能其实非常简单,就是使用数组来保存已经创建的节点实例列表。如果有其他更复杂的需求,你也可以参考 暗黑斩 Demo 中的 PoolMng 脚本(GitHub | Gitee)来实现自己的对象池。
使用 cc.NodePool 的注意事项
当获取和返还节点时,cc.NodePool
内部会不断地对节点执行 removeFromParent
和 addChild
操作,当大批量、频繁地操作对象池时(比如制作射击游戏弹幕),可能在低端机器上仍然会引起卡顿。
除了性能问题,不断地执行 removeFromParent
和 addChild
也会导致节点的默认渲染顺序发生变化。如有必要避免,可以调用 setSiblingIndex
修改节点的索引。
存储和读取用户数据
我们在游戏中通常需要存储用户数据,如音乐开关、显示语言等,如果是单机游戏还需要存储玩家存档。Cocos Creator 中我们使用 cc.sys.localStorage
接口来进行用户数据存储和读取的操作。
cc.sys.localStorage
接口是按照 Web Storage API 来实现的,在 Web 平台运行时会直接调用 Web Storage API,在原生平台上会调用 sqlite 的方法来存储数据。一般用户不需要关心内部的实现。
配合本篇文档可以参考 数据存储范例(GitHub | Gitee)。
存储数据
cc.sys.localStorage.setItem(key, value)
上面的方法需要两个参数,用来索引的字符串键值 key
,和要保存的字符串数据 value
。
假如我们要保存玩家持有的金钱数,假设键值为 gold
:
cc.sys.localStorage.setItem('gold', 100);
对于复杂的对象数据,我们可以通过将对象序列化为 JSON 后保存:
userData = {
name: 'Tracer',
level: 1,
gold: 100
};
cc.sys.localStorage.setItem('userData', JSON.stringify(userData));
读取数据
cc.sys.localStorage.getItem(key)
和 setItem
相对应,getItem
方法只要一个键值参数就可以取出我们之前保存的值了。对于上文中储存的用户数据:
var userData = JSON.parse(cc.sys.localStorage.getItem('userData'));
移除键值对
当我们不再需要一个存储条目时,可以通过下面的接口将其移除:
cc.sys.localStorage.removeItem(key)
清空数据
当我们不再需要已存储的用户数据时,可以通过下面的接口将其清空:
cc.sys.localStorage.clear()
数据加密
对于单机游戏来说,对玩家存档进行加密可以延缓游戏被破解的时间。要加密存储数据,只要在将数据通过 JSON.stringify
转化为字符串后调用你选中的加密算法进行处理,再将加密结果传入 setItem
接口即可。
您可以搜索并选择一个适用的加密算法和第三方库,比如 encryptjs,将下载好的库文件放入你的项目。
存储时:
var encrypt=require('encryptjs');
var secretkey= 'open_sesame'; // 加密密钥
var dataString = JSON.stringify(userData);
var encrypted = encrypt.encrypt(dataString,secretkey,256);
cc.sys.localStorage.setItem('userData', encrypted);
读取时:
var cipherText = cc.sys.localStorage.getItem('userData');
var userData=JSON.parse(encrypt.decrypt(cipherText,secretkey,256));
注意:数据加密不能保证对用户档案的完全掌控,如果您需要确保游戏存档不被破解,请使用服务器进行数据存取。