前言
植物大戰殭屍這款經典遊戲相信大家都玩過,最近我用原生JS+ES6語法,並通過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畫布上繪製遊戲場景相關元素,然後在定時器函式中根據當前遊戲狀態(遊戲準備
、遊戲開始
、遊戲執行
、遊戲暫停
、遊戲結束
)來繪製對應遊戲場景。
loading
遊戲狀態:遊戲引擎繪製了頁面載入圖片,並新增了一個開始遊戲按鈕
start
遊戲狀態:遊戲開始讀條倒數計時,提醒使用者遊戲即將開始
running
遊戲狀態:繪製遊戲執行時所需所有遊戲場景素材
stop
遊戲狀態:遊戲進入暫停階段,遊戲中如生成陽關、殭屍的定時器都將清除,角色動畫處於靜止狀態
gameover
遊戲狀態:分為玩家獲得勝利以及殭屍獲得勝利兩種情況,並分別繪製不同遊戲結算畫面
遊戲場景篇
在這裡我將遊戲中所有可控制的元素都歸於遊戲場景中,並且將這些元素都抽象為類,方便管理,這裡包括:植物類
、殭屍類
、陽光計分板類
、植物卡片類
、動畫類
,子彈類
。
遊戲場景中最核心的兩個類為植物類
、殭屍類
,不過在這兩個核心類中都會用到動畫類
,這裡我先介紹一下。
動畫類(Animation)
動畫類的作用是將每一個角色的不同動畫序列儲存起來,每隔一定時間切換當前顯示的圖片物件,達到播放動畫的效果。
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 ============================
角色類(Role)
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
屬性用於區分當前角色到底是哪一種,如豌豆射手、雙發射手、加特林射手、普通殭屍。
植物類(Plant)
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
複製程式碼
攻擊形態的切換,這裡就涉及需要迴圈當前植物物件與所有的殭屍物件所組成的陣列,判斷是否有殭屍處於當前植物物件的射程內(即處於同一行草坪,且進行螢幕顯示範圍)。
這裡主要介紹了植物類的相關屬性,其方法包括初始化植物物件
、植物繪製
、植物射擊
、更新植物狀態
、檢測植物是否可攻擊殭屍
...
殭屍類(Zombie)
// 殭屍類
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