1. 程式人生 > >實現一個跑酷遊戲

實現一個跑酷遊戲

Phaser入門教程
當使用谷歌瀏覽器訪問網頁的時候,如果斷網了,這時候頁面就會現一個跑酷的小遊戲。按下空格,這個遊戲就開始了。
chrome.gif
本篇教程就使用phaser來實現這個遊戲。演示圖如下所示。
預覽.gif
一個簡單的跑酷遊戲需要一個不斷移動的背景層,產生一個運動的效果。然後需要一個主角,我們的小龍。再次是道具,比如這個遊戲的仙人掌和烏鴉。碰到任何一個道具遊戲就結束了。
  
跟之前的教程一樣,我們需要一個場景先載入進度條圖片資源。一個場景載入其它所有資源,同時使用進度條顯示資源的載入進度,載入完成後進入選單場景。

// 引導
game.states.boot = function() {
this.preload = function() { this.load.image('loading', 'assets/image/progress.png'); }, this.create = function() { this.state.start('preloader'); } }

第一個場景任務就是載入進度條圖片,載入完成後切換到第二個場景。我稱它為引導場景。

// 用來顯示資源載入進度
game.states.preloader = function() {
    this.preload = function
() {
this.day = '#FFFFFF'; game.stage.backgroundColor = this.day; var loadingSprite = this.add.sprite((this.world.width - 311) / 2, this.world.height / 2, 'loading'); this.load.setPreloadSprite(loadingSprite, 0); this.load.atlasJSONHash('image', 'assets/image/image.png'
, 'assets/image/image.json'); game.load.bitmapFont('font', 'assets/font/font_0.png', 'assets/font/font.fnt'); this.load.audio('die', 'assets/audio/hit.mp3'); this.load.audio('jump', 'assets/audio/press.mp3'); }, this.create = function() { this.state.start('menu'); } }
  • 這個場景任務是載入所有資源,同時使用第一個場景載入的進度條資源顯示一個載入中的進度條。
  • game.load.bitmapFont是載入一個位圖字型,第一個引數是別名,後面在建立點陣圖字型文字物件的時候需要用到。第二個引數是點陣圖圖片,第三個引數是配套的xml檔案。載入完成後啟動選單場景。
// 遊戲選單
game.states.menu = function() {
    this.create = function() {
        this.day = '#FFFFFF';
        game.stage.backgroundColor = this.day;
        this.land = this.add.tileSprite(0, game.height - 14, game.width, 14, 'image', 'land.png');//新增到螢幕底部
        this.dragon = this.add.sprite(0, 0, 'image', 'stand1.png')//新增小龍
        this.dragon.animations.add('stand', ['stand1.png', 'stand2.png'], 2, true, false);
        this.dragon.animations.play('stand');
        this.dragon.anchor.set(0.5);
        this.dragon.x = this.dragon.width;
        this.dragon.y = game.height - this.dragon.height / 2;
        this.tip = game.add.bitmapText(0, 0, 'font', "點選開始遊戲", 28);
        this.tip.anchor.set(0.5);
        this.tip.x = game.world.centerX;
        this.tip.y = game.world.centerY;
        game.input.onDown.add(this.startGame, this);
    },
    this.startGame = function() {
        this.land.destroy();
        this.dragon.destroy();
        this.tip.destroy();
        game.state.start('start');
    }
}
  • 在這個場景上,我們使用game.stage.backgroundColor給舞臺背景設定了一個白色。同時載入了陸地,和小龍的圖片資源,又給小龍添加了一個動畫。
  • game.add.bitmapText是建立一個點陣圖字型文字物件,前兩個引數是座標。第三個引數是點陣圖要使用的字型資源名稱,之前設定的‘font’。第四個引數是顯示的文字內容。第五個引數是字型大小。它會使用點陣圖資源中的圖片字型來顯示給它設定的文字內容。

先看下這個點陣圖資源,如下圖所示。
點陣圖字型圖片

可以看到只有0-9和一些漢字還有個空格(這個空格圖片上看不出來吧),需要注意的是如果給文字物件設定的文字內容裡包含了點陣圖字型上沒有的字型或符號,那麼執行的時候會出現異常。

