Cocos Creator 教程(1)——第一個遊戲:一步兩步

第一個遊戲

這節我們從頭做一個比較有意思的小遊戲——一步兩步。

下面是最終效果(跳一步得一分,跳兩步得三分,電腦端左右鍵12步):

一步兩步

寫在前面

這是本教程第一個遊戲,所以我會講的詳細一點,但是也不能避免遺漏,所以有什麼問題你可以先嚐試查閱文檔自己解決或者在下方留言,後面我會跟進完善。

另外這個遊戲並非原創,參照的是騰訊的微信小遊戲《一步兩步H5》,請不要直接搬過去,微信裏重複的遊戲太多了。

創建工程

選擇空白項目創建工程

clipboard.png

你可以從這裏下載遊戲素材,然後將素材導入工程(直接拖進編輯器,或者放在工程目錄)

也可以從https://github.com/potato47/one-two-step下載完整代碼進行參照。

clipboard.png

準備工作做完後,我會把這個遊戲製作過程分爲若干個小過程,讓你體會一下實際的遊戲製作體驗。

從一個場景跳轉到另一個場景

在res文件夾下新建一個scenes文件夾,然後在scenes裏新建兩個場景menu和game(右鍵->新建->Scene)。然後雙擊menu進入menu場景。

clipboard.png

在層級管理器中選中Canvas節點,在右側屬性檢查器中將其設計分辨率調整爲1280x720,然後將background圖片拖入Canvas節點下,併爲其添加Widget組件(添加組件->UI組件->Widget),使其充滿畫布。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章