Cocos Creator 教程(1)——第一个游戏:一步两步

第一个游戏

这节咱们从头作一个比较有意思的小游戏——一步两步。javascript

下面是最终效果(跳一步得一分,跳两步得三分,电脑端左右键12步):html

一步两步java

写在前面

这是本教程第一个游戏,因此我会讲的详细一点,可是也不能避免遗漏,因此有什么问题你能够先尝试查阅文档本身解决或者在下方留言,后面我会跟进完善。node

另外这个游戏并不是原创,参照的是腾讯的微信小游戏《一步两步H5》,请不要直接搬过去,微信里重复的游戏太多了。git

建立工程

选择空白项目建立工程github

clipboard.png

你能够从这里下载游戏素材,而后将素材导入工程(直接拖进编辑器,或者放在工程目录)算法

也能够从https://github.com/potato47/one-two-step下载完整代码进行参照。chrome

clipboard.png

准备工做作完后,我会把这个游戏制做过程分为若干个小过程,让你体会一下实际的游戏制做体验。

从一个场景跳转到另外一个场景

在res文件夹下新建一个scenes文件夹,而后在scenes里新建两个场景menu和game(右键->新建->Scene)。而后双击menu进入menu场景。windows

clipboard.png

在层级管理器中选中Canvas节点,在右侧属性检查器中将其设计分辨率调整为1280x720,而后将background图片拖入Canvas节点下,并为其添加Widget组件(添加组件->UI组件->Widget),使其充满画布。api

clipboard.png

在Canvas下新建一个Label节点(右键->建立节点->建立渲染节点->Label),而后调整文字大小,并添加标题文字

clipboard.png

咱们知道节点和组件一般是一块儿出现的,带有常见组件的节点在编辑器里能够直接建立,好比刚才的带有Label组件的节点和带有Sprite组件的节点,可是咱们也能够新建一个空节点而后为其添加对应的组件来组装一个带有特殊功能的节点。

新建节点->UI节点下有一个Button,若是你直接建立Button,你会发现它是一个带有Button组件的节点,而且有一个Label的子节点。如今咱们用另外一种方法建立Button:在Canvas右键新建一个空节点,而后为其添加Button组件,这时你会发现按钮并无背景,因此咱们再添加一个Sprite组件,拖入资源中的按钮背景图片,最后添加一个Label子节点给按钮添加上文字。

clipboard.png

下面咱们给这个按钮添加点击事件。

在资源管理器中新建src文件夹用来存放脚本,而后新建一个TypeScript脚本,名字为Menu(注意脚本组件名称区分大小写,这里建议首字母大写)。

clipboard.png

双击用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的设计分辨率也改成1280x720)。

clipboard.png

保存game场景后再回到Menu场景。

Button组件点击后会发出一个事件,这个事件能够跟某个节点上的某个脚本内的某个函数绑定在一块儿。听着有点绕,动手作一遍就会明白这个机制。

首先将Menu脚本添加为Canvas节点的组件,而后在开始按钮的Button组件里添加一个Click Event,将其指向Canvas节点下的Menu脚本里的onBtnStart函数。

clipboard.png

咱们再调整一下Button的点击效果,将Button组件的Transition改成scale(伸缩效果),另外还有颜色变化和图片变化,能够本身尝试。

clipboard.png

最后点击上方的预览按钮,不出意外的话就能够在浏览器中看见预期效果。

clipboard.png

组织代码结构

如今咱们来编写游戏逻辑。

首先我来说一下我看到的一种现象:

不少新手很是喜欢问,“看代码我都能看懂啊,可是要我本身写我就没思路啊”

这时一位经验颇多的长者就会甩给他一句,“多写写就有思路了“

不知道大家发现没有,这居然是一个死循环。

对于一个刚开始学习作游戏的人,首先要了解的是如何组织你的代码,这里我教给你们一个最容易入门的代码结构——单向分权结构(这是我想了足足两分钟的自认为很酷炫的一个名字)

脚本分层:

这个结构最重要的就是“权”这个字,咱们把一个场景中使用的脚本按照“权力”大小给它们分层,权力最大的在最上层且只有一个,这个脚本里保存着它直接控制的若干个脚本的引用,被引用的脚本权力就小一级,被引用的脚本还会引用比它权力更小的脚本,依此类推。

脚本互操做:

  1. 上一层的脚本因为保存着下一层脚本的引用,因此能够直接操做下一层的脚本。
  2. 下一层的脚本由上一层的脚本初始化,在初始化的时候会传入上一层的引用(可选),这样在须要的时候会反馈给上一层,由上一层执行更具体的操做。
  3. 同层的脚本尽可能不要互相操做,统一交给上层处理,同层解耦。
  4. 不可避免的同层或跨层脚本操做可使用全局事件来完成。
  5. 具备通用功能的脚本抽离出来,任意层的脚本均可以直接使用。

写了这么多,但你确定没看懂,如今你能够翻到最上面再分析一下游戏的game场景,如何组织这个场景的脚本结构?