選單預覽

  
現在設計最後一個也是最重要的一個場景,就是遊戲場景。

  • 背景層

背景層一直在重複滾動,有一個運動的效果。可以加些雲朵,讓雲朵稍微運動慢些。這樣可以造成近的物體移動快,而遠的物體移動慢的層次感效果。

this.land = this.add.tileSprite(0, game.height - 14, game.width, 14, 'image', 'land.png');
game.physics.arcade.enable(this.land);//對land物件開啟物理引擎
this.land.autoScroll(this.speed, 0);//自動重複滾動
this.land.body.allowGravity = false;//不用重力
this.land.body.immovable = true;//不可移動的,別的物體碰到後會反彈

這裡通過把地面圖片資源載入成一個紋理圖片物件,使用autoScroll來一直滾動,產生一個運動的效果。對這個物件啟動物理引擎。immovable表示產生碰撞後這個物件是不動的,即不受碰撞影響。如果碰撞它的那個物體的這個屬性設為false,並且設定了bounce彈性引數,那麼會發生反向的位移,即反彈。

//----------------------------------雲朵初始化----------------------------------
this.cloudGroup = game.add.group();//雲朵分組,迴圈使用雲朵,新增5個雲朵物件,意思就是一螢幕最多5個雲朵
this.cloudGroup.enableBody = true;//開啟物理引擎
for (var i = 0;i < 5;i++) {
    var cloud = this.add.sprite(game.width, 0, 'image', 'cloud.png');//把雲朵放到螢幕最右邊
    cloud.visible = false;//預設不可見的
    cloud.alive = false;//預設是dead狀態
    this.cloudGroup.add(cloud);
}
this.cloudGroup.setAll('checkWorldBounds',true); //邊界檢測
this.cloudGroup.setAll('outOfBoundsKill',true); //出邊界後自動kill
//----------------------------------雲朵初始化完成----------------------------------

初始化雲朵分組,enableBody = true會使所有新增到這個分組裡的物件開啟物理引擎。這裡雲朵物件預設都是不可見的並且是dead狀態。
cloudGroup.setAll(‘checkWorldBounds’,true)所有云朵都開啟場景邊界檢測。
cloudGroup.setAll(‘outOfBoundsKill’,true)所有云朵都在運動到場景外的時候自動kill掉,轉換為dead狀態。

  • 道具
    道具是非常重要的一個環節,沒有他們,就只剩下一個主角獨舞,遊戲也不會完善。如果設計過難或過於簡單,都會影響整個遊戲的吸引力。
    本遊戲中,只有三種道具,小型的仙人掌、大型的仙人掌、烏鴉。這三種道具邏輯其實一樣,就是在遊戲過程中隨機產生一個指定範圍內的數字,根據這個數字來選擇該出現一個什麼道具。素材裡面能看到,兩種仙人掌都是3種圖片,而烏鴉只有一種,可以使用兩個分組來表示仙人掌。
//----------------------------------小仙人掌初始化----------------------------------
this.smallGroup = game.add.group();//小仙人掌,看素材總共有三組
this.smallGroup.enableBody = true;//開啟物理引擎
//game.height - 35,其中35是仙人掌的圖片高度,意思是放到螢幕最下面
var small1 = this.add.sprite(game.width, game.height - 35, 'image', 'small1.png');//仙人掌預設y座標在螢幕最下方
var small2 = this.add.sprite(game.width, game.height - 35, 'image', 'small2.png');//仙人掌預設y座標在螢幕最下方
var small3 = this.add.sprite(game.width, game.height - 35, 'image', 'small3.png');//仙人掌預設y座標在螢幕最下方
small1.visible = small2.visible = small3.visible = false;//預設不可見的
small1.alive = small2.alive = small3.alive = false;//預設狀態是dead
this.smallGroup.add(small1);
this.smallGroup.add(small2);
this.smallGroup.add(small3);
small1.body.setCircle(small1.width/ 2);
small2.body.setCircle(small2.width/ 2);
small3.body.setCircle(small3.width/ 2);
this.smallGroup.setAll('checkWorldBounds', true); //邊界檢測
this.smallGroup.setAll('outOfBoundsKill', true); //出邊界後自動kill

//----------------------------------小仙人掌初始化完成----------------------------------

//----------------------------------大仙人掌初始化----------------------------------
this.bigGroup = game.add.group();//大仙人掌,看素材,同小仙人掌一樣是三組
this.bigGroup.enableBody = true;//開啟物理引擎
var big1 = this.add.sprite(game.width, game.height - 35, 'image', 'big1.png');//仙人掌預設y座標在螢幕最下方
var big2 = this.add.sprite(game.width, game.height - 35, 'image', 'big2.png');//仙人掌預設y座標在螢幕最下方
var big3 = this.add.sprite(game.width, game.height - 35, 'image', 'big3.png');//仙人掌預設y座標在螢幕最下方
big1.visible = big2.visible = big3.visible = false;//預設不可見的
big1.alive = big2.alive = big3.alive = false;//預設狀態是dead
this.bigGroup.add(big1);
this.bigGroup.add(big2);
this.bigGroup.add(big3);
big1.body.setCircle(big1.width/ 2);
big2.body.setCircle(big2.width/ 2);
big3.body.setCircle(big3.width/ 2);
this.bigGroup.setAll('checkWorldBounds', true); //邊界檢測
this.bigGroup.setAll('outOfBoundsKill', true); //出邊界後自動kill
//----------------------------------大仙人掌初始化完成----------------------------------

//----------------------------------烏鴉初始化----------------------------------
this.bird = this.game.add.sprite(game.width, 0, 'image', 'bird1.png');
this.bird.animations.add('fly', ['bird1.png', 'bird2.png'], 10, true, false);
this.bird.visible = false;//預設不可見
this.bird.alive = false;//預設是dead狀態
this.bird.checkWorldBounds = true;//檢測邊界
this.bird.outOfBoundsKill = true;//出了邊界就變成dead,後面會重新使用
game.physics.arcade.enable(this.bird);
this.bird.body.setCircle(this.bird.height / 2);
//----------------------------------烏鴉初始化完成----------------------------------

這樣3種道具就初始化完成了。關於body.setCircle這個在後面碰撞檢測那裡詳細解釋。其它的程式碼跟前面雲朵那裡很類似。

  • 定時器資源
    通過開啟一個定時器,可以在一個固定的時間間隔後觸發一個動作。比如每隔3秒出現一個雲朵,或是每隔1秒出現一個仙人掌。也可以用定時器來計分。
game.time.events.loop(2800, this.addCloud, this); //利用時鐘事件來新增雲朵
game.time.events.loop(1000, this.addProps, this); //利用時鐘事件來迴圈新增道具
game.time.events.loop(100, this.updateScore, this);//利用時鐘事件更新分數

這裡開啟了3個定時器做不同的事。產生雲朵,新增道具,計分。

  • 看看addCloud方法。
//新增雲層
var cloud = this.cloudGroup.getFirstDead();
if (cloud !== null) {
    var y = game.rnd.between(cloud.height, game.height - this.dragon.height);
    cloud.reset(game.width, y);
    cloud.body.velocity.x = this.speed / 5;
}
  • 使用cloudGroup.getFirstDead()來獲取分組裡第一個狀態是dead狀態的雲朵,隨機一個y座標。reset會重置雲朵到指定的座標,並且修改狀態visiblealive為true(可見的和非dead的)。
  • cloud.body.velocity.x = this.speed / 5;五分一的橫向運動速度會讓雲朵移動慢於陸地的移動,有層次感。
  • 再來看看addProps方法
//新增烏鴉還是仙人掌,隨機來獲取
var random = game.rnd.between(1, 100);
if (this.score >= 100) {//分數大於300後再隨機道具裡新增烏鴉
    if (random > 60) {
        if (this.bird.alive === false) {
            var y = game.rnd.between(this.birdMinY, this.birdMaxY);
            this.bird.reset(game.width, y);
                this.bird.body.velocity.x = this.speed;
                this.bird.animations.play('fly');
        }
    } else if (random > 30) {//大仙人掌
        var big = this.bigGroup.getFirstDead();
            if (big !== null) {
                big.reset(game.width, game.height - big.height);
                big.body.velocity.x = this.speed;
            }
    } else {//小仙人掌
        var small = this.smallGroup.getFirstDead();
            if (small !== null) {
                small.reset(game.width, game.height - small.height);
                small.body.velocity.x = this.speed;
            }
    }
} else {
    if (random < 50) {//小仙人掌
        var small = this.smallGroup.getFirstDead();
            if (small !== null) {
                small.reset(game.width, game.height - small.height);
                small.body.velocity.x = this.speed;
            }
    } else {//大仙人掌
        var big = this.bigGroup.getFirstDead();
            if (big !== null) {
                big.reset(game.width, game.height - big.height);
                big.body.velocity.x = this.speed;
            }
    }
}
  • 判斷當前的分數,如果大於100分,道具會有烏鴉和大小仙人掌,這時40%的機率產生一隻烏鴉,大小仙人掌各30%的機率。
  • 如果分數小於100,道具就只有大小仙人掌,各50%的機率。
  • 下面是updateScore方法。
this.score++;
this.scoreText.text = "" + this.score + "    最高 " + Math.max(this.score, this.topScore);
this.scoreText.x = game.width - this.scoreText.width - 30;

這個是時間間隔最短的定時器觸發的動作。每次觸發分數加1。修改分數提示文字,同時調整下它的顯示位置。因為隨著分數的提升,分數的位數會一直增加(10->100…),那麼文字物件的寬度會增加。

  • 主角
    這個主角我稱之為小龍。
//----------------------------------小龍和分數初始化----------------------------------
this.dragon = this.add.sprite(0, 0, 'image', 'stand1.png')//新增小龍
game.physics.arcade.enable(this.dragon);
this.dragon.anchor.set(0.5);
this.dragon.x = this.dragon.width;
this.dragon.y = game.height - this.dragon.height / 2;
this.dragon.body.collideWorldBounds = true;
this.dragon.animations.add('die', ['die.png'], 1, true, false);
this.dragon.animations.add('run', ['run1.png', 'run2.png'], 10, true, false);
this.dragon.animations.add('down_run', ['down_run1.png', 'down_run2.png'], 10, true, false);
this.dragon.animations.play('run');
this.dragon.body.setCircle(this.dragon.width / 2);
this.dragon.downRun = false;//是否在趴著跑
this.dragon.isJumping = false;//記錄是否正在跳躍
  • 小龍有3個動畫,一個是死亡時的,動畫名稱叫‘die’,只有一個動畫幀。一個是跑動時的動畫,這個動畫名稱‘run’,這個動畫有兩幀,即兩幅圖片。還有一個是趴下跑的動畫,名稱叫‘down_run’,也是兩幀。遊戲一開始,預設播放的是跑動動畫。
  • 小龍有一個跳躍動作,有一個趴著跑的動作,這兩個是衝突的。即趴著跑的時候不允許跳躍。
  • 給小龍設定this.dragon.body.collideWorldBoundstrue,會檢測場景邊緣,當小龍落到陸地上的時候不至於直接穿過陸地。因為沒有設定彈性引數,所以小龍碰撞到陸地的時候不會反彈。
  • 這裡給小龍新增兩個屬性(JavaScript的方便之處,想添加個屬性直接新增),this.dragon.downRun記錄當前是不是在趴著跑,this.dragon.isJumping記錄當前是不是在跳躍過程中。

