天天看點

HTML5 坦克大戰遊戲的制作思路

代碼

DEMO

不管寫的過程中覺得有多便秘,寫完了回過頭再去看這個遊戲其實并不算多麼的複雜,一些基本的問題處理好就行——這也是這篇文章所想要說明的東西。是以這篇部落格隻能算是記錄了一下寫一個遊戲過程中的一些思路,如果有同學也想要自己寫一個遊戲并不知道如何開始的話,我推薦下面兩個内容:

如何開發一個簡單的HTML5 Canvas 小遊戲

HTML5小遊戲—愛心魚

鍵盤事件觸發問題:

如果需要玩家通過按鍵操控坦克進行運動,很多人第一個想到的應該就是把相應的運動函數綁定到相應按鍵的onkeydown事件之上。

一般來說這麼寫有一個問題,那就是為了防止諸如像老人松手慢導至鍵盤事件多次觸發這種情況,隻有當你按下按鍵到一定的時間以後事件才會連續進行觸發。

這個問題反應到遊戲上就是你的坦克總是要在你按下按鍵後過一段時間才會開始連續運動,非常影響遊戲體驗。

這個問題的解決方法很簡單:

let keyInfo = {};     //按鍵是否被按下的資訊
let aKey = [ ,  ,  ,  ,  ,  ,  ,  ,  ,  , ];      //這裡面的數字是wasdhj等按鍵的鍵值

for (let i = ; i < aKey.length; i++) {
    keyInfo[aKey[i]] = {
        pressed : false
    }
}
           
  • 将按鍵的鍵值作為屬性名,将按鍵狀态儲存到keyInfo對象中,初始值都為false,表明按鍵未按下。
  • 在按下鍵盤上相應的按鍵的時候,通過事件委托直接捕獲到按下按鍵的keyCode也就是鍵值。
  • onkeydown事件觸發以後将keyInfo中對應的屬性設定為true,表明按鍵被按下,在onkeyup事件觸發以後再将keyInfo中對應的屬性設定為false。
  • 最後在遊戲中循環檢測keyInfo中對應按鍵的屬性的真假并執行相應的操作就可以了

路徑問題:

在不提坦克與子彈之間的碰撞問題的前提下,路徑問題基本上就是在确定你的坦克跟子彈(子彈的問題其實更複雜一點,後面再詳細讨論)在地圖上哪裡能走哪裡不能走,雖然這個問題并不是很複雜,但在我看來這個問題可以說是整個遊戲的核心所在,因為後面很多問題都是圍繞着路勁而來。

要搞清楚路勁問題還是先要有一些準備工作:

1、地圖:

遊戲的主界面大小為416*416像素,總共由13*13個32*32像素的區域構成。

遊戲中的坦克,障礙物及獎勵的圖檔的大小都是32*32像素,是以隻要使用一個13*13的數組就能将整個地圖資料給存儲下來了。

障礙物圖檔:

HTML5 坦克大戰遊戲的制作思路

第一關地圖資料:

let mapData[] =[   
    //0代表空白,1代表32*32的磚塊,2代表32*16的磚塊,後面類推
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ],
    [ ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  ,  , ]
];
           

有了地圖資料以後,隻要循環調用drawImage方法就能夠将地圖畫出來了:

for (let i = ; i < ; i++) {
    for(let j = ; j < ; j++){
        //擷取對應的值
        let iData = mapData[][i][j];
        if (iData) {
            //如果擷取的值不為0,那麼開始繪制地圖
            cxt.drawImage(oImg,  * iData, , , , *j, *i, , );
        }
    }
}
           

第一關界面:

HTML5 坦克大戰遊戲的制作思路

2、子彈的初始坐标

HTML5 坦克大戰遊戲的制作思路

其他先不說,看這張圖,最大的那個32px的方塊表示的就是一個坦克,左上角頂點為其坐标,四個8px的小方塊表示四個方向發射的子彈,坐标也是左上角。

很明顯當坦克在發射子彈的時候,子彈的初始坐标必須要根據坦克的坐标及方向進行調整,不然發射的子彈位置就不對了。

//iDir表示坦克的方向,x和y的初始值也是坦克的坐标,這裡需要根據坦克的方向進行調整才是子彈的初始坐标
// 1、3
if (iDir%) {
    y += ;
    x += *(+!(iDir-));
// 0, 2
} else {
    x += ;
    y += *iDir/;
}
           

3、坦克轉換方向後的對齊

大家可以看上面那個第一關的圖檔,很容易就能看出來沒有磚塊的黑色路徑其實跟坦克的寬度是差不多的,當然,他們的寬度都是32個像素。

那麼問題來了,遊戲中玩家的坦克每次循環(一次循環16毫秒到17毫秒不等)會移動兩個像素,除了一開始坦克正好對準了位置以外,以後每次轉換方向,玩家根本沒辦法做到每次都是分毫不差的卡到那個32像素的點上,那麼按照遊戲的一般規定,對不起你前面有障礙物你無法通過。。。

