微信小游戲flappy bird開發詳解


一.設計

 

二.創建框架類

微信小游戲中game.js和game.json是必備的兩個文件。

首先創建js文件夾中存放base、player、和runtime三個文件夾用來存放相關類,以及一個導演類。

1.base

base中存放為基本類,包括變量緩沖器(DataStore)變量緩存器,方便我們在不同的類中訪問和修改變量。資源文件加載器(ResourceLoader),確保canvas在圖片資源加載完成后才進行渲染。Resources類,以及精靈類(Sprite)精靈的基類,負責初始化精靈加載的資源和大小以及位置。

2.player

player中存放與玩家發生交互的類。包括小鳥類(Birds),計分器類(Score),開始按鈕類(StartButton)。

3.runtime

runtime類存放與游戲進行有關的類,背景類(BackGround),陸地類(Land)不斷移動的陸地,上半部分障礙物類(UpPencil)這里是鉛筆和下半部分鉛筆類(DownPencil)。

之外js中還包括一個導演類(Director),用來控制游戲的邏輯。

外層還有一個main.js,初始化整個游戲的精靈,作為游戲開始的入口。

此時目錄列表如下:

 

 三. 導入圖片文件

資源類resources:

1 /*創建一個數組 background對應的是相應的資源*/
2 export const Resources = [
3     ['background', 'res/background.png'],
4     ['land', 'res/land.png'],
5     ['pencilUp', 'res/pie_up.png'],
6     ['pencilDown', 'res/pie_down.png'],
7     ['birds', 'res/birds.png'],
8     ['startButton', 'res/start_button.png']
9 ]

資源文件加載器 resourceloader:

 1 //資源文件加載器,確保canvas在圖片資源加載完成后才進行渲染
 2 import {Resources} from "./Resources.js";
 3 
 4 export class ResourceLoader {
 5 
 6     constructor() {
 7         //直接this.map自動創建對象
 8         /*Map是一個數據類型,實質上是一個鍵值對,前面是名后面是值,
 9         可以通過set的方法來設置  m.set(o,'content')
10         也可以直接傳入一個數組來設置,這里傳入Resource數組*/
11         this.map = new Map(Resources);
12         for (let [key, value] of this.map) {
13             //將map里的value替換,將相對路徑替換為圖片image本身
14             const image = new Image();
15             image.src = value;
16             this.map.set(key, image);
17         }
18     }
19 
20     /*確保所有圖片加載完畢*/
21     onLoaded(callback) {
22         let loadedCount = 0;
23         for (let value of this.map.values()) {
24             value.onload = () => {
25                 //this指向外部的實力對象
26                 loadedCount++;
27                 if (loadedCount >= this.map.size) {
28                     callback(this.map)
29                 }
30             }
31         }
32     }
33 
34     //靜態工廠
35     static create(){
36         return new ResourceLoader();
37     }
38 }

四.主體開發

1、導演類單例開發