處理小龍的跳躍。

    this.jump = function() {//跳躍方法
        if (this.gameOver || this.dragon.isJumping || this.dragon.downRun) {
            return;
        }
        this.dragon.isJumping = true;//修改小龍狀態為在跳躍種
        this.jumpAudio.play();//播放跳躍聲音
        this.jumpTween = game.add.tween(this.dragon).to({//跳躍的運動
            y : this.dragon.y - 90
        }, 300, Phaser.Easing.Cubic.InOut, true);
        this.jumpTween.yoyo(true, 0);//反過來運動回來
        this.jumpTween.onComplete.add(this.jumpOver, this, 0, this.dragon);//運動結束回掉
    },
    this.jumpOver = function() {//跳躍完成
        this.dragon.isJumping = false;
    },
  • jump方法是小龍跳躍的方法,跳躍完成的方法是jumpOver。
    小龍跳躍是有條件約束的。首先是遊戲沒有結束,其次是小龍當前狀態不是在跳躍中,也沒有在趴著跑的狀態。
  • 小龍的跳躍是用位移實現的this.jumpTween,這個位移的最終目標地址是小龍現在的y座標減去90個畫素,意思就是垂直跳起,然後再反向運動回來,這樣一次來回運動完成後就算跳躍結束。
  • 這個位移結束後的回掉函式就是jumpOver,跳躍結束,修改小龍的當前狀態this.dragon.isJumping為false,表示不是在跳躍中。
  • 輸入處理
    這裡說輸入就是點選或是按鍵。
game.input.onDown.add(this.jump, this); //給滑鼠按下事件繫結龍的跳躍動作
this.spaceKey = game.input.keyboard.addKey(Phaser.KeyCode.SPACEBAR);
this.upKey = game.input.keyboard.addKey(Phaser.KeyCode.UP);
game.input.keyboard.addKeyCapture([Phaser.KeyCode.SPACEBAR, Phaser.KeyCode.UP]);
  • 預設點選的時候觸發小龍的jump方法。
  • 添加了空格、上兩個按鍵的監聽。
  • 沒有新增下方向鍵的監聽,可以在下面看到它的處理跟其它兩個鍵的處理不一樣,但是效果一樣。至於喜歡哪種處理方式看個人愛好。

下面看下這三個按鍵的處理。

if (this.spaceKey.isDown || this.upKey.isDown) {
    this.jump();
}
if (game.input.keyboard.isDown(Phaser.KeyCode.DOWN) && this.dragon.downRun === false) {
    this.dragon.downRun = true;
    this.dragon.animations.stop();
    this.dragon.animations.play('down_run');
    this.dragon.body.setSize(this.dragon.width, this.dragon.height / 2, 0, this.dragon.height / 2);
} else if (!game.input.keyboard.isDown(Phaser.KeyCode.DOWN) && this.dragon.downRun === true) {
    this.dragon.downRun = false;
    this.dragon.animations.stop();
    this.dragon.animations.play('run');
    this.dragon.body.setCircle(this.dragon.width / 2);
    this.dragon.body.offset.set(0, 0);//恢復一下偏移為0
}
  • 空格鍵和上方向鍵按下的時候呼叫小龍的jump方法。
  • 下方向鍵按下的時候,先判斷當前小龍是不是處於趴著跑的狀態。如果不是,則修改小龍的狀態為趴著跑,並且播放趴著跑的動畫。
  • 如果下方向鍵沒有按下,但是小龍當前處於趴著跑的狀態,就修改小龍的狀態為正常狀態,並播放正常跑動的動畫。
  • this.dragon.body.setSizethis.dragon.body.setCircle在下面碰撞檢測的地方解釋。
  • 碰撞檢測
    這個遊戲的靈魂就是碰撞檢測。
if (game.physics.arcade.overlap(this.dragon, this.smallGroup) //跟小仙人掌碰撞檢測
    || game.physics.arcade.overlap(this.dragon, this.bigGroup)//跟大仙人掌碰撞檢測
    || game.physics.arcade.overlap(this.dragon, this.bird)) {//跟烏鴉碰撞檢測
    this.smallGroup.forEach(function(item){
        item.body.stop();
    });
    this.bigGroup.forEach(function(item){
        item.body.stop();
    });
    this.bird.animations.stop();
    this.bird.body.stop();
    this.failed();
}
  • 碰撞檢測方法game.physics.arcade.overlap支援精靈與精靈、精靈與分組、分組與分組之間的檢測。如果發生碰撞,方法返回true。
  • 一旦發生碰撞,立即停止遊戲。遍歷仙人掌分組,停止所有仙人掌的移動。停止烏鴉的動畫與位移(如果精靈的alive狀態是false,即dead,則會忽略)。
  • 最後呼叫遊戲結束的方法this.failed