是以,我們需要在坦克每次改變方向之後,都要正好對準這麼一個點,代碼如下:

// 在坦克轉換方向後重新定位坦克的位置,使坦克目前移動方向的左邊正好能夠整除16,這樣就正好對齊了磚塊的契合處
//iDir表示坦克目前的方向02表上下,13表右左
//x表示坦克目前的橫坐标,y表示坦克目前的縱坐标
if (iDir % ) {  
    y = Math.round(y / );
} else {
    x = Math.round(x / ) * ;
}
           

如果仔細看了代碼,可能有人心中就會出現一個疑問,為什麼是要能夠整除16?OK,下面就來回答這個問題。

4、路徑資料

先回過頭來看看上面那張障礙物的圖檔,拿灰褐色的磚塊來說,很明顯可以看到磚塊一共有四種尺寸7張圖,最小是16*16px,最大是32*32px。

想要告訴坦克或者子彈哪裡有障礙物能否通過有兩種方式:

  • 一是将每一種狀态的磚塊都儲存下來,這樣磚塊跟鋼筋加起來共十四種狀态,判斷起來過于麻煩,而且子彈打掉磚塊後的判斷也相應增加了變化的情況。
  • 二是将磚塊都分解為16*16的小磚塊,這樣就不需要判斷磚塊的尺寸了,然後用一個26*26的數組就能夠将整個地圖的路徑情況給記錄下來。

令:其實這裡還有一個方法那就是把障礙物全部分解為16*16的尺寸,這樣地圖資料直接就是路徑資料了。

碰撞檢測問題:

1、具體有哪些碰撞:

HTML5 坦克大戰遊戲的制作思路

所謂的碰撞檢測,按照上面這張遊戲截圖來說明的話主要分為兩類:

  • 坦克的碰撞,這裡面又包括了:
    • 坦克與獎勵的碰撞
    • 坦克與坦克的碰撞
    • 坦克與障礙物的碰撞
  • 子彈的碰撞,這裡面又包括了:
    • 子彈與子彈的碰撞
    • 子彈與坦克的碰撞
    • 子彈與障礙物的碰撞

2、坦克與獎勵、子彈與子彈以及坦克與坦克的碰撞:

①、坦克與獎勵以及子彈與子彈的碰撞的檢測代碼基本上沒啥差別,是以隻舉坦克與獎勵的碰撞來說明:

//坦克與獎勵的碰撞檢測
//坦克的x、y坐标分别減去獎勵的x、y坐标,如果都小于一個坦克的大小32,那麼表明坦克與獎勵已經碰撞
let xVal = Math.abs(tank.x - bonus.x),
    yVal = Math.abs(tank.y - bonus.y);

if (xVal <  && yVal < ) {
    //碰撞了,執行相應的代碼
}
           

如上面代碼所示,他們之間的碰撞檢測主要就是檢查橫縱坐标之差的絕對值,如果這兩個值都小于坦克本身的尺寸,那麼表明他們碰到了一起。

子彈同理,不過是檢測是否小于子彈本身的尺寸8就可以了。

②、表面上看坦克與坦克的碰撞檢測似乎與坦克與獎勵、子彈與子彈的碰撞沒什麼不同,實際上還是有差別的,下面用一張圖說明:

HTML5 坦克大戰遊戲的制作思路

左邊紅色的NPC坦克正在渲染出生的動畫,右邊動畫播放完成坦克開始運動,如果這裡還是像之前那樣去檢測,很明顯兩個坦克已經碰到了一起,接下裡兩個坦克可能就都無法運動了。

那麼坦克之間的碰撞檢測如下:

//同樣是檢查x、y坐标的內插補點的絕對值
let xVal = Math.abs(this.x - tank.x),
let yVal = Math.abs(this.y - tank.y);
//這裡根據方向的不同,檢測的值也不同,這裡的26留下的餘地,如果兩個坦克正好重疊,那麼他們也是可以運動的
//iDir表示坦克目前的方向02表上下,13表右左
if (iDir % ) {
    //iDir的值為1或者3,也就是坦克的方向是左右
    if (xVal <  && xVal >  && yVal < ) {
         //...
    }
} else {
    //iDir的值為0或者2,也就是坦克的方向是上下
    if (yVal <  && yVal >  && xVal < ) {
         //....
    }
}
           

這裡判斷的值之是以為26,拿y坐标來舉例,如之前坦克轉向後對齊裡面的y = Math.round(y / 16)所示,在坦克轉向後坦克坐标是會四舍五入的,因為移動速度最慢的坦克每個循環會移動1px,是以當(y / 16)< n.5的時候,n*16+1px ~ n* 16+7px會被舍棄,最多是6px,這樣檢測值是32-6=26正好能夠讓兩個坦克在重疊後通過轉向可以繼續運動。

當然這裡也會導至一個BUG,那就是某個時候如果我的坦克正好轉向,坐标四舍五入後,有可能會導至兩個坦克重疊,是以這裡也需要在坦克轉換方向後的做一個碰撞檢測,如果正好有重疊那就不往那個方向轉。

