添加开始按钮
实现按钮功能
显示"Game Over"
限制主角移动范围
让跳跃动作更加细腻
加入星星收集动画
编写动画脚本
加入触屏控制
添加游戏提示
Cocos Creator官方所发布的新手教程——快速上手:制作第一个游戏写得详细且通俗易懂,笔者出于学习的目的编写了关于摘星星进阶版的教程,希望对大家有所帮助。自知不能达到官方那样的水平,所以写得比较简单,还请大家不要见笑。
运行效果如下:
Cocos Creator版本号:2.0.10
注:大家可以先下载官方进阶版项目文件来获取资源。
注:笔者会按照官方提供的代码进行讲解,但个别地方可能会稍作修改,当然游戏设计和功能还是保持一致的。
公众号后台回复"摘星星",获取该教程完整项目下载地址:
只有玩家点击了开始按钮,游戏才会开始。
我们将textures文件夹下的按钮图片拖入层级管理器中,使其成为Canvas的子节点:
修改节点名称为BtnStart:
接着在属性检查器中修改节点Y坐标为-50,改变按钮在屏幕上的位置:
为体现按钮特性,我们在属性检查器中给这个节点加一个Button组件:
将该按钮组件的目标节点设置为BtnStart,直接将BtnStart节点拖入Target框中即可,这样Button组件就知道自己所应用的对象是谁了:
当玩家把鼠标(或手指)移动到按钮上或者点击按钮时,按钮的大小、颜色或者其他方面应当发生相应改变,这样可以让用户界面显得更加友好。要做到这样我们只需修改Button组件中的Transition即可(这里我们通过颜色变化作为响应方式):
大家可以看到一共有四种状态,分别是Normal(正常),Pressed(按下不放开)、Hover(悬停)和Disabled(禁用)。我们可以给各个状态设置相应的颜色:
四种状态的十六进制值分别为:#FFFFFF,#C9C9C9,#C9C4C4,#757575。大家可以在颜色修改界面中的Hex Color输出框中输入:
现在我们运行下游戏,发现当我们把鼠标悬停在按钮上,或者点击按钮时,按钮的颜色都会发生改变:
现在我们给按钮加上相应功能:玩家点击按钮后,游戏开始。
既然按钮被按下了游戏才开始,那也就是说在没按下之前星星先不生成,主角也静止不动。于是修改代码如下:
在Game.js的onLoad函数中注释掉或者删除生成星星的代码:
// Game.js
onLoad: function () {
// 获取地平面的 y 轴坐标
this.groundY = this.ground.y + this.ground.height/2;
// 初始化计时器
this.timer = 0;
this.starDuration = 0;
// is showing menu or running game
this.enabled = false;
// 生成一个新的星星
// this.spawnNewStar();
// 初始化计分
this.score = 0;
},
然后在Player.js中注释掉或者让主角运动的代码:
// Player.js
onLoad: function() {
this.enabled = false;
// 初始化跳跃动作
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);
},
Game.js和Player.js中都加入了this.enabled = false;这行代码,目的是为了停止update以及其他计时器(Scheduler)。如果不加这行代码,会发现update方法一直在运行(可加入console.log()试验下)。
此时我们运行游戏,发现主角是静止的,星星也没有生成:
接着我们编写按钮的回调函数:
onStartGame: function () {
// 初始化计分
this.score = 0;
this.scoreDisplay.string = 'Score: ' + this.score.toString();
// set game state to running
this.enabled = true;
// set button and gameover text out of screen
this.btnNode.x = 3000;
// reset player position and move speed
this.player.getComponent('Player').startMoveAt(cc.v2(0, this.groundY));
// spawn star
this.spawnNewStar();
},
按钮按下后,游戏开始。
以下是startMoveAt方法,这里不再赘述,相信大家可以理解:
// Player.js
startMoveAt: function (pos) {
this.enabled = true;
this.xSpeed = 0;
this.node.setPosition(pos);
this.node.runAction(this.setJumpAction());
},
之后我们在properties属性中添加btnNode属性:
// Game.js
btnNode: {
default: null,
type: cc.Node
},
并将BtnStart节点拖入相应的框中:
现在我们选择BtnStart节点,在按钮组件的Click Events选值框中输入1,此时下方就会显示一些需要设置的属性,一共有四个:
填完之后显示如下:
此时运行游戏,点击Play按钮后,主角开始运动,星星也生成了:
我们应该在游戏失败的时候显示"Game Over"字样,这样区分更加明显。
在层级管理器中我们新建一个Label节点,命名为GameOver:
在属性检查器中我们将该节点的各个属性设置如下:
"Game Over"文本应当只有在游戏失败时显示,所以刚开始应当让它不可见。我们将属性检查器下的勾给去掉就行:
此时层级管理器中的GameOver节点显示灰色,而且场景编辑器中也没有显示"Game Over":
现在将GameOver节点写进代码中。首先在properties中添加gameOverNode属性:
// Game.js
gameOverNode: {
default: null,
type: cc.Node
},
然后在gameOver方法中,设置该节点可见:
// Game.js
gameOver: function () {
this.gameOverNode.active = true;
this.btnNode.x = 0;
this.player.getComponent('Player').enabled = false;
this.player.stopAllActions(); //停止 player 节点的跳跃动作
// cc.director.loadScene('game');
}
在该方法中我们还要让按钮重新回到屏幕上以及停止主角的一切动作,还要去掉cc.director.loadScene('game')这行代码,否则游戏会立即重新载入。
此时我们运行游戏,发现游戏失败后显示"Game Over"字样:
但是如果我们点击Play按钮后,"Game Over"没有消失,而且还会再出现一个星星:
也就是说我们应该在用户点击了Play按钮后使"Game Over"文本不可见,并且要删除已存在的星星节点(该删除操作也可以放在gameOver方法中)。我们首先完成第一个:
// Game.js
onStartGame: function () {
...
// "Game Over" not visible
this.gameOverNode.active = false;
...
},
在编写删除星星的代码前,我们将生成星星的代码稍微修改下。目前我们是通过直接实例化一个Prefab来实现,而在进阶版中我们将使用对象池来更好的管理星星对象。
对象池就是一组可回收的节点对象,我们通过创建
cc.NodePool
的实例来初始化一种节点的对象池。通常当我们有多个 prefab 需要实例化时,应该为每个 prefab 创建一个cc.NodePool
实例。 当我们需要创建节点时,向对象池申请一个节点,如果对象池里有空闲的可用节点,就会把节点返回给用户,用户通过node.addChild
将这个新节点加入到场景节点树中。
我们首先在onLoad方法中创建两个变量:
// Game.js
onLoad: function () {
...
// store last star's x position
this.currentStar = null;
...
// initialize star and score pool
this.starPool = new cc.NodePool('Star');
},
currentStar用于存储新生成的星星对象,currentStarX用于存储它的x坐标,而starPool就是对象池实例。
接着修改spawnNewStar方法:
// Game.js
spawnNewStar: function() {
var newStar = null;
// 使用给定的模板在场景中生成一个新节点
if (this.starPool.size() > 0) {
newStar = this.starPool.get(this); // this will be passed to Star's reuse method
} else {
newStar = cc.instantiate(this.starPrefab);
// pass Game instance to star
newStar.getComponent('Star').reuse(this);
}
// 为星星设置一个随机位置
newStar.setPosition(this.getNewStarPosition());
// 将新增的节点添加到 Canvas 节点下面
this.node.addChild(newStar);
// 重置计时器,根据消失时间范围随机取一个值
this.starDuration = this.minStarDuration + Math.random() * (this.maxStarDuration - this.minStarDuration);
this.timer = 0;
this.currentStar = newStar;
},
我们通过调用size()来获取对象池大小。如果对象池中有星星对象,则调用get()获取一个;如果没有则实例化生成一个,并直接传入Game实例。接着设置星星的坐标并添加到Canvas上,重置计时器,并将新生成的星星对象赋值给currentStar变量。
大家可能已经注意到get()中传入了一个this,并且会对reuse()产生疑问(这哪来的?)。当我们在实例化对象池的时候,传入了'Star'(其实就是Star.js),这样的话当我们调用get方法时,就会自动调用Star.js中的reuse方法;而当调用put方法回收星星时,程序就会自动调用Star.js中的unuse方法。当然我们还得自己手动往Star.js中加入这两个函数先:
// Star.js
reuse(game) {
this.game = game;
this.enabled = true;
this.node.opacity = 255;
},
unuse() {
// 因为回收时不执行任何操作,所以该方法可以不写
},
星星生成的代码已经搞定,接下来编写回收代码:
// Game.js
despawnStar (star) {
this.starPool.put(star);
this.spawnNewStar();
},
该方法带一个参数star,也就是说我们调用的时候要传入星星对象所在的节点。在方法中我们调用put()来回收星星对象,同时生成一个新的星星。
现在我们在Star.js的onPicked方法中调用despawnStar():
// Star.js
onPicked: function() {
// 调用 Game 脚本的得分方法
this.game.gainScore();
// 然后回收当前星星节点,并生成新的星星
this.game.despawnStar(this.node);
},
上面是星星被主角收集,而在游戏结束时,我们还应当销毁当前存在于屏幕上的星星:
// Game.js
gameOver: function () {
...
this.currentStar.destroy();
}
现在运行游戏,发现结束后星星会消失(节点被回收):
现在我们给主角的活动范围加一个边界,禁止它跳到屏幕外边。要实现该功能,我们只需要往Player.js中添加一些代码即可:
// Player.js
update: function (dt) {
...
// limit player position inside screen
if ( this.node.x > this.node.parent.width/2) {
this.node.x = this.node.parent.width/2;
this.xSpeed = 0;
} else if (this.node.x < -this.node.parent.width/2) {
this.node.x = -this.node.parent.width/2;
this.xSpeed = 0;
}
},
通过比较主角节点的x坐标与屏幕一半大小来判断主角是否超出边界。若超出,则设置节点x坐标值为屏幕一半大小即可。
现在运行游戏,发现主角已无法跳出屏幕外:
主角现在的跳跃动作还是非常平淡的,没有一种伸缩的感觉。按道理来讲,这种长得像果冻一样的主角,跳跃动作应该表现得像果冻一样才对。
要实现这样的功能也非常容易,我们只需要往Play.js的setJumpAction方法中再加入一些动作:
// 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 squash = cc.scaleTo(this.squashDuration, 1, 0.6);
var stretch = cc.scaleTo(this.squashDuration, 1, 1.2);
var scaleBack = cc.scaleTo(this.squashDuration, 1, 1);
// 添加一个回调函数,用于在动作结束时调用我们定义的其他方法
var callback = cc.callFunc(this.playJumpSound, this);
// 不断重复,而且每次完成落地动作后调用回调来播放声音
return cc.repeatForever(cc.sequence(squash, stretch, jumpUp, scaleBack, jumpDown, callback));
},
可以看到我们加入了squash(挤压),stretch(伸展)和scaleBack(复原)这三个动作,持续时间都为squashDuration,然后加入到了sequence中。记得在properties中添加相应属性:
// Player.js
properties: {
// 主角跳跃高度
jumpHeight: 0,
// 主角跳跃持续时间
jumpDuration: 0,
// 辅助形变动作时间
squashDuration: 0,
...
},
接着我们在属性检查器中修改squashDuration的值:
现在运行游戏,发现跳跃动作更细腻了,更像一个果冻了:
为了让收集星星时的效果更佳华丽酷炫,我们可以在主角和星星碰撞时加入一小段动画。官方在项目中提供了几张用于动画制作的图片,大家可以先导入进来:
在创建动画前,我们先来了解下动画组件和动画剪辑这两个概念。前者作为组件可以被添加到节点上,它可以用来管理一个或多个动画剪辑,而动画剪辑其实就是动画的所播放的内容了。
我们先在层级管理器中新建一个空节点,并将其命名为animRoot:
接着在该节点下再创建一个Sprite精灵节点和一个Label节点,分别命名为pop和score。前者跟动画中的图片有关,后者跟文本有关,相当于有两个节点的动画要实现。
我们这里直接将score节点的文本颜色改#F8DD4D,内容改成+1并拖入字体文件:
现在可以往animRoot上添加一个动画组件了:
添加之后我们看到该组件一共有三个属性:Default Clip,Clips和Play On Load。
以下是官方对这三种属性的解释,非常清楚:
属性 | 功能说明 |
---|---|
Default Clip | 默认的动画剪辑,如果这一项设置了值,并且 Play On Load 也为true,那么动画会在加载完成后自动播放 Default Clip 的内容 |
Clips | 列表类型,默认为空,在这里面添加的 AnimationClip 会反映到 动画编辑器 中,用户可以在 动画编辑器 里编辑 Clips 的内容 |
Play On Load | 布尔类型,是否在动画加载完成后自动播放 Default Clip 的内容 |
那我们这里只会用到一个动画剪辑,所以使用Default Clip就好(当Default Clip被放入动画剪辑时,Clips属性值自动会变为1)。而动画是在主角碰到星星后才会播放,所以Play On Load不勾选。
现在我们可以在资源管理器中新建一个动画剪辑,并命名为score_pop:
然后将该动画剪辑拖入到刚才动画组件的Default Clip属性中:
前期工作已经做好,可以开始编辑动画了!我们点击控制台旁边的动画编辑器标签页,显示编辑器界面:
按照提示点击左上角按钮开始编辑,由于不需要太高质量,我们这里把帧速率(每秒动画帧数)改为25:
关于该编辑器的介绍,大家可以直接去看官方文档,笔者这里不再赘述。
为方便理解,我们先来处理score节点的动画。点击编辑器中的score,然后在属性列表中添加position属性(位置)和opacity属性(透明度):
所谓动画,其实就是把不同时刻表现形式不同的各个帧给连接起来。那也就是说我们只要让position和opacity在特定的帧数下表现不同就行了。
先搞定position。在开始处(第0帧),我们给position插入一帧(Windows下点击右键,Mac下双指点击触控板):
然后再把那条红色移动到第15帧,并插入一帧:
第15帧的时候,position肯定要表现不同,我们这里让它的y值变大,也就是说播放动画的时候socre节点会上升。此时我们保持红色位置不变,在属性检查器中修改y坐标为92(当然也可以其他数字,这里只是按照官方来):
此时我们发现在动画编辑器中出现一条淡蓝的线,将表现不同的两个帧给连接了起来:
现在点击动画编辑器左上角的播放按钮,可以在场景编辑器中看到冉冉升起的+1文本。但是这种匀速升起有点平淡,我们可以高点花样。双击蓝色线条,会出现一个速率编辑器:
点击Custom标签,然后把右边的蓝色线条拖曳成下面这个妖娆的样子(先慢后快再慢):
点击左边保存按钮后,我们关掉它,然后再次点击播放按钮,可以看到+1文本的运动显得更加有趣了些。
同理我们处理下opacity属性,插入帧位置如下(15,19,20):
我们将第19帧和第20帧的透明度分别改为51和0(注意移动红色位置),不用改变化速率:
现在来处理下pop节点。给它加上opacity,scaleX,scaleY和cc.Sprite.spriteFrame这四个属性:
scaleX和scaleY用来改变大小,而cc.Sprite.spriteFrame就是用来放不同图片的。我们先来处理下opacity,scaleX和scaleY:
在opacity属性的第8帧和第10帧插入一帧,然后修改第10帧的透明度为0。在scaleX和scaleY的第0帧,第2帧和第5帧插入一帧,修改第2帧的Scale X或Scale Y值为1.5。
针对cc.Sprite.spriteFrame属性,我们并不用插入帧,而是可以直接将图片拖入到相应帧位置。首先将第一章图片拖入到第二帧的位置:
大家现在可以点击播放按钮看下效果。
动画制作到此结束,我们点击左上角的编辑按钮来保存(或者可以使用ctrl/cmd + s组合键),这样pop和score节点的动画就都保存在score_pop动画剪辑中了:
每当主角吃到星星,该动画就会显示出来,所以我们应该将animRoot节点变成一个预制(相信大家已经知道怎么把节点变预制了)。接着再在Game.js的properties中添加一个animRootPrfab属性:
// Game.js
properties: {
// 这个属性引用了星星预制资源
starPrefab: {
default: null,
type: cc.Prefab
},
animRootPrefab: {
default: null,
type: cc.Prefab
},
// 星星产生后消失时间的随机范围
maxStarDuration: 0,
minStarDuration: 0,
...
},
同样我们要使用对象池来管理生成的预制,所以在onLoad方法中新建一个currentAnimRoot和scorePool变量:
// Game.js
onLoad: function () {
// store last star's x position
this.currentStar = null;
this.currentAnimRoot = null;
...
// initialize star and score pool
this.starPool = new cc.NodePool('Star');
this.scorePool = new cc.NodePool('ScoreAnim');
},
这里传入一个ScoreAnim脚本名,该脚本内容如下:
// ScoreAnim.js
cc.Class({
extends: cc.Component,
reuse(game) {
this.game = game;
},
despawn() {
this.game.despawnAnimRoot();
}
});
将该脚本添加到animRoot节点上:
现在编写一个spawnAnimRoot方法用于生成动画预制:
// Game.js
spawnAnimRoot: function () {
var fx;
if (this.scorePool.size() > 0) {
fx = this.scorePool.get(this);
} else {
fx = cc.instantiate(this.animRootPrefab);
fx.getComponent('ScoreAnim').reuse(this);
}
return fx;
},
可以看到我们同时将this传给ScoreAnim.js中的reuse方法,目的就是为了调用Game.js中的despawnAnimRoot(至于该方法有什么用,后面会讲解)。
每加一分,动画播放一次,所以我们会在gainScore方法中调用spawnAnimRoot方法:
// Game.js
gainScore: function (pos) {
this.score += 1;
// 更新 scoreDisplay Label 的文字
this.scoreDisplay.string = 'Score: ' + this.score;
// 播放特效
this.currentAnimRoot = this.spawnAnimRoot();
this.node.addChild(this.currentAnimRoot.node);
this.currentAnimRoot.node.setPosition(pos);
this.currentAnimRoot.getComponent(cc.Animation).play('score_pop');
// 播放得分音效
cc.audioEngine.playEffect(this.scoreAudio, false);
},
可以看到gainScore方法多了一个参数pos。每当主角碰到星星的时候,获取星星所在的位置,传给gainScore方法作为动画预制的坐标。这样动画就会在星星被吃掉的位置播放了。于是我们修改下Star.js中的onPicked方法:
// Star.js
onPicked: function() {
var pos = this.node.getPosition();
// 调用 Game 脚本的得分方法
this.game.gainScore(pos);
// 然后销毁当前星星节点
this.game.despawnStar(this.node);
},
动画播放完之后肯定要回收,代码如下:
// Game.js
despawnAnimRoot () {
this.scorePool.put(this.currentAnimRoot);
},
那问题来了,我们怎么知道动画什么时候会播放结束呢?其实在动画编辑器中我们可以为帧添加回调函数,所以我们在动画播放的最后一帧添加despawnAnimRoot方法即可。
首先打开动画编辑器,然后我们把红线移动到最后一帧,也就是第20帧:
然后在点击左上角的一个小箭头按钮给该帧添加一个回调函数:
双击该小块,进入到回调函数编辑区域:
我们可以直接往输入框中输入方法名称,不过该方法必须来自animRoot节点上的脚本组件,也就是ScoreAnim.js,那么这里就填入despawn(我们已经在该方法中调用了Game.js中的despawnAnimRoot方法):
输入完毕点击保存。接着将animRoot节点拖入资源管理器使其成为一个预制,再将其拖入到Canvas的Game组件中:
运行游戏,动画正常播放:
目前该游戏只能通过键盘来控制主角移动,现在我们为移动设备加入触屏控制。非常简单,只需要监听触摸事件即可:
// Player.js
onLoad: function() {
...
var touchReceiver = cc.Canvas.instance.node;
touchReceiver.on('touchstart', this.onTouchStart, this);
touchReceiver.on('touchend', this.onTouchEnd, this);
},
onDestroy () {
// 取消键盘输入监听
...
var touchReceiver = cc.Canvas.instance.node;
touchReceiver.off('touchstart', this.onTouchStart, this);
touchReceiver.off('touchend', this.onTouchEnd, this);
},
然后在onTouchStart和onTouchEnd方法中编写相应逻辑代码:
// Player.js
onTouchStart (event) {
var touchLoc = event.getLocation();
if (touchLoc.x >= cc.winSize.width/2) {
this.accLeft = false;
this.accRight = true;
} else {
this.accLeft = true;
this.accRight = false;
}
},
onTouchEnd (event) {
this.accLeft = false;
this.accRight = false;
},
当手指点击屏幕右侧时,主角往右加速;点击左侧时,往左移动加速。
这里插个小曲,在基础版摘星星代码中,官方还没有用键盘上的左右箭头来控制。而在进阶版中添加了:
// Player.js
onKeyDown (event) {
// set a flag when key pressed
switch(event.keyCode) {
case cc.macro.KEY.a:
case cc.macro.KEY.left:
this.accLeft = true;
break;
case cc.macro.KEY.d:
case cc.macro.KEY.right:
this.accRight = true;
break;
}
},
onKeyUp (event) {
// unset a flag when key released
switch(event.keyCode) {
case cc.macro.KEY.a:
case cc.macro.KEY.left:
this.accLeft = false;
break;
case cc.macro.KEY.d:
case cc.macro.KEY.right:
this.accRight = false;
break;
}
},
很简单,无非就是多了个case判断。
添加游戏提示可以让游戏更加友好,否则玩家可能一开始不知道要怎么样往下进行。
我们在层级管理器中加入两个Label控件,分别命名为Instruction_Control和Instruction_Goal:
将前者的x和y坐标以及文本内容修改如下:
将后者的x和y坐标以及文本内容修改如下:
同时修改下score节点的y坐标值为270:
最后修改Player节点y坐标值为0,让它不要被开始按钮遮住:
运行截图如下:
好到这里就讲解结束啦,希望大家有所收获!
欢迎关注我的微信公众号,发现更多有趣内容: