前端er开发cocos小游戏快速入门

前段时间一直在更 vue2的源码系列,最近换了换口味,学了一下 cocos ,照猫画虎的写了一个「挑战1024」小游戏。

学习一门新语言或者新框架其实就是一个堆时间的过程了,整个过程就是结合已有经验进行不同的猜测,然后验证,搞不定就去官网或者搜索引擎找答案,99.9% 的问题应该都能找到。

cocos 网上很多是视频教程,虽然对新手友好,但是信息密度太低了,这里我总结一下 cocos 专有的或者不太符合直觉的一些点,前端的同学看完以后能更快的进入 cocos 的开发中。

建议先跟着官方的 快速上手 先一步一步实现一个小游戏,再读下边的文章效果会更佳。

介绍

cocos 提供了游戏引擎,一些常用的操作,碰撞检测、重力模拟、变换位置、旋转、缩放、粒子系统等都可以通过配置一键实现,游戏引擎最终会帮我们把界面渲染到 canvas 节点上。

image-20221113101837574

因为是渲染至 canvas ,当然很自然的可以支持跨端,一套代码可以编译至 h5、微信小游戏等平台。

同一个功能不同平台之间有不同的 api ,比如 localstorage 的使用会有所不同,cocos 会帮我们在上层抹平,只需要按照 cocos 的语法编写,编译的时候选择相应的平台就会转成对应平台的 api

编辑器

cocos 开发和平常的前端开发不太一样,它是代码结合 UI 拖拽来实现的,通过拖拽我们可以快速的布局、添加组件、设置属性等。

基于此,项目和编辑器就有了强绑定的关系,如果下载别人的项目,还需要下载相应的编辑器。

image-20221113104527992

打开项目的时候需要选择相应的编辑器。

image-20221113104543991

当然,如果编辑器差的版本比较小,Cocos 也可以帮我们自动升级项目的编辑器版本。如果是 2.x 升到 3.x 就会有 break changes ,需要手动进行一些代码的兼容。

psMAC M1 版本不支持 2.4.5 以下的版本。

场景/Scene

游戏的 ui 、逻辑都挂载在某个场景(Scene)下,可以在资源管理器右键创建场景,然后双击打开。

image-20221113110410892

接下来我们就可以在当前 Canvas 添加各种节点和代码逻辑了。

游戏如果有多个页面,可以新建多个场景各自维护。

ps:如果从导入网上下载的 cocos 项目,场景不会自动加载,需要双击一下场景然后再预览。

节点/Node

我们可以通过右键创建节点,除了空结点,还帮我们预设了其他的很多节点,比如 LabelButton 等。

image-20221113112755384

节点是树状关系,每个节点可以得到它的父节点,也可以得到它的子节点。

比如我们可以通过 getChildByName 得到它的子节点。

1
this.node.getChildByName("message"); // 得到相应的 Node 节点

通过 this.node.parent 拿到它的父节点。

组件

一个空结点只有一些位置、大小属性。

image-20221113153540396

我们可以在 Node 节点上挂载一些组件让 Node 拥有样式和功能。

Label 组件

如果我们创建一个 Label 节点,会自动挂一个 Label 组件。

image-20221113153748303

通过 Label 组件我们可以设置文案 、字体大小等,展示到场景中的就是一个普通的 Label

image-20221113153900887

图片组件

我们可以通过将「资源管理器」中的图片拖动到「层级管理器」中生成一个带背景的 Node 节点。

image-20221113154522620

拖过去之后会生成一个带有 Sprite 组件的节点,并将该图片设置为 Sprite Frame 属性的值,这样这张图片就会展示到场景中了。

image-20221113154930782

如果想要更改图片,只要把 Sprite Frame 属性清空,重新拖一个图片上去即可。

脚本组件

这个是最重要的,我们可以编写游戏逻辑,设置一些点击监听、节点之间联动等逻辑,然后挂到 Node 节点上。

先新建一个 js 文件,会自动帮我们生成带有生命周期的一些代码。

image-20221113160458554

双击打开新建的 js 文件,我们可以把文件和 VSCode 关联,用 VSCode 进行代码的编辑。

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
28
29
30
31
32
33
34
35
36
37
38
// Learn cc.Class:
// - https://docs.cocos.com/creator/manual/en/scripting/class.html
// Learn Attribute:
// - https://docs.cocos.com/creator/manual/en/scripting/reference/attributes.html
// Learn life-cycle callbacks:
// - https://docs.cocos.com/creator/manual/en/scripting/life-cycle-callbacks.html

