1. 先搭架子
整體效果:
點擊開始后進入主場景
左側是植物卡片
右上角是游戲的開始和暫停鍵
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><link rel="stylesheet" href="css/common.css"><link rel="stylesheet" href="css/style.css">
</head>
<body><div id="js-startGame-btn" class="startGame-btn">點擊開始游戲</div><!--主場景--><div class="content-box"><canvas id="canvas" width="1400" height="600"></canvas></div><!--左側植物--><ul class="cards-list"><li class="cards-item" data-section="sunflower"><div class="card-intro"><span>向日葵</span><span>冷卻時間:5秒</span></div></li><li class="cards-item" data-section="wallnut"><div class="card-intro"><span>堅果墻</span><span>冷卻時間:12秒</span></div></li><li class="cards-item" data-section="peashooter"><div class="card-intro"><span>豌豆射手</span><span>冷卻時間:7秒</span></div></li><li class="cards-item" data-section="repeater"><div class="card-intro"><span>雙發豌豆射手</span><span>冷卻時間:10秒</span></div></li><li class="cards-item" data-section="gatlingpea"><div class="card-intro"><span>加特林射手</span><span>冷卻時間:15秒</span></div></li><li class="cards-item" data-section="chomper"><div class="card-intro"><span>食人花</span><span>冷卻時間:15秒</span></div></li><li class="cards-item" data-section="cherrybomb"><div class="card-intro"><span>櫻桃炸彈</span><span>冷卻時間:25秒</span></div></li></ul><!--Start and Pause--><div class="menu-box"><div id="pauseGame" class="contro-btn">暫停</div><div id="restartGame" class="contro-btn">開始游戲</div></div><!--自動生成陽光--><!-- <img class="sum-img systemSun" src="images/sun.gif" alt=""> --><script src="js/common.js"></script><script src="js/scene.js"></script><script src="js/game.js"></script><script src="js/main.js"></script>
</body>
</html>
2. 導入植物/僵尸/陽光...的圖片?
圖片包含:植物cd好的狀態和冷卻期的狀態,植物空閑狀態/攻擊狀態,僵尸包含移動狀態/攻擊狀態/櫻桃炸彈炸的效果, 同時我們提供對外的imageFromPath函數, 用來生成圖片路徑
const imageFromPath = function(src){let img = new Image()img.src = './images/' + srcreturn img
}
// 原生動畫參數
// const keyframesOptions = {
// iterations: 1,
// iterationStart: 0,
// delay: 0,
// endDelay: 0,
// direction: 'alternate',
// duration: 3000,
// fill: 'forwards',
// easing: 'ease-out',
// }
// 圖片素材路徑
const allImg = {startBg: 'coverBg.jpg', // 首屏背景圖bg: 'background1.jpg', // 游戲背景bullet: 'bullet.png', // 子彈普通狀態bulletHit: 'bullet_hit.png', // 子彈擊中敵人狀態sunback: 'sunback.png', // 陽光背景框zombieWon: 'zombieWon.png', // 僵尸勝利畫面car: 'car.png', // 小汽車圖片loading: { // loading 畫面write: {path: 'loading/loading_*.png',len: 3,},},plantsCard: { // 植物卡片sunflower: { // 向日葵img: 'cards/plants/SunFlower.png',imgG: 'cards/plants/SunFlowerG.png',},peashooter: { // 豌豆射手img: 'cards/plants/Peashooter.png',imgG: 'cards/plants/PeashooterG.png',},repeater: { // 雙發射手img: 'cards/plants/Repeater.png',imgG: 'cards/plants/RepeaterG.png',},gatlingpea: { // 加特林射手img: 'cards/plants/GatlingPea.png',imgG: 'cards/plants/GatlingPeaG.png',},cherrybomb: { // 櫻桃炸彈img: 'cards/plants/CherryBomb.png',imgG: 'cards/plants/CherryBombG.png', },wallnut: { // 堅果墻img: 'cards/plants/WallNut.png',imgG: 'cards/plants/WallNutG.png',},chomper: { // 食人花img: 'cards/plants/Chomper.png',imgG: 'cards/plants/ChomperG.png',},},plants: { // 植物 sunflower: { // 向日葵idle: {path: 'plants/sunflower/idle/idle_*.png',len: 18,},},peashooter: { // 豌豆射手idle: {path: 'plants/peashooter/idle/idle_*.png',len: 8,},attack: {path: 'plants/peashooter/attack/attack_*.png',len: 8,},},repeater: { // 雙發射手idle: {path: 'plants/repeater/idle/idle_*.png',len: 15,},attack: {path: 'plants/repeater/attack/attack_*.png',len: 15,},},gatlingpea: { // 加特林射手idle: {path: 'plants/gatlingpea/idle/idle_*.png',len: 13,},attack: {path: 'plants/gatlingpea/attack/attack_*.png',len: 13,},},cherrybomb: { // 櫻桃炸彈idle: {path: 'plants/cherrybomb/idle/idle_*.png',len: 7,},attack: {path: 'plants/cherrybomb/attack/attack_*.png',len: 5,},},wallnut: { // 堅果墻idleH: { // 血量高時動畫path: 'plants/wallnut/idleH/idleH_*.png',len: 16,},idleM: { // 血量中等時動畫path: 'plants/wallnut/idleM/idleM_*.png',len: 11,},idleL: { // 血量低時動畫path: 'plants/wallnut/idleL/idleL_*.png',len: 15,},},chomper: { // 食人花idle: { // 站立動畫path: 'plants/chomper/idle/idle_*.png',len: 13,},attack: { // 攻擊動畫path: 'plants/chomper/attack/attack_*.png',len: 8,},digest: { // 消化階段動畫path: 'plants/chomper/digest/digest_*.png',len: 6,}},},zombies: { // 僵尸idle: { // 站立動畫path: 'zombies/idle/idle_*.png',len: 31,},run: { // 移動動畫path: 'zombies/run/run_*.png',len: 31,},attack: { // 攻擊動畫path: 'zombies/attack/attack_*.png',len: 21,},dieboom: { // 被炸死亡動畫path: 'zombies/dieboom/dieboom_*.png',len: 20,},dying: { // 瀕死動畫head: {path: 'zombies/dying/head/head_*.png',len: 12,},body: {path: 'zombies/dying/body/body_*.png',len: 18,},},die: { // 死亡動畫head: {path: 'zombies/dying/head/head_*.png',len: 12,},body: {path: 'zombies/die/die_*.png',len: 10,},},}
}
3. 場景的塑造
例如:左上角的陽光顯示板, 右側的植物卡片, 小汽車和子彈等等...
先來了解一下Canvas這個標簽, 你可以把它想像成一個畫布,我們可以通過獲取上下文來繪制在畫布上進行繪畫(坐標系如下)?
<canvas id="canvas" width="500" height="500"></canvas><script>let canvas=document.getElementById("canvas")let cxt=canvas.getContext("2d") //畫筆//繪制一個矩形ctx.rect(0,0,100,200)//實心ctx.fill() //描邊ctx.stroke()//為上下文填充顏色cxt.fillStyle="orange"//填充文本ctx.font="700 16px Arial"ctx.fillText("內容",x,y,[,maxWidth])//添加圖片let img=new Image()img.src='myImage.png'cxt.drawImage(img,x,y,width,height)//預加載let img=new Image()img.onload=function(){ctx.drawImage(img,0,0)}img.src='myImage.png'</script>
?
?
陽光顯示板:1. 背景img? 2. 所顯示的陽光總數量 3. 字體大小和顏色
class SunNum{constructor(){let s={img:null,sun_num:window._main.allSunVal, //陽光總數量x:105,y:0,}Object.assign(this,s)}static new(){let s=new this()s.img=imageFromPath(allImg.sunback)return s}draw(cxt){let self=thiscxt.drawImage(self.img,self.x+120,self.y) //用于在Canvas上繪制圖像cxt.fillStyle='black'cxt.font='24px Microsoft YaHei'cxt.fontWeight=700cxt.fillText(self.sun_num,self.x+175,self.y+27)}//修改陽光 !!!!!changeSunNum(num=25){let self=thiswindow._main.allSunVal+=numself.sun_num+=num}
}
左側卡片:當我們使用了一個植物后,它的狀態就會改變, 類似于進入到冷卻時間
class Card{constructor(obj){let c={name:obj.name,canGrow:true,canClick:true,img:null,images:[],timer:null,timer_spacing:obj.timer_spacing,timer_num:1,sun_val:obj.sun_val,row:obj.row,x:0,y:obj.y,}Object.assign(this,c)}static new(obj){let b=new this(obj)b.images.push(imageFromPath(allImg.plantsCard[b.name].img)) b.images.push(imageFromPath(allImg.plantsCard[b.name].imgG)) if(b.canClick){b.img=b.images[0]}else{b.img=b.images[1]}b.timer_num = b.timer_spacing / 1000 //1000ms return b}draw(cxt) {let self = this, marginLeft = 120if(self.sun_val > window._main.allSunVal){self.canGrow = false}else{self.canGrow = true}if(self.canGrow && self.canClick){self.img = self.images[0]}else{self.img = self.images[1]}cxt.drawImage(self.img, self.x + marginLeft, self.y)cxt.fillStyle = 'black'cxt.font = '16px Microsoft YaHei'cxt.fillText(self.sun_val, self.x + marginLeft + 60, self.y + 55)if (!self.canClick && self.canGrow) {cxt.fillStyle = 'rgb(255, 255, 0)'cxt.font = '20px Microsoft YaHei'cxt.fillText(self.timer_num, self.x + marginLeft + 30, self.y + 35)}}drawCountDown(){let self=thisself.timer=setInterval(()=>{ //定時器if(self.timer_num>0){self.timer_num--}else{clearInterval(self.timer)self.timer_num=self.timer_spacing/1000}},1000)}changeState(){let self=thisif(!self.canClick){self.timer=setTimeout(()=> { //延時器self.canClick=true},self.timer_spacing)}}
}
?除草車:當僵尸靠近坐標x(在一定范圍內)的時候,? 就會清除整行僵尸
class Car{constructor(obj){let c={img: imageFromPath(allImg.car),state:1,state_NORMALE:1,state_ATTACK:2,w:71,h:57,x:obj.x,y:obj.y,row:obj.row,}Object.assign(this,c)}static new(obj){let c=new this(obj)return c}draw(game,cxt){let self = thisself.canMove()self.state === self.state_ATTACK && self.step(game)cxt.drawImage(self.img, self.x, self.y)}step(game) {game.state === game.state_RUNNING ? this.x += 15 : this.x = this.x}// 判斷是否移動小車 (zombie.x < 150時)canMove () {let self = thisfor (let zombie of window._main.zombies) {if (zombie.row === self.row) {if (zombie.x < 150) { self.state = self.state_ATTACK}if (self.state === self.state_ATTACK) { if (zombie.x - self.x < self.w && zombie.x < 950) {zombie.life = 0zombie.changeAnimation('die')}}}}}
}
?子彈:例如像豌豆射手就會發射子彈,但是只有在state_RUNNING狀態下, 才會進行觸發
class Bullet{constructor(plant){let b={img: imageFromPath(allImg.bullet),w:56,h:34,x:0,y:0,}Object.assign(this,b)}static new(plant){let b=new this(plant)switch (plant.section) {case 'peashooter':b.x = plant.x + 30b.y = plant.ybreakcase 'repeater':b.x = plant.x + 30b.y = plant.ybreakcase 'gatlingpea':b.x = plant.x + 30b.y = plant.y + 10break}return b}draw(game,cxt){let self=thisself.step(game)cxt.drawImage(self.img,self.x,self.y)}step(game){if(game.state === game.state_RUNNING){this.x+=4}else{this.x=this.x}}
}
?為角色設置動畫
class Animation{constructor (role, action, fps) {let a = {type: role.type, // 動畫類型(植物、僵尸等等)section: role.section, // 植物或者僵尸類別(向日葵、豌豆射手)action: action, // 根據傳入動作生成不同動畫對象數組images: [], // 當前引入角色圖片對象數組img: null, // 當前顯示角色圖片imgIdx: 0, // 當前角色圖片序列號count: 0, // 計數器,控制動畫運行imgHead: null, // 當前顯示角色頭部圖片imgBody: null, // 當前顯示角色身體圖片imgIdxHead: 0, // 當前角色頭部圖片序列號imgIdxBody: 0, // 當前角色身體圖片序列號countHead: 0, // 當前角色頭部計數器,控制動畫運行countBody: 0, // 當前角色身體計數器,控制動畫運行fps: fps, // 角色動畫運行速度系數,值越小,速度越快}Object.assign(this, a)}// 創建,并初始化當前對象static new (role, action, fps) {let a = new this(role, action, fps)// 瀕死動畫、死亡動畫對象(僵尸)if (action === 'dying' || action === 'die') {a.images = {head: [],body: [],}a.create()} else {a.create()a.images[0].onload = function () {role.w = this.widthrole.h = this.height}}return a}/*** 為角色不同動作創造動畫序列*/create () {let self = this,section = self.section // 植物種類switch (self.type) {case 'plant':for(let i = 0; i < allImg.plants[section][self.action].len; i++){let idx = i < 10 ? '0' + i : i,path = allImg.plants[section][self.action].path// 依次添加動畫序列self.images.push(imageFromPath(path.replace(/\*/, idx)))}breakcase 'zombie':// 瀕死動畫、死亡動畫對象,包含頭部動畫以及身體動畫if (self.action === 'dying' || self.action === 'die') {for(let i = 0; i < allImg.zombies[self.action].head.len; i++){let idx = i < 10 ? '0' + i : i,path = allImg.zombies[self.action].head.path// 依次添加動畫序列self.images.head.push(imageFromPath(path.replace(/\*/, idx)))}for(let i = 0; i < allImg.zombies[self.action].body.len; i++){let idx = i < 10 ? '0' + i : i,path = allImg.zombies[self.action].body.path// 依次添加動畫序列self.images.body.push(imageFromPath(path.replace(/\*/, idx)))}} else { // 普通動畫對象for(let i = 0; i < allImg.zombies[self.action].len; i++){let idx = i < 10 ? '0' + i : i,path = allImg.zombies[self.action].path// 依次添加動畫序列self.images.push(imageFromPath(path.replace(/\*/, idx)))}}breakcase 'loading': // loading動畫for(let i = 0; i < allImg.loading[self.action].len; i++){let idx = i < 10 ? '0' + i : i,path = allImg.loading[self.action].path// 依次添加動畫序列self.images.push(imageFromPath(path.replace(/\*/, idx)))}break}}
}
?為植物和僵尸設置不同狀態下的動畫效果
/*** 角色類* 植物、僵尸類繼承的基礎屬性*/
class Role{constructor (obj) {let r = {id: Math.random().toFixed(6) * Math.pow(10, 6), // 隨機生成 id 值,用于設置當前角色 IDtype: obj.type, // 角色類型(植物或僵尸)section: obj.section, // 角色類別(豌豆射手、雙發射手...)x: obj.x, // x軸坐標y: obj.y, // y軸坐標row: obj.row, // 角色初始化行坐標col: obj.col, // 角色初始化列坐標w: 0, // 角色圖片寬度h: 0, // 角色圖片高度isAnimeLenMax: false, // 是否處于動畫最后一幀,用于判斷動畫是否執行完一輪isDel: false, // 判斷是否死亡并移除當前角色isHurt: false, // 判斷是否受傷}Object.assign(this, r)}
}
// 植物類
class Plant extends Role{constructor (obj) {super(obj)// 植物類私有屬性let p = {life: 3, // 角色血量idle: null, // 站立動畫對象idleH: null, // 堅果高血量動畫對象idleM: null, // 堅果中等血量動畫對象idleL: null, // 堅果低血量動畫對象attack: null, // 角色攻擊動畫對象digest: null, // 角色消化動畫對象bullets: [], // 子彈數組對象state: obj.section === 'wallnut' ? 2 : 1, // 保存當前狀態值state_IDLE: 1, // 站立不動狀態state_IDLE_H: 2, // 站立不動高血量狀態(堅果墻相關動畫)state_IDLE_M: 3, // 站立不動中等血量狀態(堅果墻相關動畫)state_IDLE_L: 4, // 站立不動低血量狀態(堅果墻相關動畫)state_ATTACK: 5, // 攻擊狀態state_DIGEST: 6, // 待攻擊狀態(食人花消化僵尸狀態)canShoot: false, // 植物是否具有發射子彈功能canSetTimer: obj.canSetTimer, // 能否設置生成陽光定時器sunTimer: null, // 生成陽光定時器sunTimer_spacing: 10, // 生成陽光時間間隔(秒)}Object.assign(this, p)}// 創建,并初始化當前對象static new (obj) {let p = new this(obj)p.init()return p}// 設置陽光生成定時器setSunTimer () {let self = thisself.sunTimer = setInterval(function () {// 創建陽光元素let img = document.createElement('img'), // 創建元素container = document.getElementsByTagName('body')[0], // 父級元素容器id = self.id, // 當前角色 IDtop = self.y + 30,left = self.x - 130,keyframes1 = [ // 陽光移動動畫 keyframes{ transform: 'translate(0,0)', opacity: 0 },{ offset: .3,transform: 'translate(0,0)', opacity: 1 },{ offset: .5,transform: 'translate(0,0)', opacity: 1 },{ offset: 1,transform: 'translate(-'+ (left - 110) +'px,-'+ (top + 50) +'px)',opacity: 0 }]// 添加陽關元素img.src = 'images/sun.gif'img.className += 'sun-img plantSun' + idimg.style.top = top + 'px'img.style.left = left + 'px'container.appendChild(img)// 添加陽光移動動畫let sun = document.getElementsByClassName('plantSun' + id)[0]sun.animate(keyframes1,keyframesOptions)// 動畫完成,清除陽光元素setTimeout(()=> {sun.parentNode.removeChild(sun)// 增加陽光數量window._main.sunnum.changeSunNum()}, 2700)}, self.sunTimer_spacing * 1000)}// 清除陽光生成定時器clearSunTimer () {let self = thisclearInterval(self.sunTimer)}// 初始化init () {let self = this,setPlantFn = null// 初始化植物動畫對象方法集setPlantFn = {sunflower () { // 向日葵self.idle = Animation.new(self, 'idle', 12)// 定時生成陽光self.canSetTimer && self.setSunTimer()},peashooter () { // 豌豆射手self.canShoot = trueself.idle = Animation.new(self, 'idle', 12)self.attack = Animation.new(self, 'attack', 12)},repeater () { // 雙發射手self.canShoot = trueself.idle = Animation.new(self, 'idle', 12)self.attack = Animation.new(self, 'attack', 8)},gatlingpea () { // 加特林射手// 改變加特林渲染 y 軸距離self.y -= 12self.canShoot = trueself.idle = Animation.new(self, 'idle', 8)self.attack = Animation.new(self, 'attack', 4)},cherrybomb () { // 櫻桃炸彈self.x -= 15self.idle = Animation.new(self, 'idle', 15)self.attack = Animation.new(self, 'attack', 15)setTimeout(()=> {self.state = self.state_ATTACK}, 2000)},wallnut () { // 堅果墻self.x += 15// 設置堅果血量self.life = 12// 創建堅果三種不同血量下的動畫對象self.idleH = Animation.new(self, 'idleH', 10)self.idleM = Animation.new(self, 'idleM', 8)self.idleL = Animation.new(self, 'idleL', 10)},chomper () { // 食人花self.life = 5self.y -= 45self.idle = Animation.new(self, 'idle', 10)self.attack = Animation.new(self, 'attack', 12)self.digest = Animation.new(self, 'digest', 12)},}// 執行對應植物初始化方法for (let key in setPlantFn) {if (self.section === key) {setPlantFn[key]()}}}// 繪制方法draw (cxt) {let self = this,stateName = self.switchState()switch (self.isHurt) {case false:if (self.section === 'cherrybomb' && self.state === self.state_ATTACK) {// 正常狀態,繪制櫻桃炸彈爆炸圖片cxt.drawImage(self[stateName].img, self.x - 60, self.y - 50)} else {// 正常狀態,繪制普通植物圖片cxt.drawImage(self[stateName].img, self.x, self.y)}breakcase true:// 受傷或移動植物時,繪制半透明圖片cxt.globalAlpha = 0.5cxt.beginPath()cxt.drawImage(self[stateName].img, self.x, self.y)cxt.closePath()cxt.save()cxt.globalAlpha = 1break}}// 更新狀態update (game) {let self = this,section = self.section,stateName = self.switchState()// 修改當前動畫序列長度let animateLen = allImg.plants[section][stateName].len// 累加動畫計數器self[stateName].count += 1// 設置角色動畫運行速度self[stateName].imgIdx = Math.floor(self[stateName].count / self[stateName].fps)// 一整套動畫完成后重置動畫計數器self[stateName].imgIdx === animateLen - 1 ? self[stateName].count = 0 : self[stateName].count = self[stateName].count// 繪制發射子彈動畫if (game.state === game.state_RUNNING) {// 設置當前幀動畫對象self[stateName].img = self[stateName].images[self[stateName].imgIdx]if (self[stateName].imgIdx === animateLen - 1) {if (stateName === 'attack' && !self.isDel) {// 未死亡,且為可發射子彈植物時if (self.canShoot) {// 發射子彈self.shoot()// 雙發射手額外發射子彈self.section === 'repeater' && setTimeout(()=> {self.shoot()}, 250)}// 當為櫻桃炸彈時,執行完一輪動畫,自動消失self.section === 'cherrybomb' ? self.isDel = true : self.isDel = false// 當為食人花時,執行完攻擊動畫,切換為消化動畫if (self.section === 'chomper') {// 立即切換動畫會出現圖片未加載完成報錯setTimeout(()=> {self.changeAnimation('digest')}, 0)}} else if (self.section === 'chomper' && stateName === 'digest') {// 消化動畫完畢后,間隔一段時間切換為正常狀態setTimeout(()=> {self.changeAnimation('idle')}, 30000)}self.isAnimeLenMax = true} else {self.isAnimeLenMax = false}}}// 檢測植物是否可攻擊僵尸方法canAttack () {let self = this// 植物類別為向日葵和堅果墻時,不需判定if (self.section === 'sunflower' || self.section === 'wallnut') return false// 循環僵尸對象數組for (let zombie of window._main.zombies) {if (self.section === 'cherrybomb') { // 當為櫻桃炸彈時// 僵尸在以櫻桃炸彈為圓心的 9 個格子內時if (Math.abs(self.row - zombie.row) <= 1 && Math.abs(self.col - zombie.col) <= 1 && zombie.col < 10) {// 執行爆炸動畫self.changeAnimation('attack')zombie.life = 0// 僵尸炸死動畫zombie.changeAnimation('dieboom')}} else if (self.section === 'chomper' && self.state === self.state_IDLE) { // 當為食人花時// 僵尸在食人花正前方時if (self.row === zombie.row && (zombie.col - self.col) <= 1 && zombie.col < 10) {self.changeAnimation('attack')setTimeout(()=> {zombie.isDel = true}, 1300)}} else if (self.canShoot && self.row === zombie.row) { // 當植物可發射子彈,且僵尸和植物處于同行時// 僵尸進入植物射程范圍zombie.x < 940 && self.x < zombie.x + 10 && zombie.life > 0 ? self.changeAnimation('attack') : self.changeAnimation('idle')// 植物未被移除時,可發射子彈if (!self.isDel) {self.bullets.forEach(function (bullet, j) {// 當子彈打中僵尸,且僵尸未死亡時if (Math.abs(zombie.x + bullet.w - bullet.x) < 10 && zombie.life > 0) { // 子彈和僵尸距離小于 10 且僵尸未死亡// 移除子彈self.bullets.splice(j, 1)// 根據血量判斷執行不同階段動畫if (zombie.life !== 0) {zombie.life--zombie.isHurt = truesetTimeout(()=> {zombie.isHurt = false}, 200)}if (zombie.life === 2) {zombie.changeAnimation('dying')} else if (zombie.life === 0) {zombie.changeAnimation('die')}}})}}}}// 射擊方法shoot () {let self = thisself.bullets[self.bullets.length] = Bullet.new(self)}/*** 判斷角色狀態并返回對應動畫對象名稱方法*/switchState () {let self = this,state = self.state,dictionary = {idle: self.state_IDLE,idleH: self.state_IDLE_H,idleM: self.state_IDLE_M,idleL: self.state_IDLE_L,attack: self.state_ATTACK,digest: self.state_DIGEST,}for (let key in dictionary) {if (state === dictionary[key]) {return key}}}/*** 切換角色動畫* game => 游戲引擎對象* action => 動作類型* -idle: 站立動畫* -idleH: 角色高血量動畫(堅果墻)* -idleM: 角色中等血量動畫(堅果墻)* -idleL: 角色低血量動畫(堅果墻)* -attack: 攻擊動畫* -digest: 消化動畫(食人花)*/changeAnimation (action) {let self = this,stateName = self.switchState(),dictionary = {idle: self.state_IDLE,idleH: self.state_IDLE_H,idleM: self.state_IDLE_M,idleL: self.state_IDLE_L,attack: self.state_ATTACK,digest: self.state_DIGEST,}if (action === stateName) returnself.state = dictionary[action]}
}
// 僵尸類
class Zombie extends Role{constructor (obj) {super(obj)// 僵尸類私有屬性let z = {life: 10, // 角色血量canMove: true, // 判斷當前角色是否可移動attackPlantID: 0, // 當前攻擊植物對象 IDidle: null, // 站立動畫對象run: null, // 奔跑動畫對象attack: null, // 攻擊動畫對象dieboom: null, // 被炸死亡動畫對象dying: null, // 瀕臨死亡動畫對象die: null, // 死亡動畫對象state: 1, // 保存當前狀態值,默認為1state_IDLE: 1, // 站立不動狀態state_RUN: 2, // 奔跑狀態state_ATTACK: 3, // 攻擊狀態state_DIEBOOM: 4, // 死亡狀態state_DYING: 5, // 瀕臨死亡狀態state_DIE: 6, // 死亡狀態state_DIGEST: 7, // 消化死亡狀態speed: 3, // 移動速度head_x: 0, // 頭部動畫 x 軸坐標head_y: 0, // 頭部動畫 y 軸坐標}Object.assign(this, z)}// 創建,并初始化當前對象static new (obj) {let p = new this(obj)p.init()return p}// 初始化init () {let self = this// 站立self.idle = Animation.new(self, 'idle', 12)// 移動self.run = Animation.new(self, 'run', 12)// 攻擊self.attack = Animation.new(self, 'attack', 8)// 炸死self.dieboom = Animation.new(self, 'dieboom', 8)// 瀕死self.dying = Animation.new(self, 'dying', 8)// 死亡self.die = Animation.new(self, 'die', 12)}// 繪制方法draw (cxt) {let self = this,stateName = self.switchState()if (stateName !== 'dying' && stateName !== 'die') { // 繪制普通動畫if (!self.isHurt) { // 未受傷時,繪制正常動畫cxt.drawImage(self[stateName].img, self.x, self.y)} else { // 受傷時,繪制帶透明度動畫// 繪制帶透明度動畫cxt.globalAlpha = 0.5cxt.beginPath()cxt.drawImage(self[stateName].img, self.x, self.y)cxt.closePath()cxt.save()cxt.globalAlpha = 1}} else { // 繪制瀕死、死亡動畫if (!self.isHurt) { // 未受傷時,繪制正常動畫cxt.drawImage(self[stateName].imgHead, self.head_x + 70, self.head_y - 10)cxt.drawImage(self[stateName].imgBody, self.x, self.y)} else { // 受傷時,繪制帶透明度動畫// 繪制帶透明度身體cxt.globalAlpha = 0.5cxt.beginPath()cxt.drawImage(self[stateName].imgBody, self.x, self.y)cxt.closePath()cxt.save()cxt.globalAlpha = 1// 頭部不帶透明度cxt.drawImage(self[stateName].imgHead, self.head_x + 70, self.head_y - 10)}}}// 更新狀態update (game) {let self = this,stateName = self.switchState()// 更新能否移動狀態值self.canMove ? self.speed = 3 : self.speed = 0// 更新僵尸列坐標值self.col = Math.floor((self.x - window._main.zombies_info.x) / 80 + 1)if (stateName !== 'dying' && stateName !== 'die') { // 普通動畫(站立,移動,攻擊)// 修改當前動畫序列長度let animateLen = allImg.zombies[stateName].len// 累加動畫計數器self[stateName].count += 1// 設置角色動畫運行速度self[stateName].imgIdx = Math.floor(self[stateName].count / self[stateName].fps)// 一整套動畫完成后重置動畫計數器if (self[stateName].imgIdx === animateLen) {self[stateName].count = 0self[stateName].imgIdx = 0if (stateName === 'dieboom') { // 被炸死亡狀態// 當死亡動畫執行完一輪后,移除當前角色self.isDel = true}// 當前動畫幀數達到最大值self.isAnimeLenMax = true} else {self.isAnimeLenMax = false}// 游戲運行狀態if (game.state === game.state_RUNNING) {// 設置當前幀動畫對象self[stateName].img = self[stateName].images[self[stateName].imgIdx]if (stateName === 'run') { // 當僵尸移動時,控制移動速度self.x -= self.speed / 17}}} else if (stateName === 'dying') { // 瀕死動畫,包含兩個動畫對象// 獲取當前動畫序列長度let headAnimateLen = allImg.zombies[stateName].head.len,bodyAnimateLen = allImg.zombies[stateName].body.len// 累加動畫計數器if (self[stateName].imgIdxHead !== headAnimateLen - 1) {self[stateName].countHead += 1}self[stateName].countBody += 1// 設置角色動畫運行速度self[stateName].imgIdxHead = Math.floor(self[stateName].countHead / self[stateName].fps)self[stateName].imgIdxBody = Math.floor(self[stateName].countBody / self[stateName].fps)// 設置當前幀動畫對象,頭部動畫if (self[stateName].imgIdxHead === 0) {self.head_x = self.xself.head_y = self.yself[stateName].imgHead = self[stateName].images.head[self[stateName].imgIdxHead]} else if (self[stateName].imgIdxHead === headAnimateLen) {self[stateName].imgHead = self[stateName].images.head[headAnimateLen - 1]} else {self[stateName].imgHead = self[stateName].images.head[self[stateName].imgIdxHead]}// 設置當前幀動畫對象,身體動畫if (self[stateName].imgIdxBody === bodyAnimateLen) {self[stateName].countBody = 0self[stateName].imgIdxBody = 0// 當前動畫幀數達到最大值self.isAnimeLenMax = true} else {self.isAnimeLenMax = false}// 游戲運行狀態if (game.state === game.state_RUNNING) {// 設置當前幀動畫對象self[stateName].imgBody = self[stateName].images.body[self[stateName].imgIdxBody]if (stateName === 'dying') { // 瀕死狀態,可以移動self.x -= self.speed / 17}}} else if (stateName === 'die') { // 死亡動畫,包含兩個動畫對象// 獲取當前動畫序列長度let headAnimateLen = allImg.zombies[stateName].head.len,bodyAnimateLen = allImg.zombies[stateName].body.len// 累加動畫計數器if (self[stateName].imgIdxBody !== bodyAnimateLen - 1) {self[stateName].countBody += 1}// 設置角色動畫運行速度self[stateName].imgIdxBody = Math.floor(self[stateName].countBody / self[stateName].fps)// 設置當前幀動畫對象,死亡狀態,定格頭部動畫if (self[stateName].imgIdxHead === 0) {if (self.head_x == 0 && self.head_y == 0) {self.head_x = self.xself.head_y = self.y}self[stateName].imgHead = self[stateName].images.head[headAnimateLen - 1]}// 設置當前幀動畫對象,身體動畫if (self[stateName].imgIdxBody === 0) {self[stateName].imgBody = self[stateName].images.body[self[stateName].imgIdxBody]} else if (self[stateName].imgIdxBody === bodyAnimateLen - 1) {// 當死亡動畫執行完一輪后,移除當前角色self.isDel = trueself[stateName].imgBody = self[stateName].images.body[bodyAnimateLen - 1]} else {self[stateName].imgBody = self[stateName].images.body[self[stateName].imgIdxBody]}}}// 檢測僵尸是否可攻擊植物canAttack () {let self = this// 循環植物對象數組for (let plant of window._main.plants) {if (plant.row === self.row && !plant.isDel) { // 當僵尸和植物處于同行時if (self.x - plant.x < -20 && self.x - plant.x > -60) {if (self.life > 2) {// 保存當前攻擊植物 hash 值,在該植物被刪除時,再控制當前僵尸移動self.attackPlantID !== plant.id ? self.attackPlantID = plant.id : self.attackPlantID = self.attackPlantIDself.changeAnimation('attack')} else {self.canMove = false}if (self.isAnimeLenMax && self.life > 2) { // 僵尸動畫每執行完一輪次// 扣除植物血量if (plant.life !== 0) {plant.life--plant.isHurt = truesetTimeout(()=> {plant.isHurt = false// 堅果墻判斷切換動畫狀態if (plant.life <= 8 && plant.section === 'wallnut') {plant.life <= 4 ? plant.changeAnimation('idleL') : plant.changeAnimation('idleM')}// 判斷植物是否可移除if (plant.life <= 0) {// 設置植物死亡狀態plant.isDel = true// 清除死亡向日葵的陽光生成定時器plant.section === 'sunflower' && plant.clearSunTimer()}}, 200)}} }}}}/*** 判斷角色狀態并返回對應動畫對象名稱方法*/switchState () {let self = this,state = self.state,dictionary = {idle: self.state_IDLE,run: self.state_RUN,attack: self.state_ATTACK,dieboom: self.state_DIEBOOM,dying: self.state_DYING,die: self.state_DIE,digest: self.state_DIGEST,}for (let key in dictionary) {if (state === dictionary[key]) {return key}}}/*** 切換角色動畫* game => 游戲引擎對象* action => 動作類型* -idle: 站立不動* -attack: 攻擊* -die: 死亡* -dying: 瀕死* -dieboom: 爆炸* -digest: 被消化*/changeAnimation (action) {let self = this,stateName = self.switchState(),dictionary = {idle: self.state_IDLE,run: self.state_RUN,attack: self.state_ATTACK,dieboom: self.state_DIEBOOM,dying: self.state_DYING,die: self.state_DIE,digest: self.state_DIGEST,}if (action === stateName) returnself.state = dictionary[action]}
}
?游戲引擎
class Game {constructor (){let g = {actions: {}, // 注冊按鍵操作keydowns: {}, // 按鍵事件對象cardSunVal: null, // 當前選中植物卡片index以及需消耗陽光值cardSection: '', // 繪制隨鼠標移動植物類別canDrawMousePlant: false, // 能否繪制隨鼠標移動植物canLayUp: false, // 能否放置植物mousePlant: null, // 鼠標繪制植物對象mouseX: 0, // 鼠標 x 軸坐標mouseY: 0, // 鼠標 y 軸坐標mouseRow: 0, // 鼠標移動至可種植植物區域的行坐標mouseCol: 0, // 鼠標移動至可種植植物區域的列坐標state: 0, // 游戲狀態值,初始默認為 0state_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, // 動畫幀數}Object.assign(this,g)}static new(){let g=new this()g.init()return g}// clearGameTimer(){// let g=this// clearInterval(g.timer)// }drawBg(){let g=this,cxt=g.context,sunnum=window._main.sunnum,cards=window._main.cards,img=imageFromPath(allImg.bg)cxt.drawImage(img,0,0)sunnum.draw(cxt)}drawCars(){let g=this,cxt=g.context,cars=window._main.carscars.forEach((car,idx)=>{if(car.x>950){cars.splice(idx,1)}car.draw(g,cxt)})}drawCards(){let g=this,cxt=g.context,cards=window._main.cardsfor(let card of cards){card.draw(cxt)}}drawPlantWon(){let g=this,cxt=g.context,text='恭喜玩家獲得勝利!'cxt.fillStyle='red'cxt.font='48px Microsoft YaHei'cxt.fillText(text,354,300)}drawZombieWon(){let g=this,cxt=g.context,img=imageFromPath(allImg.zombieWon)cxt.drawImage(img,293,66)}drawLoading(){let g=this,cxt=g.context,img=imageFromPath(allImg.startBg)cxt.drawImage(img,119,0)}drawStartAnime(){let g=this,stateName='write',loading=window._main.loading,cxt=g.context,canvas_w=g.canvas.width,canvas_h=g.canvas.height,animateLen=allImg.loading[stateName].lenif(loading.imgIdx!=animateLen){loading.count+=1} loading.imgIdx=Math.floor(loading.count/loading.fps)if(loading.imgIdx==animateLen){loading.img=loading.images[loading.imgIdx-1]}else{loading.img=loading.images[loading.imgIdx]}cxt.drawImage(loading.img,437,246)}drawBullets(plants){let g=this,context = g.context, canvas_w = g.canvas.width - 440for(let item of plants){item.bullets.forEach((bullet,idx,arr)=>{bullet.draw(g,context)if(bullet.x>=canvas_w){arr.splice(idx,1)}})}}drawBlood (role) {let g = this,cxt = g.context,x = role.x,y = role.ycxt.fillStyle = 'red'cxt.font = '18px Microsoft YaHei'if(role.type === 'plant'){cxt.fillText(role.life, x + 30, y - 10)}else if(role.type === 'zombie') {cxt.fillText(role.life, x + 85, y + 10)}}updateImage(plants,zombies){let g = this,cxt = g.contextplants.forEach((plant, idx)=>{ plant.canAttack() plant.update(g)})zombies.forEach((zombie, idx)=>{if (zombie.x < 50){ g.state = g.state_ZOMBIEWON}zombie.canAttack()zombie.update(g)})}drawImage (plants, zombies){let g = this,cxt = g.context, delPlantsArr = []plants.forEach((plant, idx, arr)=>{if(plant.isDel){delPlantsArr.push(plant)arr.splice(idx,1)}else{plant.draw(cxt)// g.drawBlood(plant)}})zombies.forEach(function (zombie, idx) {if(zombie.isDel){ zombies.splice(idx, 1)if(zombies.length === 0) {g.state = g.state_PLANTWON}}else{zombie.draw(cxt)// g.drawBlood(zombie)}for(let plant of delPlantsArr) {if(zombie.attackPlantID === plant.id) {zombie.canMove = trueif(zombie.life > 2){zombie.changeAnimation('run')}}}})
}getMousePos(){let g = this,_main=window._main,cxt=g.context,cards=_main.cards,x=g.mouseX,y=g.mouseYif(g.canDrawMousePlant){g.mousePlantCallback(x,y)}}drawMousePlant(plant_info){let g = this,cxt = g.context,plant = nulllet mousePlant_info={type:'plant',section:g.cardSection,x: g.mouseX + 82,y: g.mouseY - 40,row: g.mouseRow,col: g.mouseCol,}if(g.canLayUp){plant=Plant.new(plant_info)plant.isHurt=trueplant.update(g)plant.draw(cxt)}g.mousePlant = Plant.new(mousePlant_info)g.mousePlant.update(g)g.mousePlant.draw(cxt)}mousePlantCallback(x,y){let g = this,_main = window._main,cxt = g.context, row = Math.floor((y - 75) / 100) + 1, col = Math.floor((x - 175) / 80) + 1let plant_info={type:'plant' ,section: g.cardSection,x: _main.plants_info.x + 80 * (col - 1),y: _main.plants_info.y + 100 * (row - 1),row: row,col: col,}g.mouseRow = rowg.mouseCol = colif(row>=1&&row<=5&&col>=1&&col<=9){g.canLayUp=truefor(let plant of _main.plants){if(row==plant.row&&col==plant.col){g.canLayUp=false}}}else{g.canLayUp=false}if(g.canDrawMousePlant){g.drawMousePlant(plant_info)}}registerAction (key, callback) {this.actions[key] = callback}setTimer(_main) {let g = this,plants = _main.plants,zombies = _main.zombies let actions = Object.keys(g.actions)for (let i = 0; i < actions.length; i++) {let key = actions[i]if (g.keydowns[key]) {g.actions[key]()}}g.context.clearRect(0, 0, g.canvas.width, g.canvas.height)if (g.state === g.state_LOADING) {g.drawLoading()} else if (g.state === g.state_START) {g.drawBg()g.drawCars()g.drawCards()g.drawStartAnime()} else if (g.state === g.state_RUNNING) {g.drawBg()g.updateImage(plants, zombies)g.drawImage(plants, zombies)g.drawCars()g.drawCards()g.drawBullets(plants)g.getMousePos()} else if (g.state === g.state_STOP) {g.drawBg()g.updateImage(plants, zombies)g.drawImage(plants, zombies)g.drawCars()g.drawCards()g.drawBullets(plants)_main.clearTiemr()} else if (g.state === g.state_PLANTWON) {g.drawBg()g.drawCars()g.drawCards()g.drawPlantWon()_main.clearTiemr()} else if (g.state === g.state_ZOMBIEWON) { g.drawBg()g.drawCars()g.drawCards()g.drawZombieWon()_main.clearTiemr()}}//========================================================================init(){let g=this,_main=window._main// window.addEventListener('keydown', function (event) {// g.keydowns[event.keyCode] = 'down'// })// window.addEventListener('keyup', function (event) {// g.keydowns[event.keyCode] = 'up'// })g.registerAction = function (key, callback) {g.actions[key] = callback}g.timer = setInterval(function () {g.setTimer(_main)}, 1000/g.fps)document.getElementById('canvas').onmousemove = function (event) {let e = event || window.event,scrollX = document.documentElement.scrollLeft || document.body.scrollLeft,scrollY = document.documentElement.scrollTop || document.body.scrollTop,x = e.pageX || e.clientX + scrollX,y = e.pageY || e.clientY + scrollYg.mouseX = xg.mouseY = y}document.getElementById('js-startGame-btn').onclick = function () {g.state = g.state_STARTsetTimeout(function () {g.state = g.state_RUNNINGdocument.getElementById('pauseGame').className += ' show'document.getElementById('restartGame').className += ' show'_main.clearTiemr()_main.setTimer()}, 2500)document.getElementsByClassName('cards-list')[0].className += ' show'document.getElementsByClassName('menu-box')[0].className += ' show'document.getElementById('js-startGame-btn').style.display = 'none'document.getElementById('js-intro-game').style.display = 'none'document.getElementById('js-log-btn').style.display = 'none'}document.querySelectorAll('.cards-item').forEach(function (card, idx) {card.onclick = function () {let plant = null,cards = _main.cardsif (cards[idx].canClick) {g.cardSection = this.dataset.sectiong.canDrawMousePlant = trueg.cardSunVal = {idx: idx,val: cards[idx].sun_val,}}}})document.getElementById('canvas').onclick = function (event) {let plant = null,cards = _main.cards,x = g.mouseX,y = g.mouseY,plant_info = { type: 'plant',section: g.cardSection,x: _main.plants_info.x + 80 * (g.mouseCol - 1),y: _main.plants_info.y + 100 * (g.mouseRow - 1),row: g.mouseRow,col: g.mouseCol,canSetTimer: g.cardSection === 'sunflower' ? true : false, }for (let item of _main.plants){if(g.mouseRow === item.row && g.mouseCol === item.col) {g.canLayUp = falseg.mousePlant = null}}if (g.canLayUp && g.canDrawMousePlant) {let cardSunVal = g.cardSunValif (cardSunVal.val <= _main.allSunVal) { cards[cardSunVal.idx].canClick = falsecards[cardSunVal.idx].changeState()cards[cardSunVal.idx].drawCountDown()plant = Plant.new(plant_info)_main.plants.push(plant)_main.sunnum.changeSunNum(-cardSunVal.val)g.canDrawMousePlant = false} else { g.canDrawMousePlant = falseg.mousePlant = null}} else {g.canDrawMousePlant = falseg.mousePlant = null}}document.getElementById('pauseGame').onclick = function (event) {g.state = g.state_STOP}document.getElementById('restartGame').onclick = function (event) {if (g.state === g.state_LOADING) { g.state = g.state_START}else{g.state = g.state_RUNNINGfor (let plant of _main.plants) {if (plant.section === 'sunflower') {plant.setSunTimer()}}}_main.setTimer()}}}
?主程序入口
class Main{constructor(){let m={allSunVal:200, // 陽光總數量loading:null, // loading 動畫對象sunnum:null, // 陽光實例對象cars:[], // 實例化除草車對象數組cars_info:{ // 初始化參數x:170, // x 軸坐標y:102, // y 軸坐標position:[{row:1},{row:2},{row:3},{row:4},{row:5},],},cards:[],cards_info:{x:0,y:0,position:[{name: 'sunflower', row: 1, sun_val: 50, timer_spacing: 5 * 1000},{name: 'wallnut', row: 2, sun_val: 50, timer_spacing: 12 * 1000},{name: 'peashooter', row: 3, sun_val: 100, timer_spacing: 7 * 1000},{name: 'repeater', row: 4, sun_val: 150, timer_spacing: 10 * 1000},{name: 'gatlingpea', row: 5, sun_val: 200, timer_spacing: 15 * 1000},{name: 'chomper', row: 6, sun_val: 200, timer_spacing: 15 * 1000},{name: 'cherrybomb', row: 7, sun_val: 250, timer_spacing: 25 * 1000},]},plants:[],zombies:[],plants_info:{type:'plant',x:250,y:92,position:[]},zombies_info:{type:'zombie',x:170,y:15,position:[]},zombies_idx: 0, zombies_row: 0, zombies_iMax: 50, sunTimer: null, sunTimer_difference: 20, zombieTimer: null, zombieTimer_difference: 12, game: null, fps: 60,}Object.assign(this,m)}setZombiesInfo () {let self = this,iMax = self.zombies_iMaxfor(let i = 0; i < iMax; i++) {let row = Math.ceil(Math.random() * 4 + 1)self.zombies_info.position.push({section: 'zombie',row: row,col: 11 + Number(Math.random().toFixed(1))})}}clearTiemr(){let self=thisclearInterval(self.sunTimer)clearInterval(self.zombieTimer)for(let plant of self.plants){if(plant.section=='sunflower'){plant.clearSunTimer()}}}// 設置全局陽光、僵尸生成定時器setTimer(){let self=this,zombies=self.zombiesself.sunTimer = setInterval(function () {let left = parseInt(window.getComputedStyle(document.getElementsByClassName('systemSun')[0],null).left), // 獲取當前元素left值top = '-100px',keyframes1 = [{ transform: 'translate(0,0)', opacity: 0 },{ offset: .5,transform: 'translate(0,300px)', opacity: 1 },{ offset: .75,transform: 'translate(0,300px)', opacity: 1 },{ offset: 1,transform: 'translate(-'+ (left - 110) +'px,50px)',opacity: 0 }] document.getElementsByClassName('systemSun')[0].animate(keyframes1,keyframesOptions)setTimeout(function () {self.sunnum.changeSunNum()document.getElementsByClassName('systemSun')[0].style.left = Math.floor(Math.random() * 200 + 300) + 'px'document.getElementsByClassName('systemSun')[0].style.top = '-100px'}, 2700)}, 1000 * self.sunTimer_difference)self.zombieTimer = setInterval(function () {let idx = self.zombies_iMax - self.zombies_idx - 1if(self.zombies_idx === self.zombies_iMax) { // 僵尸生成數量達到最大值,清除定時器return clearInterval(self.zombieTimer)}if(self.zombies[idx]) {self.zombies[idx].state = self.zombies[idx].state_RUN}self.zombies_idx++},1000 * self.zombieTimer_difference)}setCars(cars_info){let self=thisfor(let car of cars_info.position){let info={x: cars_info.x,y: cars_info.y + 100 * (car.row - 1),row: car.row,}self.cars.push(Car.new(info))}}setCards(cards_info){let self=thisfor (let card of cards_info.position) {let info={name:card.name,row:card.row,sun_val:card.sun_val,timer_spacing: card.timer_spacing,y: cards_info.y + 60 * (card.row - 1),}self.cards.push(Card.new(info))}}//palnt or zombiesetRoles(roles_info){let self=this,type = roles_info.typefor (let role of roles_info.position){let info = {type: roles_info.type,section: role.section,x: roles_info.x + 80 * (role.col - 1),y: roles_info.y + 100 * (role.row - 1),col: role.col,row: role.row,}if(type==='plant'){self.plants.push(Plant.new(info))}else if(type==='zombie'){self.zombies.push(Zombie.new(info))}}}//===========================================start(){let self=thisself.loading = Animation.new({type: 'loading'}, 'write', 55)self.sunnum = SunNum.new()self.setZombiesInfo()self.setCars(self.cars_info)self.setCards(self.cards_info)self.setRoles(self.plants_info)self.setRoles(self.zombies_info)self.game = Game.new()}
}window._main=new Main()
window._main.start()
只對JS中常見的DOM/BOM和基礎語法進行鞏固,后續的CSS代碼和相關圖片資源也會上傳
感謝大家的點贊和關注,你們的支持是我創作的動力!?
?