第一个游戏
这节我们从头做一个比较有意思的小游戏——一步两步。
下面是最终效果 (跳一步得一分,跳两步得三分,电脑端左右键 12 步):
一步两步
写在前面
这是本教程第一个游戏,所以我会讲的详细一点,但是也不能避免遗漏,所以有什么问题你可以先尝试查阅文档自己解决或者在下方留言,后面我会跟进完善。
另外这个游戏并非原创,参照的是腾讯的微信小游戏《一步两步 H5》,请不要直接搬过去,微信里重复的游戏太多了。
创建工程
选择空白项目创建工程
你可以从这里下载游戏素材, 然后将素材导入工程(直接拖进编辑器,或者放在工程目录)
也可以从 https://github.com/potato47/one-two-step 下载完整代码进行参照。
准备工作做完后,我会把这个游戏制作过程分为若干个小过程,让你体会一下实际的游戏制作体验。
从一个场景跳转到另一个场景
在 res 文件夹下新建一个 scenes 文件夹,然后在 scenes 里新建两个场景 menu 和 game(右键 -> 新建 ->Scene)。然后双击 menu 进入 menu 场景。
在层级管理器中选中 Canvas 节点,在右侧属性检查器中将其设计分辨率调整为 1280×720,然后将 background 图片拖入 Canvas 节点下,并为其添加 Widget 组件(添加组件 ->UI 组件 ->Widget), 使其充满画布。
在 Canvas 下新建一个 Label 节点(右键 -> 创建节点 -> 创建渲染节点 ->Label),然后调整文字大小,并添加标题文字
我们知道节点和组件通常是一起出现的,带有常见组件的节点在编辑器里可以直接创建,比如刚才的带有 Label 组件的节点和带有 Sprite 组件的节点,但是我们也可以新建一个空节点然后为其添加对应的组件来组装一个带有特殊功能的节点。
新建节点 ->UI 节点下有一个 Button,如果你直接创建 Button,你会发现它是一个带有 Button 组件的节点,并且有一个 Label 的子节点。现在我们用另一种方法创建 Button:在 Canvas 右键新建一个空节点,然后为其添加 Button 组件,这时你会发现按钮并没有背景,所以我们再添加一个 Sprite 组件,拖入资源中的按钮背景图片,最后添加一个 Label 子节点给按钮添加上文字。
下面我们给这个按钮添加点击事件。
在资源管理器中新建 src 文件夹用来存放脚本,然后新建一个 TypeScript 脚本,名字为 Menu(注意脚本组件名称区分大小写,这里建议首字母大写)。
双击用 VS Code 打开脚本,更改如下:
const {ccclass} = cc._decorator;
@ccclass // 让编辑器能够识别这是一个组件
export class Menu extends cc.Component {
private onBtnStart() {
cc.director.loadScene(‘game’); // 加载 game 场景
}
}
一个类只有加上 @ccclass 才能被编辑器识别为脚本组件,如果你去掉 @ccclass,你就不能把这个组件拖到节点上。另外可以看到代码中出现了几次 cc 这个东西,cc 其实是 Cocos 的简称,在游戏中是引擎的主要命名空间,引擎代码中所有的类、函数、属性和常量都在这个命名空间中定义。
很明显,我们想在点击开始按钮的时候调用 onBtnStart 函数,然后跳转到 game 场景。为了测试效果我们先打开 game 场景,然后放一个测试文字(将 Canvas 的设计分辨率也改为 1280×720)。
保存 game 场景后再回到 Menu 场景。
Button 组件点击后会发出一个事件,这个事件可以跟某个节点上的某个脚本内的某个函数绑定在一起。听着有点绕,动手做一遍就会明白这个机制。
首先将 Menu 脚本添加为 Canvas 节点的组件,然后在开始按钮的 Button 组件里添加一个 Click Event,将其指向 Canvas 节点下的 Menu 脚本里的 onBtnStart 函数。
我们再调整一下 Button 的点击效果, 将 Button 组件的 Transition 改为 scale(伸缩效果),另外还有颜色变化和图片变化,可以自己尝试。
最后点击上方的预览按钮,不出意外的话就可以在浏览器中看见预期效果。
组织代码结构
现在我们来编写游戏逻辑。
首先我来讲一下我看到的一种现象:
很多新手非常喜欢问,“看代码我都能看懂啊,但是要我自己写我就没思路啊”
这时一位经验颇多的长者就会甩给他一句,“多写写就有思路了“
不知道你们发现没有,这竟然是一个死循环。
对于一个刚开始学习做游戏的人,首先要了解的是如何组织你的代码,这里我教给大家一个最容易入门的代码结构——单向分权结构(这是我想了足足两分钟的自认为很酷炫的一个名字)
脚本分层:
这个结构最重要的就是“权”这个字,我们把一个场景中使用的脚本按照“权力”大小给它们分层,权力最大的在最上层且只有一个,这个脚本里保存着它直接控制的若干个脚本的引用,被引用的脚本权力就小一级,被引用的脚本还会引用比它权力更小的脚本,依此类推。
脚本互操作:
上一层的脚本由于保存着下一层脚本的引用,所以可以直接操作下一层的脚本。
下一层的脚本由上一层的脚本初始化,在初始化的时候会传入上一层的引用(可选),这样在需要的时候会反馈给上一层,由上一层执行更具体的操作。
同层的脚本尽量不要互相操作,统一交给上层处理,同层解耦。
不可避免的同层或跨层脚本操作可以使用全局事件来完成。
具有通用功能的脚本抽离出来,任意层的脚本都可以直接使用。
写了这么多,但你肯定没看懂,现在你可以翻到最上面再分析一下游戏的 game 场景,如何组织这个场景的脚本结构?
首先,一个场景的根节点会挂载一个脚本,通常以场景名命名,这里就是 Game。
然后跳跃的人物也对应着一个脚本 Player。
跟 Player 同层的还应该有 Block 也就是人物踩着的地面方块。
因为 Player 和 Block 之间互相影响并且我想让 Game 脚本更简洁,所以这里再加一个 Stage(舞台)脚本来控制 Player 和 Block。
最终它们的层级关系如下:
Game
Stage
Player
Block
上面这些都是我们的思考过程,下面我们落实到场景中。
先新建几个脚本
现在搭建场景,先添加一个跟 menu 场景一样的全屏背景
然后添加一个空节点 Stage,在 Stage 下添加一个 Player 节点和一个 Block 节点
在 Stage 同层添加两个按钮来控制跳一步两步
先添加第一个按钮,根据实际效果调整文字大小(font size)颜色(node color)和按钮的缩放倍数(scale)
第二个按钮可以直接由第一个按钮复制
这两个按钮显然是要放置在屏幕左下角和右下角的,但是不同屏幕大小可能导致这两个按钮的位置跑偏,所以最好的方案是给这两个按钮节点添加 Widget 组件,让它们跟左下角和右下角保持固定的距离,这里就不演示了,相信你可以自己完成(其实是我忘录了。。。)
添加一个 Label 节点记录分数,系统字体有点丑,这里替换成我们自己的字体
最后把脚本挂在对应的节点上。
场景搭建到这里基本完成了,现在可以编写脚本了。
Game 作为一个统领全局的脚本,一定要控制关键的逻辑,,比如开始游戏和结束游戏,增加分数,还有一些全局的事件。
Game.ts
import {Stage} from ‘./Stage’;
const {ccclass, property} = cc._decorator;
@ccclass
export class Game extends cc.Component {
@property(Stage)
private stage: Stage = null;
@property(cc.Label)
private scoreLabel: cc.Label = null;
private score: number = 0;
protected start() {
this.startGame();
}
public addScore(n: number) {
this.score += n;
this.scoreLabel.string = this.score + ”;
}
public startGame() {
this.score = 0;
this.scoreLabel.string = ‘0’;
this.stage.init(this);
}
public overGame() {
cc.log(‘game over’);
}
public restartGame() {
cc.director.loadScene(‘game’);
}
public returnMenu() {
cc.director.loadScene(‘menu’);
}
private onBtnOne() {
this.stage.playerJump(1);
}
private onBtnTwo() {
this.stage.playerJump(2);
}
}
Stage 作为 Game 直接控制的脚本,要给 Game 暴露出操作的接口并且保存 Game 的引用,当游戏状态发生改变时,通知 Game 处理。
Stage.ts
import {Game} from ‘./Game’;
import {Player} from ‘./Player’;
const {ccclass, property} = cc._decorator;
@ccclass
export class Stage extends cc.Component {
@property(Player)
private player: Player = null;
private game: Game = null;
public init(game: Game) {
this.game = game;
}
public playerJump(step: number) {
this.player.jump(step);
}
}
而 Player 作为最底层的一个小员工,别人让你做啥你就做啥。
Player.ts
const {ccclass, property} = cc._decorator;
@ccclass
export class Player extends cc.Component {
public jump(step: number) {
if (step === 1) {
cc.log(‘ 我跳了 1 步 ’);
} else if (step === 2) {
cc.log(‘ 我跳了 2 步 ’);
}
}
public die() {
cc.log(‘ 我死了 ’);
}
}
之前讲了 @ccclass 是为了让编辑器识别这是一个组件类,可以挂在节点上,现在我们又看到了一个 @property, 这个是为了让一个组件的属性暴露在编辑器属性中,观察最上面的 Game 脚本,发现有三个成员变量,stage,scoreLabel 和 score,而只有前两个变量加上了 @property, 所以编辑器中只能看到 stage 和 scoreLabel。
@property 括号里通常要填一个编辑器可以识别的类型,比如系统自带的 cc.Label,cc.Node,cc.Sprite,cc.Integer,cc.Float 等,也可以是用户脚本类名, 比如上面的 Stage 和 Player。
回到编辑器,我们把几个脚本暴露在编辑器的变量通过拖拽的方式指向带有类型组件的节点。
再把 one,two 两个按钮分别绑定在 game 里的 onBtnOne,onBtnTwo 两个函数上。
这时我们已经有了一个简单的逻辑,点击 1 或 2 按钮,调用 Game 里的 onBtnOne 或 onBtnTwo,传递给 Stage 调用 playerJump,再传递给 Player 调用 jump,player 就会表现出跳一步还是跳两步的反应。
点击预览按钮,进行测试:
你可以按 F12(windows) 或 cmd+opt+i(mac) 打开 chrome 的开发者工具。
人物跳跃动作
现在我们来让 Player 跳起来,人物动作的实现大概可以借助以下几种方式实现:
动画系统
动作系统
物理系统
实时计算
可以看到这个游戏人物动作比较简单,跳跃路径是固定的,所以我们选择用动作系统实现人物的跳跃动作。
creator 自带一套基于节点的动作系统,形式如 node.runAction(action)。
修改 Player.ts, 添加几个描述跳跃动作的参数,并且添加一个 init 函数由上层组件即 Stage 初始化时调用并传入所需参数。另外更改 jump 函数内容让 Player 执行 jumpBy 动作。
Player.ts
…
private stepDistance: number; // 一步跳跃距离
private jumpHeight: number; // 跳跃高度
private jumpDuration: number; // 跳跃持续时间
public canJump: boolean; // 此时是否能跳跃
public init(stepDistance: number, jumpHeight: number, jumpDuration: number) {
this.stepDistance = stepDistance;
this.jumpHeight = jumpHeight;
this.jumpDuration = jumpDuration;
this.canJump = true;
}
public jump(step: number) {
this.canJump = false;
this.index += step;
let jumpAction = cc.jumpBy(this.jumpDuration, cc.v2(step * this.stepDistance, 0), this.jumpHeight, 1);
let finishAction = cc.callFunc(() => {
this.canJump = true;
});
this.node.runAction(cc.sequence(jumpAction, finishAction));
}
…
Stage.ts
…
@property(cc.Integer)
private stepDistance: number = 200;
@property(cc.Integer)
private jumpHeight: number = 100;
@property(cc.Float)
private jumpDuration: number = 0.3;
@property(Player)
private player: Player = null;
private game: Game = null;
public init(game: Game) {
this.game = game;
this.player.init(this.stepDistance, this.jumpHeight, this.jumpDuration);
}
public playerJump(step: number) {
if (this.player.canJump) {
this.player.jump(step);
}
}
…
这里要介绍一下 Cocos Creator 的动作系统,动作系统基于节点,你可以让一个节点执行一个瞬时动作或持续性的动作。比如让一个节点执行一个“3 秒钟向右移动 100”的动作,就可以这样写
let moveAction = cc.moveBy(3, cc.v2(100, 0)); // cc.v2 可以创建一个二位的点(向量),代表方向 x =100,y=0
this.node.runAction(moveAction);
更多的动作使用可查询文档 http://docs.cocos.com/creator…
回头看 Player 的 jump 方法,这里我们的意图是让 Player 执行一个跳跃动作,当跳跃动作完成时将 this.canJump 改为 true,cc.CallFunc 也是一个动作,这个动作可以执行你传入的一个函数。所以上面的 finishAction 执行的时候就可以将 this.canJump 改为 true,cc.sequence 用于将几个动作连接依次执行。
可以看到 jumpAction 传入了很多参数,有些参数可以直接根据名字猜到,有一些可能不知道代表什么意思,这时你就要善于搜索 api,另外要充分利用 ts 提示的功能, 你可以直接按住 ctrl/cmd+ 鼠标单击进入定义文件查看说明示例。
再来看 Stage,可以看到 Player 初始化的几个参数是由 Stage 传递的,并且暴露在了编辑器界面,我们可以直接在 Stage 的属性面板调整参数,来直观的编辑动作效果,这也是 Creator 编辑器的方便之处。
上面的几个参数是我调整过后的,你也可以适当的修改,保存场景后预览效果。
动态添加地面和移动场景
显而易见,我们不可能提前设置好所有的地面(Block),而是要根据 Player 跳跃的时机和地点动态添加 Block,这就涉及到一个新的知识点——如何用代码创建节点?
每一个 Block 节点都是一样的,对于这样相同的节点可以抽象出一个模板,Creator 里管这个模板叫做预制体(Prefab),想要一个新的节点时就可以通过复制 Prefab 得到。
制作一个 Prefab 很简单,我们先在 res 目录下新建一个 prefabs 目录,然后将 Block 节点直接拖到目录里就可以形成一个 Prefab 了。
你可以双击这个 prefab 进入其编辑模式,如果之前忘了将 Block 脚本挂在 Block 节点上,这里也可以挂在 Block 的 Prefab 上。
有了 Prefab 后,我们就可以利用函数 cc.instance 来创建出一个节点。
根据之前讲的组织代码原则,创建 Block 的职责应该交给他的上级,也就是 Stage。
之前编写 Player 代码时设置了一个 index 变量,用来记录 Player 跳到了“第几格”,根据游戏逻辑每当 Player 跳跃动作完成后就要有新的 Block 出现在前面。修改 Stage 如下:
Stage.ts
import {Game} from ‘./Game’;
import {Player} from ‘./Player’;
import {Block} from ‘./Block’;
const {ccclass, property} = cc._decorator;
@ccclass
export class Stage extends cc.Component {
@property(cc.Integer)
private stepDistance: number = 200;
@property(cc.Integer)
private jumpHeight: number = 100;
@property(cc.Float)
private jumpDuration: number = 0.3;
@property(Player)
private player: Player = null;
@property(cc.Prefab)
private blockPrefab: cc.Prefab = null; // 编辑器属性引用
private lastBlock = true; // 记录上一次是否添加了 Block
private lastBlockX = 0; // 记录上一次添加 Block 的 x 坐标
private blockList: Array<Block>; // 记录添加的 Block 列表
private game: Game = null;
public init(game: Game) {
this.game = game;
this.player.init(this.stepDistance, this.jumpHeight, this.jumpDuration);
this.blockList = [];
this.addBlock(cc.v2(0, 0));
for (let i = 0; i < 5; i++) {
this.randomAddBlock();
}
}
public playerJump(step: number) {
if (this.player.canJump) {
this.player.jump(step);
this.moveStage(step);
let isDead = !this.hasBlock(this.player.index);
if (isDead) {
cc.log(‘die’);
this.game.overGame();
} else {
this.game.addScore(step === 1 ? 1 : 3); // 跳一步得一分,跳两步的三分
}
}
}
private moveStage(step: number) {
let moveAction = cc.moveBy(this.jumpDuration, cc.v2(-this.stepDistance * step, 0));
this.node.runAction(moveAction);
for (let i = 0; i < step; i++) {
this.randomAddBlock();
}
}
private randomAddBlock() {
if (!this.lastBlock || Math.random() > 0.5) {
this.addBlock(cc.v2(this.lastBlockX + this.stepDistance, 0));
} else {
this.addBlank();
}
this.lastBlockX = this.lastBlockX + this.stepDistance;
}
private addBlock(position: cc.Vec2) {
let blockNode = cc.instantiate(this.blockPrefab);
this.node.addChild(blockNode);
blockNode.position = position;
this.blockList.push(blockNode.getComponent(Block));
this.lastBlock = true;
}
private addBlank() {
this.blockList.push(null);
this.lastBlock = false;
}
private hasBlock(index: number): boolean {
return this.blockList[index] !== null;
}
}
首先我们在最上面添加了几个成员变量又来记录 Block 的相关信息。然后修改了 playerJump 方法,让 player 跳跃的同时执行 moveStage,moveStage 方法里调用了一个 moveBy 动作,这个动作就是把节点相对移动一段距离,这里要注意的是 moveStage 动作和 player 里的 jump 动作水平移动的距离绝对值和时间都是相等的,player 向前跳,stage 向后移动,这样两个相反的动作,就会让 player 始终处于屏幕中的固定位置而不会跳到屏幕外了。
再看 moveStage 方法里会调用 randomAddBlock,也就是随机添加 block,随机算法要根据游戏规则推理一下:
这个游戏的操作分支只有两个:1 步或者是 2 步。所以每 2 个 Block 的间隔只能是 0 步或者 1 步。因此 randomAddBlock 里会判断最后一个 Block 是否为空,如果为空那新添加的一定不能为空。如果不为空则 50% 的概率随机添加或不添加 Block。这样就能得到无限随机的地图了。
为了激励玩家多按 2 步,所以设定跳 1 步的 1 分,跳 2 步得 3 分。
另外 Player 跳几步 randomAddBlock 就要调用几次,这样才能保证地图与 Player 跳跃距离相匹配。
再说一下 addBlock 方法,blockNode 是由 blockPrefab 复制出来的,你必须通过 addChild 方法把它添加场景中的某个节点下才能让它显示出来,这里的 this.node 就是 Stage 节点。为了方便我们把 lastBlockX 初始值设为 0,也就是水平第一个 block 的横坐标应该等于 0,所以我们要回到编辑器调整一下 stage,player,block 三个节点的位置,让 block 和 player 的 x 都等于 0,并且把 Block 的宽度设为 180(一步的距离设为 200,为了让两个相邻的 Block 有一点间距,要适当窄一些),最后不要忘记把 BlockPrefab 拖入对应的属性上。
playerJump 的的最后有一段判断游戏结束的逻辑,之前我们在 player 里设置了一个变量 index,记录 player 当前跳到第几格,stage 里也有一个数组变量 blockList 保存着所有格子的信息,当 player 跳完后判断一下落地点是否有格子就可以判断游戏是否结束。
捋顺上面的逻辑后,你就可以预览一下这个看起来有点样子的游戏了
地面下沉效果
如果每一步都让玩家想很久,那这个游戏就没有尽头了。现在我们给它加点难度。
设置的效果是:地面每隔一段时间就会下落,如果玩家没有及时跳到下一个格子就会跟着地面掉下去,为了实现这个人物和地面同时下坠的效果,我们要让 Player 和 Block 执行相同的动作,所以在 Stage 上新加两个变量 fallDuration 和 fallHeight 用来代表下落动作的时间和高度,然后传给 Player 和 Block 让它们执行。
另外这种小游戏的难度一定是要随着时间增加而增大的,所以 Block 的下落时间要越来越快。
下面我们来修改 Block,Player,Stage 三个脚本
Block.ts
const {ccclass} = cc._decorator;
@ccclass
export class Block extends cc.Component {
public init(fallDuration: number, fallHeight: number, destroyTime: number, destroyCb: Function) {
this.scheduleOnce(() => {
let fallAction = cc.moveBy(fallDuration, cc.v2(0, -fallHeight)); // 下沉动作
this.node.runAction(fallAction);
destroyCb();
}, destroyTime);
}
}
这里补充了 Block 的 init 方法,传入了四个参数,分别是坠落动作的持续时间,坠落动作的高度,销毁时间,销毁的回调函数。
scheduleOnce 是一个一次性定时函数,存在于 cc.Component 里,所以你可以在脚本里直接通过 this 来调用这个函数, 这里要实现的效果就是延迟 destroyTime 时间执行下落动作。
Player.ts
const {ccclass} = cc._decorator;
@ccclass
export class Player extends cc.Component {
private stepDistance: number; // 一步跳跃距离
private jumpHeight: number; // 跳跃高度
private jumpDuration: number; // 跳跃持续时间
private fallDuration: number; // 坠落持续时间
private fallHeight: number; // 坠落高度
public canJump: boolean; // 此时是否能跳跃
public index: number; // 当前跳到第几格
public init(stepDistance: number, jumpHeight: number, jumpDuration: number, fallDuration: number, fallHeight: number) {
this.stepDistance = stepDistance;
this.jumpHeight = jumpHeight;
this.jumpDuration = jumpDuration;
this.fallDuration = fallDuration;
this.fallHeight = fallHeight;
this.canJump = true;
this.index = 0;
}
…
public die() {
this.canJump = false;
let dieAction = cc.moveBy(this.fallDuration, cc.v2(0, -this.fallHeight));
this.node.runAction(dieAction);
}
}
首先将 init 里多传入两个变量 fallDuration 和 fallHeight 用来实现下落动作,然后补充 die 方法,这里的下落动作其实是个上面的 Block 里的下落动作是一样的。
Stage.ts
…
@property(cc.Integer)
private fallHeight: number = 500;
@property(cc.Float)
private fallDuration: number = 0.3;
@property(cc.Float)
private initStayDuration: number = 2; // 初始停留时间
@property(cc.Float)
private minStayDuration: number = 0.3; // 最小停留时间,不能再快了的那个点,不然玩家就反应不过来了
@property(cc.Float)
private speed: number = 0.1;
private stayDuration: number; // 停留时间
…
public init(game: Game) {
this.game = game;
this.stayDuration = this.initStayDuration;
this.player.init(this.stepDistance, this.jumpHeight, this.jumpDuration, this.fallDuration, this.fallHeight);
this.blockList = [];
this.addBlock(cc.v2(0, 0));
for (let i = 0; i < 5; i++) {
this.randomAddBlock();
}
}
public addSpeed() {
this.stayDuration -= this.speed;
if (this.stayDuration <= this.minStayDuration) {
this.stayDuration = this.minStayDuration;
}
cc.log(this.stayDuration);
}
public playerJump(step: number) {
if (this.player.canJump) {
this.player.jump(step);
this.moveStage(step);
let isDead = !this.hasBlock(this.player.index);
if (isDead) {
cc.log(‘die’);
this.scheduleOnce(() => { // 这时还在空中,要等到落到地面在执行死亡动画
this.player.die();
this.game.overGame();
}, this.jumpDuration);
} else {
let blockIndex = this.player.index;
this.blockList[blockIndex].init(this.fallDuration, this.fallHeight, this.stayDuration, () => {
if (this.player.index === blockIndex) {// 如果 Block 下落时玩家还在上面游戏结束
this.player.die();
this.game.overGame();
}
});
this.game.addScore(step === 1 ? 1 : 3);
}
if (this.player.index % 10 === 0) {
this.addSpeed();
}
}
}
…
Player 和 Block 下落动作都需要的 fallDuration 和 fallHeight 我们提取到 Stage 里,然后又添加了几个属性来计算 Block 存留时间。
在 playerJump 方法里,补充了 Player 跳跃后的逻辑:如果 Player 跳空了,那么就执行死亡动画也就是下落动作,如果 Player 跳到 Block 上,那么这个 Block 就启动下落计时器,当 Block 下落时 Player 还没有跳走,那就和 Player 一起掉下去。
最后增加下落速度的方式是每隔十个格子加速一次。
回到编辑器,调整 fallDuration,fallHeight,initStayDuration,minStayDuration,speed 的值。
预览游戏
添加结算面板
前面讲了这么多,相信你能自己拼出下面这个界面。
上面挂载的 OverPanel 脚本如下:
OverPanel.ts
import {Game} from “./Game”;
const {ccclass, property} = cc._decorator;
@ccclass
export class OverPanel extends cc.Component {
@property(cc.Label)
private scoreLabel: cc.Label = null;
private game: Game;
public init(game: Game) {
this.game = game;
}
private onBtnRestart() {
this.game.restartGame();
}
private onBtnReturnMenu() {
this.game.returnMenu();
}
public show(score: number) {
this.node.active = true;
this.scoreLabel.string = score + ”;
}
public hide() {
this.node.active = false;
}
}
不要忘了将两个按钮绑定到对应的方法上。
最后修改 Game, 让游戏结束时显示 OverPanel
Game.ts
import {Stage} from ‘./Stage’;
import {OverPanel} from ‘./OverPanel’;
const {ccclass, property} = cc._decorator;
@ccclass
export class Game extends cc.Component {
@property(Stage)
private stage: Stage = null;
@property(cc.Label)
private scoreLabel: cc.Label = null;
@property(OverPanel)
private overPanel: OverPanel = null;
private score: number = 0;
protected start() {
this.overPanel.init(this);
this.overPanel.hide();
this.startGame();
}
public addScore(n: number) {
this.score += n;
this.scoreLabel.string = this.score + ”;
}
public startGame() {
this.score = 0;
this.scoreLabel.string = ‘0’;
this.stage.init(this);
}
public overGame() {
this.overPanel.show(this.score);
}
public restartGame() {
cc.director.loadScene(‘game’);
}
public returnMenu() {
cc.director.loadScene(‘menu’);
}
private onBtnOne() {
this.stage.playerJump(1);
}
private onBtnTwo() {
this.stage.playerJump(2);
}
}
将 OverPanel 的属性拖上去。
为了不影响编辑器界面,你可以将 OverPanel 节点隐藏
预览效果
添加声音和键盘操作方式
如果你玩过这个游戏,肯定知道声音才是其灵魂。
既然是 Player 发出的声音,就挂在 Player 身上吧
Player.ts
const {ccclass, property} = cc._decorator;
@ccclass
export class Player extends cc.Component {
@property({
type: cc.AudioClip
})
private oneStepAudio: cc.AudioClip = null;
@property({
type:cc.AudioClip
})
private twoStepAudio: cc.AudioClip = null;
@property({
type:cc.AudioClip
})
private dieAudio: cc.AudioClip = null;
…
public jump(step: number) {
…
if (step === 1) {
cc.audioEngine.play(this.oneStepAudio, false, 1);
} else if (step === 2) {
cc.audioEngine.play(this.twoStepAudio, false, 1);
}
}
public die() {
…
cc.audioEngine.play(this.dieAudio, false, 1);
}
}
这里你可能比较奇怪的为什么这样写
@property({
type: cc.AudioClip
})
private oneStepAudio: cc.AudioClip = null;
而不是这样写
@property(cc.AudioClip)
private oneStepAudio: cc.AudioClip = null;
其实上面的写法才是完整写法,除了 type 还有 displayName 等参数可选,当只需要 type 这个参数时可以写成下面那种简写形式,但例外的是有些类型只能写完整形式,不然就会抱警告,cc.AudioClip 就是其一。
在电脑上点击两个按钮很难操作,所以我们添加键盘的操作方式。
Game.ts
import {Stage} from ‘./Stage’;
import {OverPanel} from ‘./OverPanel’;
const {ccclass, property} = cc._decorator;
@ccclass
export class Game extends cc.Component {
…
protected start() {
…
this.addListeners();
}
…
private addListeners() {
cc.systemEvent.on(cc.SystemEvent.EventType.KEY_DOWN, (event: cc.Event.EventKeyboard) => {
if (event.keyCode === cc.macro.KEY.left) {
this.onBtnOne();
} else if (event.keyCode === cc.macro.KEY.right) {
this.onBtnTwo();
}
}, this);
}
}
在游戏初始化的时候通过 cc.systemEvent 注册键盘事件,按左方向键跳一步,按右方向键跳两步。
至此我们的游戏就做完了。
一步两步
如果你有基础,这个游戏并不难,如果这是你的第一篇教程,你可能会很吃力,无论前者后者,遇到任何问题都可以在下方留言,我也会随时更新。
另外不要忘了加 QQ 交流群哦 863758586