3、子彈與坦克的碰撞:

子彈與坦克的碰撞又是另外一回事了,之前也講過子彈的坐标是根據發射子彈的坦克的坐标重新定位過的,是以檢測的判斷條件跟子彈的方向有很大的關系:

let x = bullet.x - oTank.x;
let y = bullet.y - oTank.y;
if (this.iDir % ) {
    return (this.iDir -)
    ? (x <  && x >  && y > - && y < )
    : (x > - && x <  && y > - && y < );
} else {
    return this.iDir
    ? (y > - && y <  && x > - && x < )
    : (y <  && y >  && x > - && x < );
}
           

用方向向上的子彈來舉例:

HTML5 坦克大戰遊戲的制作思路

上面兩個32*32px的正方形表示坦克,下面那個8*8的正方形表示子彈,坦克與子彈的坐标都位于左上角的頂角處。

當坦克的x坐标位于橫着的綠色線條中間之時(-8 <= bullet.x - tank.x <= 32),就表示子彈與坦克在橫坐标上相碰撞了。

當坦克的y坐标位于豎着的綠線區域内時(0 <= bullet.y - tank.y<= 32),表示子彈與坦克在縱坐标上相碰撞了。

兩個條件何在一起,就是方向向上的子彈與坦克的碰撞條件:

y <  && y >  && x > - && x < 
           

4、坦克與障礙物的碰撞:

坦克與障礙物的碰撞實際上就是去判斷最早的那個26*26的路徑數組,看坦克目前方向上所對應的兩個數組所代表的障礙物是否允許坦克通過。

代碼并沒有什麼難度,唯一需要注意的是當坦克的方向是向上跟向左的時候,需要分别将傳入的y與x坐标-1,這是因為你需要判斷的是下一個路徑數組的值,而不是目前。

5、子彈與障礙物的碰撞:

如果說子彈一次能打掉最少打掉的是16*16大小的障礙物的話,想要處理也非常簡單,根據坐标将對應區域給cxt.clearReact掉,再将相應的路徑數組置0就能夠解決。

可惜問題并不是這麼簡單,為什麼複雜呢?看下圖就明白了:

HTML5 坦克大戰遊戲的制作思路

這是坦克在沒有吃掉星星的時候子彈所能打掉的磚塊,通過對比我們能很清晰的看到子彈一次打掉的磚塊是8*32的區域,等于說一個16*16的障礙物,我們得用兩發子彈才能打掉,這就需要對于灰褐色的磚塊進行特殊處理一下了:

let oBrickStatus = {}; //建立一個對象用來儲存被子彈擊中的磚塊的狀态
let iIndex = x/* + y/;  //因為路徑數組的key值是使用x/16與y/16計算而來了,那麼我們将這兩個key值處理一下後得到一個新的數值,這個值用來作為記錄被子彈擊中的磚塊的狀态的key值
//如果oBrickStatus中沒有儲存這個磚塊對應的記錄,那麼将[1, 1, 1, 1]指派給oBrickStatus[iIndex]
//[1, 1, 1, 1]是因為一個16*16的區域正好可以分成4個8*8的區域,是以用這個數組記錄下目前16*16的區域有哪些8*8的區域不存在(不存在的數組值為0)
if (!!oBrickStatus[iIndex]) {
    //這個函數用來計算子彈擊中磚塊後如何進行處理,下面單獨進行介紹
    hitBrick();   
} else{
    oBrickStatus[iIndex] = [, , , ];
    hitBrick();
}
           

經過上面那段代碼,我們隻需要去計算oBrickStatus[iIndex]的值,就将這個磚塊的狀态給儲存了下來,如果以後子彈再打中了這個磚塊,那麼就拿出oBrickStatus[iIndex]的值來檢查就可以了。

HTML5 坦克大戰遊戲的制作思路

如圖就表示了一個16*16的格子中四個數組項的分布情況,左邊表示數組的索引,右邊表示磚塊是否被打掉(1表示存在,0表示否),一開始四個值都是1。

我們拿子彈方向向上來舉例:

子彈向上的時候首先會檢查索引值為2和3的數組項,當這兩個值中間有一個不為0的時候,表明子彈與磚塊碰撞了,那麼使用clearReact清空掉相應的區域,并将索引值為2和3的數組項置0。

如果兩個值都為0,那麼子彈繼續運動,再運動了8個像素後進入了數組項0和1表示的區域,此時再檢查這兩塊區域所代表的是否為0,重複之前的操作。

最後在确定一個16*16的磚塊全部被打掉後,直接将路徑數組中的資料由表示磚塊的1置為0,這樣就實作了子彈對磚塊的擊中後的效果了。

以上就是我對于這個遊戲的一些思考了,這些問題解決後整個遊戲感覺就沒什麼需要注意的地方了,剩下的就是寫了~~~

繼續閱讀