用JS实现植物大战僵尸(前端作业)_植物大战僵尸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 < 150时) canMove () { let self = this for (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 = 0 zombie.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 + 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 < 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))) } break case \'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))) } } break case \'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 值,用于设置当前角色 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) <= 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 { zombie.isDel = true }, 1300) } } else if (self.canShoot && self.row === zombie.row) { // 当植物可发射子弹,且僵尸和植物处于同行时 // 僵尸进入植物射程范围 zombie.x < 940 && self.x 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) 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 <= 8 && plant.section === \'wallnut\') { plant.life <= 4 ? plant.changeAnimation(\'idleL\') : plant.changeAnimation(\'idleM\') } // 判断植物是否可移除 if (plant.life 游戏引擎对象 * 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) return self.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, // 游戏状态值,初始默认为 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, // 动画帧数 } 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.cars cars.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.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<=9){ g.canLayUp=true for(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 + scrollY g.mouseX = x g.mouseY = y } document.getElementById(\'js-startGame-btn\').onclick = function () { g.state = g.state_START setTimeout(function () { g.state = g.state_RUNNING document.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.cards if (cards[idx].canClick) { g.cardSection = this.dataset.section g.canDrawMousePlant = true g.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 = false g.mousePlant = null } } if (g.canLayUp && g.canDrawMousePlant) { let cardSunVal = g.cardSunVal if (cardSunVal.val <= _main.allSunVal) { cards[cardSunVal.idx].canClick = false cards[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 = false g.mousePlant = null } } else { g.canDrawMousePlant = false g.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_RUNNING for (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_iMax for(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=this clearInterval(self.sunTimer) clearInterval(self.zombieTimer) for(let plant of self.plants){ if(plant.section==\'sunflower\'){ plant.clearSunTimer() } } } // 设置全局阳光、僵尸生成定时器 setTimer(){ let self=this,zombies=self.zombies self.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 - 1 if(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=this for(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=this for (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 zombie setRoles(roles_info){ let self=this,type = roles_info.type for (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=this self.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代码和相关图片资源也会上传
感谢大家的点赞和关注,你们的支持是我创作的动力!
地址:https://gitee.com/a-fish-v/admin_ice