cc.Class({
extends: cc.Component,

properties: {
// 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) {},
});

properties 是脚本组件的属性,写在这里的属性可以在 Cocos 的界面上看到。

比较重要是 OnLoadupdate 两个生命周期,OnLoad 会在组件渲染前进行执行,这里我们可以进行一些初始化的操作,update 生命周期会在每一帧渲染前执行,这里我们就可以更新节点的位置让一些节点动起来。

文件编写好以后,我们可以以组件的形式逻辑挂载到相应的 Node 节点上。

image-20221113161547263

Widget 组件

这个比较简单,它可以设置和边界的相对距离。

image-20221113173512981

碰撞组件

两个 Node 节点相撞,我们可以根据它们的坐标手动进行判断,也可以在 Node 节点上挂载碰撞组件,设置它们的分组,然后在脚本组件中增加 onCollisionEnter 回调函数即可。

添加 BoxCollider 组件。

image-20221113162101123

设置 Node 中的 Group 属性。

image-20221113162143563

Group 我们可以手动进行管理,并且设置哪些 Group 产生碰撞。

image-20221113162918517

接下来还需要在游戏最开始的时候开始碰撞检测,可以给层级节点中的 Canvas 节点添加一个用户脚本组件 game.js ,然后修改脚本组件的 OnLoad 中调用下边的方法。

1
2
const collisionManager = cc.director.getCollisionManager();
collisionManager.enabled = true;

最后在相应的 Node 节点的用户脚本中添加 onCollisionEnter 回调函数进行碰撞后的逻辑即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
onCollisionEnter(other) {
if (this.game.continueIng && other.node.name !== "ground") {
return;
}
switch (other.node.name) {
case "star":
this.handleStar(other);
break;
case "meteorites":
case "ground":
this.game.endGame();
break;
}
},

通过回调参数 other 可以拿到碰撞的节点。

刚体组件

这里通过刚体组件我们可以实现物体受到重力的效果。

首先给节点添加一个 RightBody 组件,并且将类型设置为 Dynamic

image-20221113165834371

和碰撞组件一样,我们在 Canvas 对应的用户脚本组件的 OnLoad 中调用下边的方法开启重力模拟即可。

1
2
3
const instance = cc.director.getPhysicsManager();
instance.enabled = true;
instance.gravity = cc.v2(0, -980 * 2);

这样相应的节点就会受到重力的作用了。

动画组件

在层级管理器选中相应的节点,点击「动画编辑器」,然后添加一个 Animation 组件

image-20221113165636129

接着添加一个 Clip,并进行编辑,设置动画的关键帧等,有点像 photoShop 里的动画编辑器。

保存后将新建的 Clip 拖到到对应的属性上即可。

image-20221113170821916

防穿透组件

button 被弹窗盖住,此时 button 依旧会响应到点击时间,此时可以通过给弹窗增加 BlockInputEvents 防止点击穿透。

image-20221119205327720

需要点击的时候激活,关闭的时候取消激活。

1
2
3
this.node.getComponent(cc.BlockInputEvents).enabled = true; // 点击的时候激活

this.node.getComponent(cc.BlockInputEvents).enabled = false; // 关闭的时候取消激活

坐标系

设置的 positon 是在父节点坐标系下的位置。

如果它有子节点,它的子节点设置的 positon 就是基于上边的红线和绿线为坐标轴进行排布。

脚本组件属性

我们可以脚本组件中添加一些属性。

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
properties: {
bird: {
default: null,
type: Bird,
},
gravity: cc.v2(0, -980 * 2),
starPool: {
default: null,
type: StarPool,
},
scoreDisplay: {
default: null,
type: cc.Label,
},
scoreResult: {
default: null,
type: cc.Node,
},
successAudio: {
default: null,
type: cc.AudioClip,
},
failAudio: {
default: null,
type: cc.AudioClip,
},
},

这样在 cocos 编辑器中我们可以通过拖动进行属性的初始化。

image-20221113172140746

值的注意的是,如果我们在 properties 外边写属性,比如下边的 num

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
28
29
30
31
32
33
34
35
cc.Class({
extends: cc.Component,

properties: {
// 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;
// }
// },
},

num: 0, // 自定义属性

// LIFE-CYCLE CALLBACKS:

onLoad () {
console.log(this.num)
},

start () {

},

// update (dt) {},
});

此时我们在 onLoad 打印该值只会是 undefind ,如果想在当前实例上挂载属性,我们可以选择在 onLoad 中进行值的初始化。

1
2
3
onLoad () {
this.num = 2; // 自定义属性
},

这里需要注意的是如果改变脚本代码,保存后我们需要重新切到 Cocos 的编辑页面 才会重新进行编译。

Prelab

节点可以在编辑器生成,当然也可以通过代码动态生成。对于需要重复生成的节点,我们可以将它保存成 Prefab(预制)资源,作为我们动态生成节点时使用的模板。

做法就是在「层级管理器」随便新建一个 Node 节点,并且添加所需要的组件和自定义的脚本组件,最后将该 node 节点拖动到「资源管理器」即可。

image-20221114064750298

之后我们就可以层级管理器中刚新建的节点删除。当然,为了后续方便编辑,该 Node 节点也可以保留,但需要将其放到画面外,并且将脚本组件取消勾选一下,不执行逻辑。

image-20221114065217375

有了预制资源后,我们可以通过下边的代码来动态生成 Node

1
2
3
let newStar = cc.instantiate(this.starPrefab); // 根据预置资源生成 node 节点
newStar.getComponent("Star").init(this, this.game); // 根据 node 节点的脚本组件进行初始化
this.node.addChild(newStar); // 加到当前 node 节点的下面

对于画面中移动的 node ,当移出画面后我们可以进行重复利用,这里可以引入 NodePool ,出画面后加入节点池,需要的时候再从里边拿。

1
2
3
4
5
6
7
this.pool = new cc.NodePool();

// 放入节点池
this.pool.put(star);

// 需要的时候从里边拿
const newStar = this.pool.get();

通过节点池我们可以节省内存的开销。

Node 和 组件

Node 和组件的关系最开始的时候有点懵逼,慢慢的调试后大致了解了,下边讲一下我的理解。

在定义 properties 的时候我们需要定义对象的属性,它可以是 type: cc.Node, ,也可以是自带的组件类型 type: cc.Label ,也可以是我们定义的脚本组件类型,可以先将编写的脚本代码引入 const Bird = require("Bird"); ,然后将其作为一种类型 type: Bird

一个节点属于复合类型,它既是本身的 cc.Node 类型,如果添加了相应的组件,它也是相应的组件类型。

以下图为例,它既是 cc.Node 类型,也是 cc.Label 类型,还是 test 类型。

image-20221113174535576

下边以动态修改 Label 的值,讲一下 Node 和组件之间的关系。

首先新建一个 canvas 的脚本组件 game.js ,将该组件挂载到 canvas 节点中。

image-20221113182122357

game.js 中添加一个 label 属性,类型为 cc.Node

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cc.Class({
extends: cc.Component,
properties: {
label: {
default: null,
type: cc.Node,
},
},

onLoad() {
},

start() {},

// update (dt) {},
});

选中 Canvas 节点,将 FirstLabel 节点拖动添加的属性中。

image-20221113174930670

image-20221113175131343

虽然 firstLabel 属于三种类型,但因为我们定义的类型是 cc.Node ,因此拿到的是一个 Node 对象,我们打印看一下。

1
2
3
onLoad() {
console.log(this.label);
},

image-20221113175207934

如果想要在运行的时候改变当前节点的位置,调用 setPositon 即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cc.Class({
extends: cc.Component,

properties: {
label: {
default: null,
type: cc.Node,
},
},

onLoad() {
console.log(this.label);
this.label.setPosition(cc.v2(0, -200));
},

start() {},

// update (dt) {},
});

如果我们想改变组件的文案,我们需要先通过 getComponent 拿到 Label 组件的实例对象,然后更新 string 属性即可。

1
2
3
4
5
onLoad() {
console.log(this.label);
this.label.setPosition(cc.v2(0, -200));
this.label.getComponent(cc.Label).string = "我改变了";
},

初值设置的是 设置文案

image-20221113175852560

运行起来会发现是我们在 onLoad 中设置的值。

image-20221113180058709

当然,我们也可以在开始的时候将组件类型设置为 cc.Label ,这样我们开始拿到的就是 Label 实例对象,就不需要再通过 getComponent 方法了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cc.Class({
extends: cc.Component,

properties: {
label: {
default: null,
type: cc.Label,
},
},

onLoad() {
console.log(this.label);
this.label.string = "我改变了";
},

start() {},

// update (dt) {},
});

改为代码后我们重新拖动,更新下属性的值。

image-20221113180445655

那么如果我们想改变 node 位置该怎么办呢?

获得的组件实例中有一个 node 属性,我们可以直接拿到当前的 node 对象实例,然后继续调用 setPosition 就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cc.Class({
extends: cc.Component,

properties: {
label: {
default: null,
type: cc.Label,
},
},

onLoad() {
console.log(this.label);
this.label.string = "我改变了";
this.label.node.setPosition(cc.v2(0, -300));
},

start() {},

// update (dt) {},
});

为了更深刻的理解,我们再绕一下,实现通过当前节点的 Node ,调用自定义脚本组件的方法,来动态修改 Label 的值。

首先编写自定义组件的代码,提供一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// test.js
cc.Class({
extends: cc.Component,

properties: {},

// LIFE-CYCLE CALLBACKS:

// onLoad () {},

start() {},

setLabelValue() {
this.getComponent(cc.Label).string = "我被 test 改变了";
},

// update (dt) {},
});

当前脚本添加到相应的属性中。

image-20221113181717593

接着我们只需要在 canvas 的脚本组件中调用 getComponent("test") 拿到上边的脚本对象实例,调用 setLabelValue 方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cc.Class({
extends: cc.Component,

properties: {
label: {
default: null,
type: cc.Node,
},
},

onLoad() {
console.log(this.label);
this.label.getComponent("test").setLabelValue();
},

start() {},

// update (dt) {},
});

小结一下,使用对象的时候,我们需要明确当前是 cc.Node 类型,还是某种组件类型,每一个种类型都有自己的方法。

如果想从 cc.Node 对象中拿到相应的组件,调用 getComponent 方法即可。

如果想从组件中拿到 cc.Node 类型,不管是自带的组件,还是自定义的脚本组件,可以直接通过 this.node 拿到当前的 node 实例对象。

显示隐藏

最直接就是设置 node 对象的 active 属性即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cc.Class({
extends: cc.Component,

properties: {
label: {
default: null,
type: cc.Node,
},
},

onLoad() {
console.log(this.label);
this.label.active = false;
},

start() {},

// update (dt) {},
});

上边的方式类似于 vuev-if ,会直接把节点销毁掉。

如果想保留节点,实现 vuev-show ,我们可以设置 opacity 透明度属性弯道实现,只需要将值设置为 0 实现隐藏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
cc.Class({
extends: cc.Component,

properties: {
label: {
default: null,
type: cc.Node,
},
},

onLoad() {
console.log(this.label);
this.label.opacity = 0;
},

start() {},

// update (dt) {},
});

这里需要注意的是,虽然通过透明度可以隐藏组件,但是此时的点击事件还是存在的,需要处理一下。

编译

编译的时候我们选择微信小游戏,填写 appId ,编译完成后通过微信开发者工具导入 build 出来的文件就可以了。

菜单 ->项目 -> 构建发布:

image-20221113183809044

我们可以设置初始场景、设备方向等。

需要注意的是,微信主包有 2M 大小的限制,如果预览的微信小游戏遇到超包的情况,我们可以将没用到的组件在编译设置中去除。

菜单 -> 项目 -> 项目设置 -> 模块设置:

image-20221113184029466

微信小游戏排行榜

微信为了防止好友的关系链泄露,提出了一个子域的概念,在子域中可以调用 wx.getFriendCloudStorage 方法拿到好友数据。

为了实现排行榜,我们需要再创建一个空项目,实现排行榜的显示逻辑,和正常项目开发是一致的。

添加 message 回调函数,供主项目调用。

1
2
3
4
5
6
7
8
9
10
private onMessage(msg: any) {
switch (msg.event) {
case "setScore":
this.setScore(msg.score);
break;
case "showRank":
this.getRank();
break;
}
}

编译的时候需要选择 微信小游戏开发数据域,名称自己定义,我写的是wxSubContext,路径选择之前项目编译的文件夹。

image-20221113184927037

然后在主项目中我们需要添加一个空节点,并且添加一个 SubContextView 组件,将这个节点作为排行榜项目的容器节点。

image-20221113184416196

如果想要调用排行榜的方法通过 postMessage

1
2
3
wx.postMessage({
event: "showRank",
});

编译的时候指定一下排行榜项目之前设置的名称 wxSubContext

image-20221113184628466

上边就是实现排行榜的整个逻辑了,详细的可以参考 这篇文章,相应的 代码仓库clone 下来可以直接用。这个项目的 cocos 编辑器是 2.3.3 ,如果升级到 2.4.5 会出现无法滚动的情况,谨慎升级。

需要注意的一点是,当子项目的容器节点显示的时候,子项目才开始初始化,这就会导致主项目 postMessage 先调用,排行榜项目的onMessage 后调用,导致错失了消息。

解决这个问题的话,我们的显藏可以通过设置透明度的方式实现,让子项目提前加载。

发布到微信

个人开发者提交的资料基本不用啥,有个自审资料网上找个模版在 word 填完截图就可以。

自查

但提交审核的道路比较坎坷,除了慢以外,甚至被拒了两次。

第一次周日提交,周三还没有结果在微信社区平台催了一下审核,下午收到结果审核失败。

小游戏涉嫌侵权,请参考示例截图标记点全面自查游戏内容,请于下个版本有效整改或举证,在微信公众平台-版本管理-提交审核-授权书/版本更新说明提交,包括但不限于游戏内容说明及对应截图、原创证明或有效授权书 主体信用分扣除-3分

原因是一开始是准备仿 Flappy bird 的,直接用了相应的素材,就被驳回了,客服截图如下:

image-20221119103132834

第二次周三早上提交,周五晚上收到结果,又被拒了,这次就无法理解了:

开发者你好,经平台审核,你的小游戏《挑战1024》未通过审核,具体原因如下:

1、小游戏需具有完整的游戏玩法,不能为简单的素材堆砌

网上搜了搜,可能是因为我的游戏只有一个界面,点击就开始了。据说加个菜单就会好,于是又改了改,不同场景也换了换背景。

第三次周六晚上提交,周二晚上收到结果,同上次,审核被拒,原因为「小游戏需具有完整的游戏玩法,不能为简单的素材堆砌」。

已经不知道该怎么改了,周三早上点了审核失败那里的提交反馈,写了一段感人肺腑的话(* 的内容这里就省略了)。

本游戏为益智类游戏,需要分数吃到 1024 才能获得胜利。
游戏场景分为菜单、第一关、最终关、好友排行,不同关卡也会通过背景色来区分。
菜单提供了分享好友、查看排行的功能。
第一关主要是为了体验游戏流程,星星的分数都是×2,因此只需要不停的吃分即可取得胜利。
最终关需要通过自己的策略,除了躲避陨石,还需要吃到星星上不同的分数,才能获得胜利。
游戏过程中,星星的速度、分数的出现会实时通过当前的状态进行变化,主要涉及到一些算法,也是本游戏的核心。
虽然素材都是星星,但结合算法上边的分数会一直变化,同时星星和陨石的比例也在不断变化。
除此之外玩家还需要躲避陨石,同时设定了策略,如果******。
在用户挑战失败的时候,增设了复活功能、重开功能。
游戏名为「挑战1024」,属于*******,来最终取得胜利。
希望审核大大可以再看一下,设计整个流程和算法确实花了很多心思。

周四早上显示反馈成功。

开发者你好,感谢你向小游戏审核团队反馈异议,经平台评估:我们已更正你的历史审核记录,如有需求,可重新提交审核

周五早上进行了重新提审,周二下午终于通过了。

image-20221130083045661

小游戏相比于小程序审核严格好多,前前后后花了有半个多月了,简单游戏竟然不让上线,这是我想不通的。

整体就是这样了,整个 cocos 项目可以理解为一棵树,整个树就是一个场景,根节点是一个包含 Canvas 组件的 node ,接下来可以创建自己的 node ,每个 node 又可以挂载想要的自带组件和用户脚本组件。

希望对大家有帮助,如果错误也欢迎指出,也可以体验一下我这次开发的小游戏,哈哈:

gh_dc7db84e6a20_258

windliang wechat