本文基于Cocos Creator开发第一个小游戏"摘星星",这款游戏的玩家要操作一个反应迟钝却蹦跳不停的小怪物来碰触不断出现的星星,难以驾驭的加速度将给玩家带来很大挑战。
一.准备项目和资源
1.初始项目
首先下载初始项目,然后一步一步完成整个项目。
2.最终完成项目
在开发的过程中如果遇到问题,可以参考最终完成项目的源代码。
二.打开初始项目
首先启动Cocos Creator,然后打开项目start_project,Cocos Creator编辑器主窗口会打开,如下所示:
三.检查游戏资源
初始项目中已经包含了所有需要的游戏资源,因此不需要再导入任何其他资源。资源管理器可以显示任意层次的目录结构,如下所示:
每个资源都是一个文件,导入项目后根据扩展名的不同而被识别为不同的资源类型,其图标也会有所区别,下面来看看项目中的资源各自的类型和作用:
四.创建游戏场景
在Cocos Creator中,游戏场景[Scene]是开发时组织游戏内容的中心,也是呈现给玩家所有游戏内容的载体。游戏场景中一般会包括以下内容:
当玩家运行游戏时,就会载入游戏场景,游戏场景加载后就会自动运行所包含组件的游戏脚本,实现各种各样开发者设置的逻辑功能。接下来新建一个场景:
1.在资源管理器中点击选中assets目录
2.点击资源管理器左上角的加号按钮,在弹出的菜单中选择Scene
3.将创建的场景文件命名为game
4.双击game,就会在场景编辑器和层级管理器中打开这个场景
五.了解Canvas
打开场景后,层级管理器中会显示当前场景中的所有节点和它们的层级关系。刚刚新建的场景中有一个名叫Canvas的节点[还有一个Main Camera节点],Canvas可以被称为画布节点或渲染根节点,点击选中Canvas,可以在属性检查器中看到它的属性。
这里的Design Resolution属性规定了游戏的设计分辨率,Fit Height和Fit Width规定了在不同尺寸的屏幕上运行时,将如何缩放Canvas以适配不同的分辨率。
1.Design Resolution:设计分辨率[内容生产者在制作场景时使用的分辨率蓝本]
2.Fit Height:适配高度[设计分辨率的高度自动撑满屏幕高度]
3.Fit Width:适配宽度[设计分辨率的宽度自动撑满屏幕宽度]
六.设置场景图像
1.添加背景
首先在资源管理器里按照assets/textures/background
的路径找到背景图像资源,点击并拖拽这个资源到层级管理器中的Canvas节点上。
2.修改背景尺寸
接下来修改背景图像的尺寸,来让它覆盖整个屏幕。首先选中background节点,然后点击主窗口左上角工具栏第四个矩形变换工具:将鼠标移动到场景编辑器中background的左边,按住并向左拖拽直到background的左边超出表示设计分辨率的紫色线框,然后再用同样的方法将background的右边向右拖拽。之后需要拖拽上下两边,使背景图的大小能够填满设计分辨率的线框。
在使用矩形变换工具修改背景图尺寸时,在属性检查器中可以看到Node[节点]中的Size属性也在随之变化。也可以直接在Size属性的输入框中输入数值,和使用矩形变换工具可以达到同样的效果。这样大小的背景图在市面流行的手机上都可以覆盖整个屏幕,不会出现穿帮情况。
3.添加地面
主角需要一个可以在上面跳跃的地面,用同样的方式拖拽资源管理器中assets/textures/ground
资源到层级管理器的Canvas上。按照修改background节点的方法,我们也可以使用矩形变换工具来为ground节点设置一个合适的大小。在使用矩形变换工具的时候,通过拖拽节点顶点和四边之外的部分,就可以更改节点的位置。如下所示:
4.添加主角
从资源管理器拖拽assets/texture/PurpleMonster
到层级管理器中Canvas的下面,确保它的排序在ground之下,并将其重命名为Player。改变锚点[Anchor]的位置,在属性检查器里找到Anchor属性,把其中的y值设为0,可以看到场景编辑器中,表示主角位置的移动工具的箭头出现在了主角脚下。
在场景编辑器中拖拽Player,把它放在地面上,效果图如下所示:
七.编写主角脚本
Cocos Creator开发游戏的一个核心理念就是让内容生产和功能开发可以流畅的并行协作,上面部分着重于处理美术内容,接下来通过编写脚本来开发功能的流程,并且写好的程序脚本可以很容易的被内容生产者使用。
1.创建脚本
通过资源管理器在assets下面新建文件夹scripts,并且创建JavaScript脚本文件Player,双击这个脚本打开代码编辑器,自己用的是PyCharm。需要说明的是Cocos Creator中脚本名称就是组件的名称,并且命名是大小写敏感的。
2.编写组件属性
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) {},
});
[1]Class()方法的参数是一个原型对象,在原型对象中以键值对的形式设定所需的类型参数,就能创建出所需要的类。
[2]组件[Component]能够挂载到场景中的节点上,提供控制节点的各种功能。
[3]修改脚本Player中的properties部分
// Player.js
//...
properties: {
// 主角跳跃高度
jumpHeight: 0,
// 主角跳跃持续时间
jumpDuration: 0,
// 最大移动速度
maxMoveSpeed: 0,
// 加速度
accel: 0,
},
//...
特别说明:不需要关心这些数值是多少,因为之后会直接在属性检查器中设置这些数值。
[4]把Player组件添加到主角节点上
在层级管理器中选中Player节点,然后在属性检查器中点击添加组件按钮,选择添加用户脚本组件->Player,为主角节点添加Player组件。
现在可以在属性检查器中[需要选中Player节点]看到刚添加的Player组件了,按照下图将主角跳跃和移动的相关属性设置好:
这些数值除了jumpDuration的单位是秒之外,其它的数值都是以像素为单位的,根据现在对Player组件的设置:主角将能够跳跃200像素的高度,起跳到最高点所需的时间是0.3秒,最大水平方向移动速度是400像素每秒,水平加速度是350像素每秒。
3.编写跳跃和移动代码
添加一个方法来让主角跳跃起来,在properties:{…}代码块的下面,添加叫做setJumpAction的方法:
setJumpAction: function() {
//跳跃上升
var jumpUp = cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight)).easing(cc.easeCubicActionOut());
//下落
var jumpDown = cc.moveBy(this.jumpDuration, cc.v2(0, -this.jumpHeight)).easing(cc.easeCubicActionIn());
//不断重复
return cc.repeatForever(cc.sequence(jumpUp, jumpDown));
}
[1]moveBy()方法:在规定的时间内移动指定的一段距离,第一个参数就是我们之前定义主角属性中的跳跃时间,第二个参数是一个Vec2[表示2D向量和坐标]类型的对象。
[2]cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight)):第二个参数传入的是使用cc.v2方法构建的Vec2类型对象,这个类型表示的是一个坐标,即有X坐标也有Y坐标。X、Y坐标是相对于节点当前的坐标位置,而不是整个坐标系的绝对坐标。
[3]moveBy()方法的返回值:它的返回值是一个ActionInterval类型的对象,ActionInterval在Cocos中表示一个时间间隔动作的类,这种动作在一定时间内完成。
[4]easing(cc.easeCubicActionOut()):该方法可以让时间间隔动作呈现为一种缓动运动,传入的参数是一个缓动对象,返回一个ActionInterval类型对象。
在onLoad方法里调用刚添加的setJumpAction方法,然后执行runAction来开始动作:
onLoad: function() {
this.jumpAction = this.setJumpAction();
this.node.runAction(this.jumpAction);
}
onLoad方法会在场景加载后立刻执行,所以会把初始化相关的操作和逻辑都放在这里面。首先将循环跳跃的动作传给了jumpAction变量,之后调用这个组件挂载的节点下的runAction方法,传入循环跳跃的Action从而让节点[主角]一直跳跃。
4.移动控制
接下来为主角添加键盘输入,用A和D来控制它的跳跃方向。在setJumpAction方法的下面添加键盘事件响应函数:
onKeyDown (event) {
switch(event.keyCode) {
case cc.macro.KEY.a:
this.accLeft = true;
break;
case cc.macro.KEY.d:
this.accRight = true;
break;
}
},
onKeyUp (event) {
switch(event.keyCode) {
case cc.macro.KEY.a:
this.accLeft = false;
break;
case cc.macro.KEY.d:
this.accRight = false;
break;
}
},
然后修改onLoad方法,在其中加入向左和向右加速的开关,以及主角当前在水平方向的速度。最后再调用cc.systemEvent,在场景加载后就开始监听键盘输入:
onLoad: function () {
// 初始化跳跃动作
this.jumpAction = this.setJumpAction();
this.node.runAction(this.jumpAction);
// 加速度方向开关
this.accLeft = false;
this.accRight = false;
// 主角当前水平方向速度
this.xSpeed = 0;
// 初始化键盘输入监听
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);
},
最后修改update方法的内容,添加加速度、速度和主角当前位置的设置:
八.制作星星
现在主角可以跳来跳去,玩家的目标就是引导小怪兽碰触星星来收集分数。被主角碰到的星星会消失,然后马上在随机位置重新生成一个。
1.制作Prefab
可以将需要重复生成的节点保存成Prefab[预制]资源,作为动态生成节点时使用的模板。在assets/scripts/
中添加Star脚本,星星组件只需要一个属性用来规定主角距离星星多近时就可以完成收集,修改properties,加入以下内容并保存脚本。
// Star.js
properties: {
// 星星和主角之间的距离小于这个数值时,就会完成收集
pickRadius: 0,
},
[1]在层级管理器中选中star节点,然后在属性检查器中点击添加组件按钮,选择添加用户脚本组件 -> Star
,该脚本便会添加到刚创建的star节点上。然后在属性管理器中把Pick Radius属性值设置为60。
[2]Star Prefab需要的设置就完成了,现在从层级管理器中将star节点拖拽到资源管理器中的assets文件夹下,就生成了名叫star的Prefab资源。
[3]现在可以从场景中删除star节点了,后续可以直接双击这个starPrefab资源进行编辑。接下去会在脚本中动态使用星星的Prefab资源生成星星。
2.添加游戏控制脚本
在assets/scripts文件夹下添加Game.js游戏主逻辑脚本,这个脚本之后还会添加计分、游戏失败和重新开始的相关逻辑。如下所示:
// Game.js
properties: {
// 这个属性引用了星星预制资源
starPrefab: {
default: null,
type: cc.Prefab
},
// 星星产生后消失时间的随机范围
maxStarDuration: 0,
minStarDuration: 0,
// 地面节点,用于确定星星生成的高度
ground: {
default: null,
type: cc.Node
},
// player 节点,用于获取主角弹跳的高度,和控制主角行动开关
player: {
default: null,
type: cc.Node
}
},
常用参数如下所示:
[1]保存脚本后将Game组件添加到层级管理器中的Canvas节点上[选中Canvas节点后,拖拽脚本到属性检查器上,或者点击属性检查器的添加组件按钮,并从添加用户脚本组件中选择Game]。
[2]从资源管理器中拖拽star的Prefab资源到Game组件的Star Prefab属性中。只有在属性声明时规定type为引用类型时,才能够将资源或节点拖拽到该属性上。
[3]从层级管理器中拖拽ground和Player节点到Canvas节点Game组件中相对应名字的属性上,完成节点引用。
[4]设置MinStarDuration和MaxStarDuration属性的值为3和5,之后生成星星时,会在这两个之间随机取值,就是星星消失前经过的时间。
3.在随机位置生成星星
继续修改Game脚本,在onLoad方法后面添加生成星星的逻辑:
onLoad: function () {
//获取地平面的y轴坐标
this.groundY = this.ground.y + this.ground.height/2;
//生成一个新的星星
this.spawnNewStar();
},
spawnNewStar: function () {
//使用给定的模板在场景中生成一个新节点
var newStar = cc.instantiate(this.starPrefab);
//将新增的节点添加到Canvas节点下面
this.node.addChild(newStar);
//为星星设置一个随机位置
newStar.setPosition(this.getNewStarPosition());
},
getNewStarPosition:function () {
var randX = 0;
//根据地平面位置和主角跳跃高度,随机得到一个星星的y坐标
var randY = this.groundY + Math.random() * this.player.getComponent('Player').jumpHeight + 50;
//根据屏幕宽度,随机得到一个星星x坐标
var maxX = this.node.width/2;
randX = (Math.random()-0.5)*2*maxX;
//返回星星坐标
return cc.v2(randX, randY);
},
[1]节点下的y属性对应的是锚点所在的y坐标,因为锚点默认在节点的中心,所以需要加上地面高度的一半才是地面的y坐标
[2]instantiate方法的作用是:克隆指定的任意类型的对象,或者从Prefab实例化出新节点,返回值为Node或者Object
[3]Node下的addChild方法作用是将新节点建立在该节点的下一级,所以新节点的显示效果在该节点之上
[4]Node下的setPosition方法作用是设置节点在父节点坐标系中的位置,可以通过两种方式设置坐标点。一是传入两个数值x和y,二是传入cc.v2(x,y)[类型为cc.Vec2的对象]
[5]通过Node下的getComponent方法可以得到该节点上挂载的组件引用
在浏览器中预览游戏可以看到,游戏开始后动态生成了一颗星星,如下所示:
4.添加主角碰触收集星星的行为
添加主角收集星星的行为逻辑的重点在于,星星要随时可以获得主角节点的位置,才能判断他们之间的距离是否小于可收集距离,如何获得主角节点的引用呢?主要在Game脚本生成Star节点实例时,将Game组件的实例传入星星并保存起来就好了,之后可以随时通过game.player来访问到主角节点。打开Game脚本,在spawnNewStar方法最后面添加一句newStar.getComponent('Star').game = this;
。如下所示:
// Game.js
spawnNewStar: function() {
// ...
// 在星星组件上暂存 Game 对象的引用
newStar.getComponent('Star').game = this;
},
打开Star脚本,现在可以利用Game组件中引用的player节点来判断距离了,在onLoad方法后面添加名为getPlayerDistance和onPicked的方法:
getPlayerDistance:function () {
//根据player节点位置判断距离
var playerPos = this.game.player.getPosition();
//根据两点位置计算两点之间距离
var dist = this.node.position.sub(playerPos).mag();
return dist;
},
onPicked:function () {
//当星星被收集时,调用Game脚本中的接口,生成一个新的星星
this.game.spawnNewStar();
//然后销毁当前星星节点
this.node.destroy();
},
Node下的getPosition()方法返回的是节点在父节点坐标系中的位置(x,y),即一个Vec2类型对象。同时注意调用Node下的destroy()方法就可以销毁节点。然后在update方法中添加每帧判断距离,如果距离小于pickRadius属性规定的收集距离,就执行收集行为:
update:function (dt) {
//每帧判断和主角之间的距离是否小于收集距离
if (this.getPlayerDistance() < this.pickRadius) {
//调用收集行为
this.onPicked();
return;
}
},
预览游戏,通过按A和D键来控制主角左右移动,就可以看到控制主角靠近星星时,星星就会消失掉,然后在随机位置生成了新的星星。
九.添加得分
接下来添加小怪兽在收集星星时增加得分奖励的逻辑和显示。
1.添加分数文字[Label]
在层级管理器中选中Canvas节点,右键点击并选择菜单中的创建新节点->创建渲染节点->Label[文字],接下来按照如下的步骤配置这个Label节点:
[1]将该节点名字改为score。
[2]将score节点的位置(position属性)设为(0,180)。
[3]选中该节点,编辑属性检查器中Label组件的string属性,填入Score:0的文字。
[4]将Label组件的FontSize属性设为50。
[5]从资源管理器中拖拽assets/mikado_outline_shadow位图字体资源到Label组件的Font属性中,将文字的字体替换成我们项目资源中的位图字体。
2.在Game脚本中添加得分逻辑
把计分和更新分数显示的逻辑放在Game脚本里,打开Game脚本开始编辑,首先在properties区块的最后添加分数显示Label的引用属性:
// Game.js
properties: {
// ...
// score label 的引用
scoreDisplay: {
default: null,
type: cc.Label
}
},
接下来在onLoad方法里面添加计分用的变量的初始化:
// Game.js
onLoad: function () {
// ...
// 初始化计分
this.score = 0;
},
然后在update方法后面添加名叫gainScore的新方法:
// Game.js
gainScore: function () {
this.score += 1;
// 更新scoreDisplay Label的文字
this.scoreDisplay.string = 'Score: ' + this.score;
},
保存Game脚本后,回到层级管理器,选中Canvas节点,然后把前面添加好的score节点拖拽到属性检查器里Game组件的ScoreDisplay属性中。
3.在Star脚本中调用Game中的得分逻辑
下面打开Star脚本,在onPicked方法中加入gainScore的调用:
// Star.js
onPicked: function() {
// 当星星被收集时,调用 Game 脚本中的接口,生成一个新的星星
this.game.spawnNewStar();
// 调用 Game 脚本的得分方法
this.game.gainScore();
// 然后销毁当前星星节点
this.node.destroy();
},
预览游戏,当收集星星时屏幕正上方显示的分数会增加。
十.失败判定和重新开始
星星小时就判定为游戏失败,即玩家需要在每颗星星消失之前完成收集,并不断重复这个过程完成玩法的循环。
1.为星星加入计时消失的逻辑
打开Game脚本,在onLoad方法的spawnNewStar调用之前加入计时需要的变量声明:
// Game.js
onLoad: function () {
// ...
// 初始化计时器
this.timer = 0;
this.starDuration = 0;
// 生成一个新的星星
this.spawnNewStar();
// 初始化计分
this.score = 0;
},
然后在spawnNewStar方法最后加入重置计时器的逻辑,其中this.minStarDuration和this.maxStarDuration是一开始声明的Game组件属性,用来规定星星消失时间的随机范围:
// Game.js
spawnNewStar: function() {
// ...
// 重置计时器,根据消失时间范围随机取一个值
this.starDuration = this.minStarDuration + Math.random() * (this.maxStarDuration - this.minStarDuration);
this.timer = 0;
},
在update方法中加入计时器更新和判断超过时限的逻辑:
// Game.js
update: function (dt) {
// 每帧更新计时器,超过限度还没有生成新的星星
// 就会调用游戏失败逻辑
if (this.timer > this.starDuration) {
this.gameOver();
return;
}
this.timer += dt;
},
在gainScore方法后面加入gameOver方法,游戏失败时重新加载场景。如下所示:
// Game.js
gameOver: function () {
// 停止player节点的跳跃动作
this.player.stopAllActions();
cc.director.loadScene('game');
}
打开Star脚本,需要为即将消失的星星加入简单的视觉提示效果,在update方法最后加入以下代码:
// Star.js
update: function() {
// ...
// 根据 Game 脚本中的计时器更新星星的透明度
var opacityRatio = 1 - this.game.timer/this.game.starDuration;
var minOpacity = 50;
this.node.opacity = minOpacity + Math.floor(opacityRatio * (255 - minOpacity));
}
十一.加入音效
1.跳跃音效
加入跳跃音效,打开Player脚本,添加引用声音文件资源的jumpAudio属性:
// Player.js
properties: {
// ...
// 跳跃音效资源
jumpAudio: {
default: null,
type: cc.AudioClip
},
},
然后改写setJumpAction方法,插入播放音效的回调,并通过添加playJumpSound方法来播放声音:
// Player.js
setJumpAction: function () {
// 跳跃上升
var jumpUp = cc.moveBy(this.jumpDuration, cc.v2(0, this.jumpHeight)).easing(cc.easeCubicActionOut());
// 下落
var jumpDown = cc.moveBy(this.jumpDuration, cc.v2(0, -this.jumpHeight)).easing(cc.easeCubicActionIn());
// 添加一个回调函数,用于在动作结束时调用我们定义的其他方法
var callback = cc.callFunc(this.playJumpSound, this);
// 不断重复,而且每次完成落地动作后调用回调来播放声音
return cc.repeatForever(cc.sequence(jumpUp, jumpDown, callback));
},
playJumpSound: function () {
// 调用声音引擎播放声音
cc.audioEngine.playEffect(this.jumpAudio, false);
},
2.得分音效
打开Game脚本,来添加得分音效,在properties中添加一个属性来引用声音文件资源:
// Game.js
properties: {
// ...
// 得分音效资源
scoreAudio: {
default: null,
type: cc.AudioClip
}
},
然后在gainScore方法里插入播放声音的代码:
// Game.js
gainScore: function () {
this.score += 1;
// 更新 scoreDisplay Label 的文字
this.scoreDisplay.string = 'Score: ' + this.score.toString();
// 播放得分音效
cc.audioEngine.playEffect(this.scoreAudio, false);
},
选中Player节点,然后从资源管理器里拖拽assets/audio/jump资源到Player组件的JumpAudio属性上。选中Canvas节点,把assets/audio/score资源拖拽到Game组件的ScoreAudio属性上。
参考文献:
[1]快速上手:制作第一个游戏:https://docs.cocos.com/creator/2.1/manual/zh/getting-started/quick-start.html