this.failed = function() {//遊戲結束
    this.die.play();//播放遊戲結束的音樂
    if (this.dragon.isJumping) {
        this.jumpTween.stop();//如果正在跳躍,停止
    }
    this.dragon.animations.stop();//停止在播放的動畫
    this.dragon.animations.play('die');//切換到死亡動畫
    this.gameOver = true;//遊戲結束
    game.time.removeAll();//停止所有定時器
    this.cloudGroup.forEach(function(item){//停止雲層運動
        item.body.stop();
    });
    game.input.onDown.remove(this.jump, this);//移除之前點選事件
    game.input.onDown.add(this.gameStart, this); //新增新的點選事件
    localStorage.setItem("run_topScore", Math.max(this.score, this.topScore));//儲存最高分
    this.land.stopScroll();//陸地停止運動
    this.over = this.add.sprite(0, 0, 'image', 'gameover.png');//gameover
    this.over.anchor.set(0.5);
    this.over.x = game.world.centerX;
    this.over.y = game.world.centerY - 30;
    this.restart = this.add.sprite(0, 0, 'image', 'restart.png');//restart button
    this.restart.anchor.set(0.5);
    this.restart.x = game.world.centerX;
    this.restart.y = game.world.centerY + this.over.height;
    this.restart.inputEnabled = true;//允許點選
    this.restart.input.useHandCursor = true;//滑鼠移動上去顯示一個手的形狀
    this.restart.events.onInputDown.add(this.gameStart, this);//點選事件
}
  • failed方法裡面停止雲朵、陸地的運動,修改狀態為遊戲結束,停止所有定時器等等不適合放到update方法裡來處理的都放在這裡處理。
  • 新增一個gameover的圖片和一個restart的圖片。
  • 移除之前的點選事件,新增新的點選事件就是重新啟動這個場景。同時restart圖片的點選事件也是重新啟動這個場景。
  • 重新啟動這個場景意味著重新開始遊戲。

現在來講講之前對精靈body屬性上的處理。

  • 一個精靈只有開啟物理引擎檢測以後,這個body屬性才有了值,不再是undefined。
  • 精靈的區域預設是整個圖片,這個做碰撞檢測大部分時候是非常不適合的,所以需要調整。

使用之前文章講過的除錯技巧,我們把跑動時候的小龍、烏鴉和仙人掌的body區域畫出來看看。
碰撞檢測1.gif

相信大家一看就明白,用這麼大的區域做碰撞檢測可玩性會打折扣,因為空白地方太多了,顯然在玩家覺得不應該碰撞的時候卻發生了碰撞(圖片的空白區域碰撞了)。

來調整下看看效果。

this.dragon.body.setCircle(this.dragon.width / 2);//調整小龍的body為寬的一半為半徑的圓。

碰撞檢測.gif

對三個都做過調整後發現好多了,仙人掌下面部分碰不到的,所以只需要上面這部分。

那當小龍趴著跑的時候怎麼調整呢?
碰撞檢測2.gif
觀察下小龍趴著跑的圖片,發現大概是站著的時候一半的高度。所以這樣調整下。

this.dragon.body.setSize(this.dragon.width, this.dragon.height / 2, 0, this.dragon.height / 2);

碰撞檢測3.gif

  • this.dragon.body.setSize會修改小龍的body區域,前兩個引數是寬高,這裡修改為高度的一半,後兩個引數是座標軸上的偏移,x軸上沒有偏移,y軸上向下偏移了高度的一半。
  • 從趴著跑恢復到正常跑動狀態的時候需要把偏移恢復了,不然圖片位置會出現問題。

大家可以新增一些積極的道具比如增加分數、新增金幣等等。
這個遊戲教程到這裡就基本講完了。後面可能會出一個型別rpg的遊戲教程。
本次教程原始碼地址:
碼雲
github