首先,一个场景的根节点会挂载一个脚本,一般以场景名命名,这里就是Game。

而后跳跃的人物也对应着一个脚本Player。

跟Player同层的还应该有Block也就是人物踩着的地面方块。

由于Player和Block之间互相影响而且我想让Game脚本更简洁,因此这里再加一个Stage(舞台)脚原本控制Player和Block。

最终它们的层级关系以下:

  • Game

    • Stage

      • Player
      • Block

上面这些都是咱们的思考过程,下面咱们落实到场景中。

先新建几个脚本

clipboard.png

如今搭建场景,先添加一个跟menu场景同样的全屏背景

clipboard.png

而后添加一个空节点Stage,在Stage下添加一个Player节点和一个Block节点

clipboard.png

在Stage同层添加两个按钮来控制跳一步两步

先添加第一个按钮,根据实际效果调整文字大小(font size)颜色(node color)和按钮的缩放倍数(scale)

clipboard.png

第二个按钮能够直接由第一个按钮复制

clipboard.png

这两个按钮显然是要放置在屏幕左下角和右下角的,可是不一样屏幕大小可能致使这两个按钮的位置跑偏,因此最好的方案是给这两个按钮节点添加Widget组件,让它们跟左下角和右下角保持固定的距离,这里就不演示了,相信你能够本身完成(实际上是我忘录了。。。)

添加一个Label节点记录分数,系统字体有点丑,这里替换成咱们本身的字体

clipboard.png

最后把脚本挂在对应的节点上。

clipboard.png

场景搭建到这里基本完成了,如今能够编写脚本了。

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,scoreLabelscore,而只有前两个变量加上了@property,因此编辑器中只能看到stagescoreLabel

clipboard.png

@property括号里一般要填一个编辑器能够识别的类型,好比系统自带的cc.Label,cc.Node,cc.Sprite,cc.Integer,cc.Float等,也能够是用户脚本类名,好比上面的StagePlayer

回到编辑器,咱们把几个脚本暴露在编辑器的变量经过拖拽的方式指向带有类型组件的节点。

clipboard.png

再把one,two两个按钮分别绑定在game里的onBtnOne,onBtnTwo两个函数上。

clipboard.png

这时咱们已经有了一个简单的逻辑,点击1或2按钮,调用Game里的onBtnOne或onBtnTwo,传递给Stage调用playerJump,再传递给Player调用jump,player就会表现出跳一步仍是跳两步的反应。

点击预览按钮,进行测试:

clipboard.png

你能够按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+鼠标单击进入定义文件查看说明示例。

clipboard.png

再来看Stage,能够看到Player初始化的几个参数是由Stage传递的,而且暴露在了编辑器界面,咱们能够直接在Stage的属性面板调整参数,来直观的编辑动做效果,这也是Creator编辑器的方便之处。

clipboard.png

上面的几个参数是我调整事后的,你也能够适当的修改,保存场景后预览效果。

clipboard.png

动态添加地面和移动场景

显而易见,咱们不可能提早设置好全部的地面(Block),而是要根据Player跳跃的时机和地点动态添加Block,这就涉及到一个新的知识点——如何用代码建立节点?

每个Block节点都是同样的,对于这样相同的节点能够抽象出一个模板,Creator里管这个模板叫作预制体(Prefab),想要一个新的节点时就能够经过复制Prefab获得。

制做一个Prefab很简单,咱们先在res目录下新建一个prefabs目录,而后将Block节点直接拖到目录里就能够造成一个Prefab了。

clipboard.png

你能够双击这个prefab进入其编辑模式,若是以前忘了将Block脚本挂在Block节点上,这里也能够挂在Block的Prefab上。

clipboard.png

有了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拖入对应的属性上。

clipboard.png

playerJump的的最后有一段判断游戏结束的逻辑,以前咱们在player里设置了一个变量index,记录player当前跳到第几格,stage里也有一个数组变量blockList保存着全部格子的信息,当player跳完后判断一下落地点是否有格子就能够判断游戏是否结束。

捋顺上面的逻辑后,你就能够预览一下这个看起来有点样子的游戏了

clipboard.png

地面下沉效果

若是每一步都让玩家想好久,那这个游戏就没有尽头了。如今咱们给它加点难度。

设置的效果是:地面每隔一段时间就会下落,若是玩家没有及时跳到下一个格子就会跟着地面掉下去,为了实现这我的物和地面同时下坠的效果,咱们要让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的值。

clipboard.png

预览游戏

clipboard.png

添加结算面板

前面讲了这么多,相信你能本身拼出下面这个界面。

clipboard.png

上面挂载的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的属性拖上去。

clipboard.png

为了避免影响编辑器界面,你能够将OverPanel节点隐藏

clipboard.png

预览效果

clipboard.png

添加声音和键盘操做方式

若是你玩过这个游戏,确定知道声音才是其灵魂。

既然是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);
    }

}

clipboard.png

这里你可能比较奇怪的为何这样写

@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