DircDirector類:

 1 //導演類,控制游戲的邏輯
 2 /*單例模式,是一種常用的軟件設計模式。在它的核心結構中只包含一個被稱為單例的特殊類。
  通過單例模式可以保證系統中,應用該模式的類一個類只有一個實例。即一個類只有一個對象實例
3 */ 4 export class Director { 5 6 //驗證單例成功 即只可以有一個實例 7 constructor(){ 8 console.log('構造器初始化') 9 } 10 11 /*使用getInstance方法為定義一個單例對象,如果實例創建了則返回創建類 12 若沒有創建則創建instance*/ 13 static getInstance() { 14 if (!Director.instance) { 15 Director.instance = new Director(); 16 } 17 return Director.instance; 18 } 19 }

我們可以通過主體函數Main.js中驗證是否導演類為單例。如下:

 1 import {ResourceLoader} from "./js/base/ResourceLoader.js";
 2 import {Director} from "./js/Director.js";
 3 
 4 export class Main {
 5     constructor() {
 6         this.canvas = document.getElementById('game_canvas');
 7         this.ctx = this.canvas.getContext('2d');
 8         const loader = ResourceLoader.create();
 9         loader.onLoaded(map => this.onResourceFirstLoaded(map))
10 
11         Director.getInstance();
12         Director.getInstance();
13         Director.getInstance();
14 
15     }
16 
17     onResourceFirstLoaded(map) {
18         console.log(map)
19     }
20 }

我們可以看到在主體函數中我們調用了三次導演類的構造函數,而瀏覽器中的顯示為下,說明只是創建了一個類,而之后則是反復調用之前的實例。

 2.canvas添加圖片示例

 1 let image = new Image();
 2 image.src='../res/background.png';
 3 
 4 image.onload = () => {
 5             /*第一個參數是image對象,要渲染的一張圖
 6             * 第二、三個參數是圖片剪裁起始位置 x.y軸
 7             * 第四、五個參數是被剪裁的圖片的寬度,即剪多大
 8             * 第六、七個參數是放置在畫布上的位置,圖形的左上角
 9             * 第八九個參數是要使用的圖片的大小*/
10             this.ctx.drawImage(
11                 image,
12                 0,
13                 0,
14                 image.width,
15                 image.height,
16                 0,
17                 0,
18                 image.width,
19                 image.height,
20             );
21         }

 具體事項見另一片填坑隨筆里。

(這是一個示例圖片加載的一個代碼,並不是項目代碼)

 

3.基礎精靈類的封裝和靜態背景的實現

精靈類:

寫一個構造函數,包括的繪制圖片的相關參數,並把這些值附到這個類的原型鏈上。

再在精靈類中寫一個draw函數用來繪制圖像,在函數中通過this.ctx.drawImage具體方法來進行繪制,並傳入相關參數。

 1 //精靈的基類,負責初始化精靈加載的資源和大小以及位置
 2 export class Sprite {
 3 
 4     /*
 5      * img 傳入Image對象
 6      * srcX 要剪裁的起始X坐標
 7      * srcY 要剪裁的起始Y坐標
 8      * srcW 剪裁的寬度
 9      * srcH 剪裁的高度
10      * x 放置的x坐標
11      * y 放置的y坐標
12      * width 要使用的寬度
13      * height 要使用的高度
14      */
15     constructor(ctx = null,
16                 img = null,
17                 srcX = 0,
18                 srcY = 0,
19                 srcW = 0,
20                 srcH = 0,
21                 x = 0,
22                 y = 0,
23                 width = 0,
24                 height = 0
25     ) {
26         //    把這些值都附到這個類的原型鏈上
27         this.ctx = ctx;
28         this.img = img;
29         this.srcX = srcX;
30         this.srcY = srcY;
31         this.srcW = srcW;
32         this.srcH = srcH;
33         this.x = x;
34         this.y = y;
35         this.width = width;
36         this.height = height;
37     }
38 
39     /*繪制函數,通過調用具體的drawImage方法來繪制image*/
40     draw(){
41         this.ctx.drawImage(
42             this.img,
43             this.srcX,
44             this.srcY,
45             this.srcW,
46             this.srcH,
47             this.x,
48             this.y,
49             this.width,
50             this.height,
51         );
52     }
53 
54 }

背景類:

背景類繼承自精靈類,所以在構造函數時傳入ctx,image兩個值后在方法中要包括super的構造方法,並傳入精靈類構造方法所需要的參數。

 1 import {Sprite} from "../base/Sprite.js";
 2 
 3 export class BackGround extends Sprite{
 4     constructor(ctx,image){
 5         super(ctx,image,
 6             0,0,
 7             image.width,image.height,
 8             0,0,
 9             window.innerWidth,window.innerHeight);
10     }
11 
12 }

主函數:

在第一次的加載方法中,傳入的map類型數據(里面是鍵值對,相對存放着對應的圖片文件)。

在這個方法中初始化背景圖,並傳入背景類所需要的兩個參數(ctx,map),因為背景類是繼承精靈類的,可以使用精靈類中剛剛寫的draw方法,所以傳入后構造之后,通過background的draw方法即可將背景繪制出來。

1 onResourceFirstLoaded(map) {
2 
3         let background = new BackGround(this.ctx, map.get('background'));
4         background.draw();
5 
6     }

 

4.資源管理器的封裝

實際上  應該把邏輯放在diractor里 初始化的創建放在main里 把所有的數據關聯放在DataStore里。

所以要對上面的背景類進行重新的邏輯封裝,將draw等放在導演類中。

 

首先將數據都放在DataStore類中,DataStore在整個程序中只有一次所以是個單例類,用之前的getinstance創建單例。之后創建一個存儲變量的容器map,寫出put、get、和delate等方法。

 1 //全局只有一個 所以用單例
 2 export class DataStore {
 3 
 4     //單例
 5     static getInstance() {
 6         if (!DataStore.instance) {
 7             DataStore.instance = new DataStore();
 8         }
 9         return DataStore.instance;
10     }
11 
12 //    創建一個存儲變量的容器
13     constructor() {
14         this.map = new Map();
15     }
16 
17     //鏈式操作put
18     put(key, value) {
19         this.map.set(key, value);
20         return this;
21     }
22 
23     get(key) {
24         return this.map.get(key);
25     }
26 
27     //銷毀資源 將資源制空
28     destroy() {
29         for (let value of this.map.value()) {
30             value = null;
31         }
32     }
33 }

然后在main類中先初始化DataStore,在第一次創建時,將不需要銷毀的數據放在單例的類變量中,隨游戲一局結束銷毀的數據放在map中。

在main中,寫一個開始的init方法,把值放在datastore中,用datastore中的put方法將background值放在類中,這時就不用開始用的let background方法了。

傳入之后的繪制圖像,調用導演類中的單例run方法。

 1     onResourceFirstLoaded(map) {
 2 
 3         //初始化Datastore附固定值 不需要每局銷毀的元素放在ctx中 每局銷毀的放在map中
 4         this.datastore.ctx = this.ctx;
 5         this.datastore.res = map;
 6         this.init();
 7 
 8     }
 9     init()
10     {
11         this.datastore
12             .put('background',
13                 new BackGround(this.ctx,
14                     this.datastore.res.get('background')));
15         Director.getInstance().run();
16 
17     }

因為邏輯要放在導演類中,所以創建一個run方法,游戲運行方法。導演類先在構造函數中引入DataStore數據類(注意引入時要加完整的 .js)。

在run方法中,調用背景類的draw。

1     run() {
2         const backgroundSprite = this.datastore.get('background');
3         backgroundSprite.draw();
4     }

這樣就可以實現背景類的繪制了,雖然效果和上面一樣,但是這樣的封裝邏輯更加清晰也更加方便操控。

 

5.代碼優化和代碼封裝

對精靈基類的優化:

將datastore直接傳入精靈類,將draw方法傳入值中傳入相關值,無參數時可以進行默認值的傳入,有具體參數時可以完成方法的重構。

在精靈內創建一個靜態的取image的方法,方便背景函數取背景用。精靈基類如下:

 1 constructor(
 2         img = null,
 3         srcX = 0,
 4         srcY = 0,
 5         srcW = 0,
 6         srcH = 0,
 7         x = 0,
 8         y = 0,
 9         width = 0,
10         height = 0
11     ) {
12         //    把這些值都附到這個類的原型鏈上
13         this.datastore = DataStore.getInstance();
14         this.ctx = this.datastore.ctx;
15         this.img = img;
16         this.srcX = srcX;
17         this.srcY = srcY;
18         this.srcW = srcW;
19         this.srcH = srcH;
20         this.x = x;
21         this.y = y;
22         this.width = width;
23         this.height = height;
24     }
25 
26     //取image static類型的方法在調用時,可以不用訪問類的實例,直接可以訪問類的方法。
27     static getImage(key) {
28         return DataStore.getInstance().res.get(key);
29     }
30 
31     /*繪制函數,通過調用具體的drawImage方法來繪制image*/
32     draw(
33         img = this.img,
34         srcX = this.srcX,
35         srcY = this.srcY,
36         srcW = this.srcW,
37         srcH = this.srcH,
38         x = this.x,
39         y = this.y,
40         width = this.width,
41         height = this.height
42     ) {
43         this.ctx.drawImage(
44             img,
45             srcX,
46             srcY,
47             srcW,
48             srcH,
49             x,
50             y,
51             width,
52             height,
53         );

在背景類中,因為在構造方法super之前無法訪問類的屬性,所以用靜態方法去調用sprite中的getImage方法得到背景圖。

 1 export class BackGround extends Sprite {
 2 
 3     constructor() {
 4 
 5         const image = Sprite.getImage('background');
 6         super(image,
 7             0, 0,
 8             image.width, image.height,
 9             0, 0,
10             window.innerWidth, window.innerHeight);
11     }
12 
13 }

 

 6.canvas運動渲染地板移動

因為地板是勻速運動的精靈類,首先完善land類。land類繼承自sprite類,注意引入時的js問題。

在構造函數中先調出land資源,應用父類sprite時傳入相關參數,這里圖片放置的高度需要注意,因為要放在底部,所以高度的設置為窗口高度減去圖片高度,為起始的高度,這樣就貼合在了底部。(window.innerHeight - image.height,)。此外,還要初始化兩個參數,landX表示地板水平變化的坐標和landSpeed表示變化的速度。

之后再在land類中寫一個繪制的方法,首先因為要避免穿幫,要在圖像移動完之前將圖像重新置位,造成一種地板可以無限延伸的錯覺,所以要先做一個判斷,如果坐標要出界,則重置坐標。之后在super的draw方法中,因為地板是從右往左移動,所以變化的坐標landX也應該是 -landX。代碼如下:

 1 export class Land extends Sprite {
 2 
 3     constructor() {
 4         const image = Sprite.getImage('land');
 5         super(image, 0, 0,
 6             image.width, image.height,
 7             0, window.innerHeight - image.height,
 8             image.width, image.height);
 9 
10         //地板的水平變化坐標
11         this.landX = 0;
12         //地板的水平移動速度
13         this.landSpeed = 2;
14     }
15 
16     draw() {
17         this.landX = this.landX + this.landSpeed;
18         //避免穿幫 ,要達到邊界時,將左邊開頭置回
19         if (this.landX > (this.img.width - window.innerWidth)) {
20             this.landX = 0;
21         }
22         super.draw(this.img,
23             this.srcX,
24             this.srcY,
25             this.srcW,
26             this.srcH,
27             -this.landX,
28             this.y,
29             this.width,
30             this.height)
31     }
32 }

之后再對導演類的邏輯進行相關的處理,首先將地板展現在畫面上,之后通過內置方法使其運動。如下:

1 run() {
2         this.datastore.get('background').draw();
3         this.datastore.get('land').draw();
4         let timer = requestAnimationFrame(() => this.run());
5         this.datastore.put('timer',timer);
6         // cancelAnimationFrame(this.datastore.get('timer'));
7     }

此時界面如下:

 

7.上下鉛筆阻礙

首先先創建一個鉛筆的父類Pencil,繼承自精靈類Sprite。

構造函數傳入image和top兩個參數,這里先說一下top函數的意義。top為鉛筆高度標准點, 上鉛筆top為上鉛筆的最下點 下鉛筆top為最高點加上空開的間隔距離。

然后在構造函數中引入父類構造,傳入相關參數,這里要注意一點是,放置元素的x位置時放在屏幕的最右點,也就是剛剛好放出屏幕看不到的位置。同時寫出top。

在鉛筆類中再寫一個draw方法,因為鉛筆和地板都以相同的速度向后退,所以可以在導演類中的構造中設置一個固定的值moveSpeed=2,鉛筆類中的x為x-speed,這里也注意改一下land中也是這個速度值。然后調用父類方法的draw傳入相關參數。鉛筆類代碼如下:

 1 export class Pencil extends Sprite {
 2 
 3     //top為鉛筆高度 上鉛筆為top為上鉛筆的最下點 下鉛筆top為最高點加上空開距離
 4     constructor(image, top) {
 5         super(image,
 6             0, 0,
 7             image.width, image.height,
 8             //放置位置剛好在canvas的右側,屏幕右側剛好看不到的位置
 9             window.innerWidth, 0,
10             image.width, image.height);
11         this.top = top;
12     }
13 
14     draw() {
15         this.x = this.x - Director.getInstance().moveSpeed;
16         super.draw(this.img,
17             0, 0,
18             this.width, this.height,
19             this.x, this.y,
20             this.width, this.height)
21     }
22 }

這時有了父類,在寫具體的上鉛筆 和 下鉛筆類。上下鉛筆類繼承自鉛筆類,在構造函數傳入top值,取用相關的image圖像,然后用鉛筆類的構造函數,傳入image和top兩個相關參數。

再在上下鉛筆類中寫一個繪制方法draw。方法中確認放置高度this.y,上鉛筆為top-height,下鉛筆為top+gap(間隙),代碼如下:

 1 export class UpPencil extends Pencil {
 2     constructor(top) {
 3         const image = Sprite.getImage('pencilUp')
 4         super(image, top);
 5     }
 6 
 7     // 鉛筆的左上角高度 為top-圖像高度 是一個負值
 8     draw() {
 9         this.y = this.top-this.height;
10         super.draw();
11     }
12 
13     /*下鉛筆為:
14         draw() {
15         //空開的間隙距離為gap
16         let gap = window.innerHeight / 5;
17         this.y = this.top + gap;
18         super.draw();
19     }*/
20 
21 }    

以上便是繪制鉛筆的過程,下面為鉛筆的邏輯相關部分。

 在繪制鉛筆之前,需要創建一組一組(一組兩梗)的鉛筆。而且每組的高度隨機。所以在導演類中創建一個新的方法 createPencil用來創建鉛筆。在此方法中實現控制高度和隨機高度。

屏幕的1/8 1/2分別為最高高度和最低高度。真實高度隨機就可以算出為 Mintop+math.rand()*(maxtop-mintop)。

高度確定后,需要一個數組值來存儲每組鉛筆。在main的put鏈里先輸入鉛筆到數組里。然后在運行邏輯之前創建第一組鉛筆。

在createPencil方法中還需要把上下鉛筆插在鉛筆數組里。所以createPencil方法如下:

1 //創建鉛筆類。有個高度限制,這里取屏幕的2和8分之一,以一個數組的類型存儲。
2     createPencil() {
3         const minTop = window.innerHeight / 8;
4         const maxTop = window.innerHeight / 2;
5         const top = minTop + Math.random() * (maxTop - minTop);
6         this.datastore.get('pencils').push(new UpPencil(top));
7         this.datastore.get('pencils').push(new DownPencil(top));
8     }

然后在run中繪制每一個pancil,pencil在鉛筆數組中,所以需要一個循環。

1 this.datastore.get('pencils').forEach(function (value,) {
2             value.draw();
3         });

此時我們做出來的畫面有一個問題,那就是鉛筆會蓋在地板上面,而且只會出現一組鉛筆。這是和canvas的圖層覆蓋有關系,以及需要判斷屏幕中鉛筆量來重復產生鉛筆。

因為canvas是按順序繪制圖層的,所以要把鉛筆放在地板后面,只需要在run中將鉛筆的繪制放在地板繪制的前面。

其次是鉛筆的重復問題,這里要在run的循環方法中寫兩個判斷,先通過const取出鉛筆數組,數組的第一二個元素就是第一組鉛筆,第三四個元素就是第二組。第一個判斷用來銷毀已經走出屏幕的鉛筆,先判斷如果第一個鉛筆的左坐標加上鉛筆寬度(就是右坐標)在屏幕之外,而且鉛筆數組長度為4時,推出前兩個元素(第一組鉛筆)。推出時用shift方法,shift方法為將數組的第一個元素推出數組並將數組長度減一。

而第二個判斷是創建新的一組鉛筆,當鉛筆走到中間位置時,而且屏幕上只有兩個鉛筆(數組長度為2)時,調用createPencil方法創建一組新的鉛筆。因為run方法不停循環,所以鉛筆也是不斷循環判斷。如下:

 1 run() {
 2         //繪制背景
 3         this.datastore.get('background').draw();
 4 
 5         //數組的第一二個元素就是第一組鉛筆,第三四個元素就是第二組
 6         //先判斷如果第一個鉛筆的左坐標加上鉛筆寬度(就是右坐標)在屏幕之外,
 7         //而且鉛筆數組長度為4時,推出前兩個元素(第一組鉛筆)
 8         //shift方法為將數組的第一個元素推出數組並將數組長度減一
 9         const pencils = this.datastore.get('pencils');
10         if (pencils[0].x + pencils[0].width <= 0 && pencils.length === 4) {
11             pencils.shift();
12             pencils.shift();
13         }
14         //當鉛筆在中間位置時,而且屏幕上只有兩個鉛筆,創建新的一組鉛筆
15         if (pencils[0].x <= (window.innerWidth - pencils[0].width) / 2
16             && pencils.length === 2) {
17             this.createPencil();
18         }
19 
20         //繪制鉛筆組中的鉛筆
21         this.datastore.get('pencils').forEach(function (value,) {
22             value.draw();
23         });
24 
25         //繪制地板
26         this.datastore.get('land').draw();
27 
28         //不斷調用同一方法達到動畫效果,刷新速率和瀏覽器有關,參數為回調函數。
29         let timer = requestAnimationFrame(() => this.run());
30         this.datastore.put('timer', timer);
31         // cancelAnimationFrame(this.datastore.get('timer'));
32     }

 

8.游戲控制邏輯整合

小游戲需要一個整體的開始結束狀態,在main中的初始化中構造一個導演類中的isGameOver屬性,先設置其為false,判斷游戲是否結束的狀態。

然后在導演類中的run方法就使用這個屬性來進行判斷,如果isGameOver是false,就執行run方法下面的具體步驟,如果是ture的話,就停止canvas的刷新,銷毀相關數據,游戲結束。

 

 9小鳥類創建和邏輯分析

首先在Main類中將小鳥志願put進datastore里,在導演類中繪制小鳥類,因為小鳥是最高層,所以在地板層后寫小鳥層。

在小鳥類中,小鳥類繼承自精靈類,構造時先使用原始方法,這是沒有進行圖片剪裁,三種小鳥一起出現在圖像上。所以需要一定的裁剪。

 在裁剪時,首先要給小鳥類添加一些屬性,小鳥的三種狀態需要一個數組來存儲,然后在數組中0,1,2不斷的調用三種狀態,從而使小鳥有飛翔的狀態。所以在構造函數中添加以下屬性:新建起始剪切點的x,y坐標,元素的剪切寬高度,圖像起始時的橫縱坐標,以及要使用的圖像的寬高度。以及記錄狀態和小標的count和index,墜落時間time。

 

 1 constructor() {
 2         const image = Sprite.getImage('birds');
 3         super(image, 0, 0,
 4             image.width, image.height,
 5             0, 0,);
 6 
 7         // 小鳥的三種狀態需要一個數組去存儲
 8         // 小鳥的寬是34 高是24,上下邊距是10,小鳥左右邊距是9
 9         //clippingX開始剪裁的x坐標,clippingWidth是剪切的寬度
10         this.clippingX = [
11             9,
12             9 + 34 + 18,
13             9 + 34 + 18 + 34 + 18];
14         this.clippingY = [10, 10, 10];
15         this.clippingWidth = [34, 34, 34];
16         this.clippingHeight = [24, 24, 24];
17         //起始時小鳥的橫坐標位置,縱坐標位置
18         this.birdX = window.innerWidth / 4;
19         this.birdsX = [this.birdX, this.birdX, this.birdX];
20         this.birdY = window.innerHeight / 2;
21         this.birdsY = [this.birdY, this.birdY, this.birdY];
22         //小鳥的寬高
23         this.birdHeight = 24;
24         this.birdWidth = 34;
25         this.birdsWidth = [this.birdWidth, this.birdWidth, this.birdWidth];
26         this.birdsHeight = [this.birdHeight, this.birdHeight, this.birdHeight];
27         //小鳥在飛動的過程只有y坐標在有變化,y為變化y坐標
28         this.y = [this.birdY, this.birdY, this.birdY];
29         //count計小鳥狀態 index為角標,time小鳥下落時間
30         this.index = 0;
31         this.count = 0;
32         this.time = 0;
33     }

同時小鳥類需要重新寫繪制方法,因為在繪制是要不停的在小鳥數組中循環,以達到飛行的效果,首先初始化一個speed為1,然后 this.count = this.count + speed,這樣每次刷新繪制時,count都會加上速度,count為小鳥不同的狀態,這時還需要做一個判斷,如果角標大於等於2了,說明已經到了最后一個狀態,令count置0,回到最初的狀態。令角標index等於count,這時小鳥就會隨着刷新的頻率來循環數組。

這時看效果會發現小鳥刷新的速度過快,所以需要降低speed的值,但是因為小鳥是數組存儲,如果角標是小數那么小鳥就不會繪制出來,會出現閃動的情況,所以在給角標賦值的時候采用Math.floor去掉小數向下取整。然后傳入相關參數進行繪制。

 1 draw() {
 2         //切換三只小鳥的速度
 3         const speed = 0.15;
 4         this.count = this.count + speed;
 5         //0,1,2
 6         if(this.index>=2){
 7             this.count=0;
 8         }
 9         //減速器的作用,向下取整
10         this.index=Math.floor(this.count);
11 
12         super.draw(
13             this.img,
14             this.clippingX[this.index],
15             this.clippingY[this.index],
16             this.clippingWidth[this.index],
17             this.clippingHeight[this.index],
18             this.birdsX[this.index],
19             this.birdsY[this.index],
20             this.birdsWidth[this.index],
21             this.birdsHeight[this.index]
22         );

這時小鳥開始飛行了,但是是直線飛行,而且沒有碰撞,沒有下墜。 

先做出小鳥下墜的重力加速度。下墜位移為s=1/2gt^2;初始化重力加速度g(之后發現下降太快,除2.4),小鳥的位移為 const offsetY = (g * this.time * this.time) / 2; 做一個循環使繪制的y坐標為本來y坐標加變化的y坐標。時間自增。

設置一個初始向上的速度offsetUp,位移公式為s=vt+1/2g*t^2。這時小鳥會有個上飛的動作再下落。

 1         //模擬重力加速度 。重力位移 1/2*g*t^2
 2         const g = 0.98 / 2.4;
 3         //設置一個向上的加速度
 4         const offsetUp = 7;
 5         //小鳥的位移
 6         //const offsetY = (g * this.time * (this.time-offsetUp)) / 2;
 7         //位移公式為s=vt+1/2g*t^2
 8         const offsetY=(g*this.time*this.time)/2-offsetUp*this.time;
 9 
10         for (let i = 0; i <= 2; i++) {
11             this.birdsY[i] = this.y[i] + offsetY;
12         }
13         this.time++;

 這里設置一個向上的初速度,是為了小鳥飛行更加自然,每當有觸摸屏幕事件時,設置剪切小鳥圖像放置的y坐標為此時的y坐標,而反應在屏幕上,則是點擊屏幕一下,小鳥向上飛一個速度再下墜。

 

這里開始設計觸摸事件,首先在main中創建registerEvent方法,在main的init方法中使用該方法。在這個方法中添加一個點擊事件,點擊后先消除js事件冒泡,然后進行判斷,如果游戲狀態為結束,則重新調用init初始化新游戲,否則游戲沒有結束,則掉用導演類中的birdsEvent方法。

 1 //注冊事件
 2     registerEvent(){
 3         //用箭頭函數指針指向main,可以取到main中的導演類等
 4         this.canvas.addEventListener('touchstart',e=>{
 5             //屏蔽掉js事件冒泡
 6             e.preventDefault();
 7             //判斷游戲是否結束  如果結束重新開始
 8             if(this.director.isGameOver){
 9                 console.log('游戲重新開始');
10                 this.init();
11             }
12             //游戲沒有結束
13             else{
14                 this.director.birdsEvent();
15             }
16         })
17     }

在導演類中的小鳥事件birdsEvent,不斷刷新三只小鳥,當點擊事件發生,即調用這個方法時,為他們的起始y坐標賦值現在的y坐標,並將下墜的事件重置為0。

1     //小鳥事件,為每只小鳥綁定相應事件
2     birdsEvent() {
3         for (let i = 0; i <= 2; i++) {
4             this.datastore.get('birds').y[i] =
5                 this.datastore.get('birds').birdsY[i];
6         }
7         this.datastore.get('birds').time = 0;
8     }

 

10 小鳥與地板和鉛筆的碰撞

在導演類中創建一個check方法,用來檢測是否有碰撞。方法先取用到的元素小鳥和地板以及鉛筆。

然后在run方法開始時調用check方法,這樣就可以一直檢測是否有碰撞了。

回到check方法,先做小鳥與地板碰撞的邏輯,判斷如果小鳥的左上角y坐標加上小鳥的高度超過了地板的左上角,即與地板發生了碰撞,則設置isGameOver狀態為true,並return停止游戲。

而判斷小鳥與鉛筆是否有撞擊有些復雜,首先需要建立小鳥和鉛筆的邊框模型,即他們的上下左右邊框。上下分別是元素的y坐標和加上高度的值,左右分別是x坐標和加上寬度的值。

在建立鉛筆模型時需要注意一點,因為一個屏幕內有最多四個鉛筆。所以需要做一個循環,遍歷到屏幕中所有的鉛筆。每一次循環,首先先建立鉛筆邊框模型,同上。然后進行判斷小鳥與鉛筆是否撞擊,用方法isStrike,如果判斷為true,則改變游戲狀態isGameOver為true,並return結束游戲。

 1 //判斷小鳥是否有撞擊
 2     check() {
 3         const birds = this.datastore.get('birds');
 4         const land = this.datastore.get('land');
 5         const pencils = this.datastore.get('pencils');
 6 
 7         //地板撞擊判斷
 8         if (birds.birdsY[0] + birds.birdsHeight[0] >= land.y) {
 9             console.log('撞擊地板');
10             this.isGameover = true;
11             return;
12         }
13 
14         //小鳥的邊框模型
15         const birdsBroder = {
16             top: birds.y[0],
17             bottom: birds.y[0] + birds.birdsHeight[0],
18             left: birds.birdsX[0],
19             right: birds.birdsX[0] + birds.birdsWidth[0]
20         };
21 
22         const length = pencils.length;
23         for (let i = 0; i < length; i++) {
24             const pencil = pencils[i];
25             const pencilBorder = {
26                 top: pencil.y,
27                 bottom: pencil.y + pencil.height,
28                 left: pencil.x,
29                 right: pencil.x + pencil.width
30             };
31 
32             if (Director.isStrike(birdsBroder, pencilBorder)) {
33                 console.log('撞到鉛筆');
34                 this.isGameover = true;
35                 return;
36             }
37         }
38     }

這里用到了一個isStrike的方法用來判斷小鳥與鉛筆是否有撞擊,判斷方法為小鳥的左右上下與鉛筆的右左下上是否有碰撞,並返回一個布爾值,方法如下:

 1 //小鳥是否與鉛筆有碰撞
 2     static isStrike(bird, pencil) {
 3         let s = false;
 4         if (bird.top > pencil.bottom ||
 5             bird.bottom < pencil.top ||
 6             bird.right < pencil.left ||
 7             bird.left > pencil.right) {
 8             s = true;
 9         }
10         return !s;
11     }

注意這里的返回邏輯,這里初始化 s = false,如果不做檢測直接 return !s,返回的就是 true 代表撞到鉛筆了。

 

中間檢測的代碼是圖中的區域,意思是當小鳥在這些區域的時候表示沒有碰撞 賦值 s = true,return !s。返回的就是 false 了。

其實這是個反向邏輯,假設是碰撞的,然后看哪些情況是沒有碰撞,如果符合條件就把 s = true,return 的就是 false,剩下的情況就是碰撞了,直接 return true;

 

11.重新開始圖標繪制

在main函數中想datastore中put相關的資源,再startbutton中引入圖片資源,如下:

 1 export class StartButton extends Sprite{
 2     constructor(){
 3         const image=Sprite.getImage('startButton');
 4         super(image,
 5             0,0,
 6             image.width,image.height,
 7             (window.innerWidth-image.height)/2,
 8             (window.innerHeight-image.height)/2.5,
 9             image.width,image.height);
10     }
11 }

在run中的游戲停止的部分加上繪制這張圖片的語句:

1 else {
2             //停止不斷canvas的刷新
3             this.datastore.get('startButton').draw();
4             cancelAnimationFrame(this.datastore.get('timer'));
5             this.datastore.destroy();
6         }

 

 12積分器的構建

先在main里put相關的資源,在分數類中,構造方法時取用ctx實例,初始化分數scoreNumber為0,因為canvas的刷新頻率很快,所以需要一個分數開關,只有當其為true時才可以增加分數。然后在屏幕上繪制出分數。如下:

 1 export class Score {
 2     constructor() {
 3         this.ctx = DataStore.getInstance().ctx;
 4         this.scoreNumber = 0;
 5 
 6         //因為canvas的刷新頻率很快 需要一個加分開關來控制不讓一次加太多分
 7         this.isScore = true;
 8     }
 9 
10     draw() {
11         this.ctx.font = '25px Arial';
12         this.ctx.fillStyle = '#76b8ff';
13         this.ctx.fillText(
14             this.scoreNumber,
15             window.innerWidth / 2,
16             window.innerHeight / 18,
17             1000
18         );
19     }
20 }

然后在導演類中做分數增加邏輯,在每次碰撞遍歷過整租鉛筆后,如果小鳥的左坐標飛過了鉛筆的右坐標並且加分開關為開,說明小鳥飛過了一組鉛筆,應該加分。分數自增。加分之后將加分開關關閉。

1 //加分邏輯
2         if (birds.birdsX[0] > pencils[0].x + pencils[0].width
3         &&score.isScore) {
4             score.isScore=false;
5             score.scoreNumber++;
6         }

而加分邏輯應該在每當銷毀一組鉛筆之后重新打開。

1 if (pencils[0].x + pencils[0].width <= 0 && pencils.length === 4) {
2                 pencils.shift();
3                 pencils.shift();
4                 //重新開啟計分器
5                 this.datastore.get('score').isScore=true;
6             }

到這里flappy bird的所有邏輯就已經實現了。下面要進行的是在微信開發者工具上的遷移。

 

 

 

 

持續更新


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM