用JS实现植物大战僵尸(前端作业)
1. 先搭架子
整体效果:
点击开始后进入主场景
左侧是植物卡片
右上角是游戏的开始和暂停键
Document 点击开始游戏
- 向日葵 冷却时间:5秒
- 坚果墙 冷却时间:12秒
- 豌豆射手 冷却时间:7秒
- 双发豌豆射手 冷却时间:10秒
- 加特林射手 冷却时间:15秒
- 食人花 冷却时间:15秒
- 樱桃炸弹 冷却时间:25秒
2. 导入植物/僵尸/阳光...的图片
图片包含:植物cd好的状态和冷却期的状态,植物空闲状态/攻击状态,僵尸包含移动状态/攻击状态/樱桃炸弹炸的效果, 同时我们提供对外的imageFromPath函数, 用来生成图片路径
const imageFromPath = function(src){ let img = new Image() img.src = './images/' + src return 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这个标签, 你可以把它想像成一个画布,我们可以通过获取上下文来绘制在画布上进行绘画(坐标系如下)
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'
阳光显示板: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=this cxt.drawImage(self.img,self.x+120,self.y) //用于在Canvas上绘制图像 cxt.fillStyle='black' cxt.font='24px Microsoft YaHei' cxt.fontWeight=700 cxt.fillText(self.sun_num,self.x+175,self.y+27) } //修改阳光 !!!!! changeSunNum(num=25){ let self=this window._main.allSunVal+=num self.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 = 120 if(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=this self.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=this if(!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 = this self.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子弹:例如像豌豆射手就会发射子弹,但是只有在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 + 30 b.y = plant.y break case 'repeater': b.x = plant.x + 30 b.y = plant.y break case 'gatlingpea': b.x = plant.x + 30 b.y = plant.y + 10 break } return b } draw(game,cxt){ let self=this self.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.width role.h = this.height } } return a } /** * 为角色不同动作创造动画序列 */ create () { let self = this, section = self.section // 植物种类 switch (self.type) { case 'plant': for(let i = 0; i为植物和僵尸设置不同状态下的动画效果
/** * 角色类 * 植物、僵尸类继承的基础属性 */ 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轴坐标 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 = this self.sunTimer = setInterval(function () { // 创建阳光元素 let img = document.createElement('img'), // 创建元素 container = document.getElementsByTagName('body')[0], // 父级元素容器 id = self.id, // 当前角色 ID top = 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' + id img.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 = this clearInterval(self.sunTimer) } // 初始化 init () { let self = this, setPlantFn = null // 初始化植物动画对象方法集 setPlantFn = { sunflower () { // 向日葵 self.idle = Animation.new(self, 'idle', 12) // 定时生成阳光 self.canSetTimer && self.setSunTimer() }, peashooter () { // 豌豆射手 self.canShoot = true self.idle = Animation.new(self, 'idle', 12) self.attack = Animation.new(self, 'attack', 12) }, repeater () { // 双发射手 self.canShoot = true self.idle = Animation.new(self, 'idle', 12) self.attack = Animation.new(self, 'attack', 8) }, gatlingpea () { // 加特林射手 // 改变加特林渲染 y 轴距离 self.y -= 12 self.canShoot = true self.idle = Animation.new(self, 'idle', 8) self.attack = Animation.new(self, 'attack', 4) }, cherrybomb () { // 樱桃炸弹 self.x -= 15 self.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 = 5 self.y -= 45 self.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) } break case true: // 受伤或移动植物时,绘制半透明图片 cxt.globalAlpha = 0.5 cxt.beginPath() cxt.drawImage(self[stateName].img, self.x, self.y) cxt.closePath() cxt.save() cxt.globalAlpha = 1 break } } // 更新状态 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) 0) { // 子弹和僵尸距离小于 10 且僵尸未死亡 // 移除子弹 self.bullets.splice(j, 1) // 根据血量判断执行不同阶段动画 if (zombie.life !== 0) { zombie.life-- zombie.isHurt = true setTimeout(()=> { zombie.isHurt = false }, 200) } if (zombie.life === 2) { zombie.changeAnimation('dying') } else if (zombie.life === 0) { zombie.changeAnimation('die') } } }) } } } } // 射击方法 shoot () { let self = this self.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) return self.state = dictionary[action] } } // 僵尸类 class Zombie extends Role{ constructor (obj) { super(obj) // 僵尸类私有属性 let z = { life: 10, // 角色血量 canMove: true, // 判断当前角色是否可移动 attackPlantID: 0, // 当前攻击植物对象 ID idle: null, // 站立动画对象 run: null, // 奔跑动画对象 attack: null, // 攻击动画对象 dieboom: null, // 被炸死亡动画对象 dying: null, // 濒临死亡动画对象 die: null, // 死亡动画对象 state: 1, // 保存当前状态值,默认为1 state_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.5 cxt.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.5 cxt.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 = 0 self[stateName].imgIdx = 0 if (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.x self.head_y = self.y self[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 = 0 self[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.x self.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 = true self[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 -60) { if (self.life > 2) { // 保存当前攻击植物 hash 值,在该植物被删除时,再控制当前僵尸移动 self.attackPlantID !== plant.id ? self.attackPlantID = plant.id : self.attackPlantID = self.attackPlantID self.changeAnimation('attack') } else { self.canMove = false } if (self.isAnimeLenMax && self.life > 2) { // 僵尸动画每执行完一轮次 // 扣除植物血量 if (plant.life !== 0) { plant.life-- plant.isHurt = true setTimeout(()=> { plant.isHurt = false // 坚果墙判断切换动画状态 if (plant.life { if(car.x>950){ cars.splice(idx,1) } car.draw(g,cxt) }) } drawCards(){ let g=this,cxt=g.context,cards=window._main.cards for(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].len if(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 - 440 for(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.y cxt.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.context plants.forEach((plant, idx)=>{ plant.canAttack() plant.update(g) }) zombies.forEach((zombie, idx)=>{ if (zombie.x { 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 = true if(zombie.life > 2){ zombie.changeAnimation('run') } } } }) } getMousePos(){ let g = this,_main=window._main,cxt=g.context,cards=_main.cards,x=g.mouseX,y=g.mouseY if(g.canDrawMousePlant){ g.mousePlantCallback(x,y) } } drawMousePlant(plant_info){ let g = this,cxt = g.context,plant = null let 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=true plant.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) + 1 let 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 = row g.mouseCol = col if(row>=1&&row=1&&col
免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。