植物大戰殭屍這款經典遊戲相信你們都玩過,最近我用原生JS+ES6語法,並經過canvas繪製的方式實現了這個遊戲的一些基本功能,在這裏我會介紹一下實現這個遊戲的心路歷程。javascript
可能又人會問,實現這樣的一個小遊戲難嗎?其實單看每個實現的遊戲模塊來講,這個遊戲的實現難度並不大,難點主要在於將全部遊戲中的模塊如何合理的融合在一塊兒,在實現這個小遊戲的過程當中,我也踩過很多坑,也重寫過一部分遊戲邏輯。寫這篇文章的目的也是爲了記錄和總結本身最近在開發中遇到的一些問題和解決問題的思路。html
不少程序猿可能會說,學習編程不過是爲了賺錢而已,也有人是爲了提升自身的技術?這個有道理的,爲何麼要提升自身的技術呢,其實,最終目的是爲了解決問題,解決用戶的問題。java
既然咱們是使用編程解決問題的人,那麼,學習它的目的就是爲了解決問題,也就是說只要能達到解決問題的深度就能夠了。固然問題的大小不一樣,對語言掌握的程度就有不一樣的要求。所以,在學習編程的過程當中切不可脫離了實際,單純的爲了學習編程語言而學習。git
固然,這裏可能扯的有點遠了,可是主要想說明一下本身寫這個小遊戲的初衷,也是爲了提醒本身,在這個浮躁的時代,不忘初心,方得始終。github
先上張截圖:編程
試玩連接:植物大戰殭屍canvas
在開發這個遊戲的時候,我選擇基於ES6的class的方式抽象了遊戲相關函數,關於這個小遊戲的核心引擎,主要的相關屬性以下:數組
class Game {
constructor () {
...
state: 0, // 遊戲狀態值,初始默認爲 0
state_LOADING: 0, // 準備階段
state_START: 1, // 遊戲開始
state_RUNNING: 2, // 遊戲運行
state_STOP: 3, // 遊戲暫停
state_PLANTWON: 4, // 遊戲結束,玩家勝利
state_ZOMBIEWON: 5, // 遊戲結束,殭屍勝利
canvas: document.getElementById("canvas"), // canvas元素
context: document.getElementById("canvas").getContext("2d"), // canvas畫布
timer: null, // 輪詢定時器
fps: window._main.fps, // 動畫幀數
}
init () { // 初始化函數
let g = this
...
// 設置輪詢定時器
g.timer = setInterval(function () {
// 根據遊戲狀態,在canvas中繪製不一樣遊戲場景
}, 1000/g.fps)
...
}
}
複製代碼
其實核心邏輯很簡單,就是定義一個遊戲引擎主函數,生成一個定時器以每秒60幀的頻率不停在canvas畫布上繪製遊戲場景相關元素,而後在定時器函數中根據當前遊戲狀態(遊戲準備
、遊戲開始
、遊戲運行
、遊戲暫停
、遊戲結束
)來繪製對應遊戲場景。dom
loading
遊戲狀態:遊戲引擎繪製了頁面載入圖片,並添加了一個開始遊戲按鈕 start
遊戲狀態:遊戲開始讀條倒計時,提醒用戶遊戲即將開始 running
遊戲狀態:繪製遊戲運行時所需全部遊戲場景素材 stop
遊戲狀態:遊戲進入暫停階段,遊戲中如生成陽關、殭屍的定時器都將清除,角色動畫處於靜止狀態 gameover
遊戲狀態:分爲玩家得到勝利以及殭屍得到勝利兩種狀況,並分別繪製不一樣遊戲結算畫面編程語言
在這裏我將遊戲中全部可控制的元素都歸於遊戲場景中,而且將這些元素都抽象爲類,方便管理,這裏包括:植物類
、殭屍類
、陽光計分板類
、植物卡片類
、動畫類
,子彈類
。
遊戲場景中最核心的兩個類爲植物類
、殭屍類
,不過在這兩個核心類中都會用到動畫類
,這裏我先介紹一下。
動畫類的做用是將每個角色的不一樣動畫序列保存起來,每隔必定時間切換當前顯示的圖片對象,達到播放動畫的效果。
class Animation{
constructor (role, action, fps) {
let a = {
type: role.type, // 動畫類型(植物、殭屍等等)
section: role.section, // 植物或者殭屍類別
action: action, // 根據傳入動做生成不一樣動畫對象數組
images: [], // 當前引入動畫圖片對象數組
img: null, // 當前顯示動畫圖片
imgIdx: 0, // 當前角色圖片序列號
count: 0, // 計數器,控制動畫運行
fps: fps, // 角色動畫運行速度係數,值越小,速度越快
}
Object.assign(this, a)
}
}
複製代碼
這裏用到的images
,就是經過new Image()
的方式生成並添加到images
中組成的動畫序列:
其中type
和section
用於判斷當前須要加載植物或殭屍、哪個動做所對應動畫序列,count
和fps
用於控制當前動畫的播放速度,而img
用於表示當前所展現的圖片對象,即images[imgIdx]
,其關係相似於如下代碼:
// 在全局定時器中每1/60秒計算一次
// 獲取動畫序列長度
let animateLen = images.length
// 計數器自增
count++
// 設置當前顯示動畫序列號
imgIdx = Math.floor(count / fps)
// 當一整套動畫完成後重置動畫計數器
imgIdx === animateLen - 1 ? count = 0 : count = count
// 設置當前顯示動畫圖片
img = images[imgIdx]
複製代碼
======================== 2017.12.4更新start ============================
class Role{
constructor (obj) {
let r = {
id: Math.random().toFixed(6) * Math.pow(10, 6), // 隨機生成 id 值,用於設置當前角色 ID,區分不一樣角色
type: obj.type, // 角色類型(植物或殭屍)
section: obj.section, // 角色類別(豌豆射手、雙發射手...)
x: obj.x, // x軸座標
y: obj.y, // y軸座標
w: 0, // 角色圖片寬度
h: 0, // 角色圖片高度
row: obj.row, // 角色初始化行座標
col: obj.col, // 角色初始化列座標
isAnimeLenMax: false, // 是否處於動畫最後一幀,用於判斷動畫是否執行完一輪
isDel: false, // 判斷是否死亡並移除當前角色
isHurt: false, // 判斷是否受傷
}
Object.assign(this, r)
}
}
複製代碼
這裏的角色類主要用於抽象植物類和殭屍類的公共屬性,基本屬性包括type
、section
、x
、y
、w
、h
、row
、col
,其中row
和col
屬性用於控制角色在草坪上繪製的橫縱座標(即x
軸和y
軸方向位於第幾個方格),section
屬性用於區分當前角色究竟是哪種,如豌豆射手、雙發射手、加特林射手、普通殭屍。
class Plant{
constructor (obj) {
let p = {
life: 3, // 角色血量
idle: null, // 站立動畫對象
attack: null, // 攻擊動畫對象
bullets: [], // 子彈對象數組
state: 1, // 保存當前狀態值,默認爲1
state_IDLE: 1, // 站立不動狀態
state_ATTACK: 2, // 攻擊狀態
}
Object.assign(this, p)
}
// 繪製方法
draw(cxt) {
// 根據當前植物的狀態,分別繪製正常狀態動畫,以及受傷時的半透明狀態動畫
let self = this
cxt.drawImage(self[stateName].img, self.x, self.y)
}
// 更新當前植物狀態
update() {
// 經過動畫計數器計算出當前植物顯示動畫序列的圖片
}
// 判斷當前植物是否可進入攻擊狀態
canAttack() {
// 經過輪詢殭屍對象數組,判斷處於當前植物同行的殭屍,且進入草坪內時,即開始攻擊殭屍
// 目前僅有三種射手類植物可以使用子彈攻擊,櫻桃炸彈屬於範圍傷害類植物(判斷範圍爲其周圍八個格子內)
// 攻擊成功時,減小對應殭屍血量,並在殭屍血量到達特殊值時,切換其動畫(如瀕死狀態,死亡狀態),在血量爲 0 時,從殭屍對象數組中移除當前殭屍
}
// 射擊方法
shoot() {
// 當前植物攻擊時, bullets 數組添加子彈對象
let self = this
self.bullets[self.bullets.length] = Bullet.new(self)
}
}
複製代碼
植物類的私有屬性包括idel
、attack
、bullets
、state
,其中idel
和attack
爲動畫對象,相信看過上面關於動畫類
介紹的小夥伴應該能理解其做用,bullets
即用於保存當前植物的全部子彈對象(同動畫類
,子彈類
也有屬性、方法的配置,這裏就不詳細敘述了)。
關於植物的狀態控制屬性,如isHurt
屬性會在植物受傷時,切換爲true
,並由此給動畫添加一個透明度,模擬受傷效果;isDel
屬性會在植物血量降爲0時,將植物從植物對象數組中移除,即再也不繪製當前植物;state
屬性用於植物在兩種形態中進行切換,即普通形態、攻擊形態,當前狀態值爲哪一種形態,即播放對應形態動畫,對應關係以下:
state === state_IDLE => // 播放植物普通形態動畫 idle
state === state_ATTACK => // 播放植物攻擊形態動畫 attack
複製代碼
攻擊形態的切換,這裏就涉及須要循環當前植物對象與全部的殭屍對象所組成的數組,判斷是否有殭屍處於當前植物對象的射程內(即處於同一行草坪,且進行屏幕顯示範圍)。
這裏主要介紹了植物類的相關屬性,其方法包括初始化植物對象
、植物繪製
、植物射擊
、更新植物狀態
、檢測植物是否可攻擊殭屍
...
// 殭屍類
class Zombie{
constructor (obj) {
let z = {
life: 10, // 角色血量
idle: null, // 站立動畫對象
run: null, // 奔跑動畫對象
attack: null, // 攻擊動畫對象
dying: null, // 瀕臨死亡動畫對象
die: null, // 死亡動畫對象
state: 1, // 保存當前狀態值,默認爲1
state_IDLE: 1, // 站立不動狀態
state_RUN: 2, // 奔跑狀態
state_ATTACK: 3, // 攻擊狀態
state_DYING: 4, // 瀕臨死亡狀態
state_DIE: 5, // 死亡狀態
canMove: true, // 判斷當前角色是否可移動
attackPlantID: 0, // 當前攻擊植物對象 ID
speed: 3, // 移動速度
}
Object.assign(this, z)
}
// 繪製方法
draw() {
// 根據當前殭屍的狀態,分別繪製正常狀態動畫,以及受傷時的半透明狀態動畫
let self = this
cxt.drawImage(self[stateName].img, self.x, self.y)
}
// 更新當前殭屍狀態
update() {
// 動畫計數器計算出當前植物顯示動畫序列的圖片
}
// 判斷當前殭屍是否可進入攻擊狀態
canAttack() {
// 經過輪詢植物對象數組,判斷處於當前殭屍同行的植物,且進入其攻擊範圍內時,即開始攻擊植物
// 攻擊成功時,當前殭屍 canMove 屬性將爲 false ,記錄其 attackPlantID ,即所攻擊植物 id 值,並減小對應植物血量;
// 在植物血量爲 0 時,切換其動畫(進入死亡狀態),並從植物對象數組中移除該植物,同時
// 將全部攻擊該植物的殭屍的狀態切換爲移動狀態, canMove 屬性值改成 true
}
}
複製代碼
這裏能夠看到殭屍類
的不少屬性與植物類
相似,就不過多敘述了,因爲目前只開發了一種殭屍,因此section
屬性是固定值。
關於殭屍的動畫對象可能會比植物複雜一點,包含idle
、run
、attack
、dying
、die
五種形態的動畫序列,其中dying
和die
對應殭屍較低血量和血量爲0時所播放的動畫。
在殭屍的控制屬性上,與植物同理,這裏殭屍的五種動畫對象也對應五種狀態值,並隨狀態值的切換而切換。
這裏主要介紹了殭屍類的相關屬性,其方法包括初始化殭屍對象
、殭屍繪製
、殭屍攻擊
、更新殭屍狀態
、檢測殭屍是否可攻擊植物
...
======================== 2017.12.4更新end ============================
在遊戲主函數中,將會把以前全部用到的遊戲相關類,進行實例化,並保存在Main
類中,在這裏調用start
遊戲啓動函數,將會開啓遊戲引擎,開始繪製遊戲場景,因此遊戲啓動函數會在頁面加載完成後當即調用。
class Main {
constructor () {
let m = {
allSunVal: 200, // 陽光總數量
loading: null, // loading 動畫對象
sunnum: null, // 陽光實例對象
cards: [], // 實例化植物卡片對象數組
cards_info: { // 初始化參數
x: 0,
y: 0,
position: [
{name: 'peashooter', row: 1, sun_val: 100},
{name: 'repeater', row: 2, sun_val: 150},
{name: 'gatlingpea', row: 3, sun_val: 200},
]
},
plants: [], // 實例化植物對象數組
zombies: [], // 實例化殭屍對象數組
plants_info: { // 初始化參數
type: 'plant', // 角色類型
x: 250, // 初始 x 軸座標,遞增量 80
y: 92, // 初始 y 軸座標,遞增量 100
len: 0,
position: [] // section:植物類別,row:橫行座標(最小值爲 5),col:豎列座標(最大值爲 9)
},
zombies_info: { // 初始化參數
type: 'zombie', // 角色類型
x: 250, // x軸座標
y: 15, // y軸座標
position: [] // section:殭屍類別,row:橫行座標(最小值爲 9),col:豎列座標(最大值爲 13)
},
zombies_idx: 0, // 隨機生成殭屍 idx
zombies_row: 0, // 隨機生成殭屍的行座標
zombies_iMax: 30, // 隨機生成殭屍數量上限
sunTimer: null, // 全局定時器,用於控制全局定時生成陽光
zombieTimer: null, // 全局定時器,用於控制全局定時生成殭屍
game: null, // 遊戲引擎對象
fps: 60, // 動畫幀數
}
Object.assign(this, m)
}
// 遊戲啓動函數
start() {
// 實例化遊戲場景篇中的全部類
}
}
window._main = new Main()
window._main.start()
複製代碼
這裏就簡單介紹下plants
、zombies
對象數組;當遊戲運行時,因此種植的植物以及生成的殭屍都會配合其相關初始化參數plants_info
、zombies_info
進行實例化再分別保存在plants
、zombies
對象數組中。
較以往的經驗來看,關於遊戲中相關的方法邏輯做者就不詳細介紹了,這個分享主要是爲了提供給對小遊戲感興趣,可是殊不知如何下手的小夥伴一個思路和經驗。
若是有小夥伴對遊戲相關代碼有任何疑問,或想了解相關小遊戲的實現邏輯,須要遊戲相關素材,均可以經過如下方式聯繫做者。
博客:www.yangyunhe.me github地址:github.com/yangyunhe36… QQ:314786482 郵箱:yangyunhe369@qq.com