一.設計json
二.建立框架類canvas
微信小遊戲中game.js和game.json是必備的兩個文件。設計模式
首先建立js文件夾中存放base、player、和runtime三個文件夾用來存放相關類,以及一個導演類。數組
1.base瀏覽器
base中存放爲基本類,包括變量緩衝器(DataStore)變量緩存器,方便咱們在不一樣的類中訪問和修改變量。資源文件加載器(ResourceLoader),確保canvas在圖片資源加載完成後才進行渲染。Resources類,以及精靈類(Sprite)精靈的基類,負責初始化精靈加載的資源和大小以及位置。緩存
2.player微信
player中存放與玩家發生交互的類。包括小鳥類(Birds),計分器類(Score),開始按鈕類(StartButton)。微信開發
3.runtimeapp
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 }
四.主體開發
一、導演類單例開發
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 * 第2、三個參數是圖片剪裁起始位置 x.y軸 7 * 第4、五個參數是被剪裁的圖片的寬度,即剪多大 8 * 第6、七個參數是放置在畫布上的位置,圖形的左上角 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的全部邏輯就已經實現了。下面要進行的是在微信開發者工具上的遷移。
持續更新