当前位置: 首页 > news >正文

用JS实现植物大战僵尸(前端作业)

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代码和相关图片资源也会上传

感谢大家的点赞和关注,你们的支持是我创作的动力! 

 

相关文章:

  • 数据结构:栈(Stack)和堆(Heap)
  • LeetCode[110]平衡二叉树
  • 前端-不对用户显示
  • 域权限维持和后渗透密码收集
  • [VMM]现代 CPU 中用于加速多级页表查找的Page‐Table Entry原理
  • Qt SQL模块基础
  • 元胞自动机(Cellular Automata, CA)
  • CQF预备知识:一、微积分 -- 1.8.3 二元泰勒展开详解
  • 【Rust 轻松构建轻量级多端桌面应用】
  • 利用aqs构建一个自己的非公平独占锁
  • 【LUT技术专题】图像自适应3DLUT
  • 设计模式——原型设计模式(创建型)
  • Cypress + React + TypeScript
  • macOS 上安装运行 PowerShell
  • 电路图识图基础知识-常用仪表识图及接线(九)
  • uniapp uni-id Error: Invalid password secret
  • Oracle用户账号过期终极解决方案
  • 嵌入式学习笔记 - STM32 HAL库以及标准库内核以及外设头文件区别问题
  • python 空气质量可视化,数据分析 + 前后端分离 + ppt 演讲大纲
  • 【数据分析】基于Cox模型的R语言实现生存分析与生物标志物风险评估
  • wordpress怎么完成/郑州关键词优化费用
  • 帝国网站系统做专题/网站加速
  • 南山公安分局网站/南宁网站seo大概多少钱
  • miui稳定版到开发版的升级一般通过/抚顺优化seo
  • 旅游商务网站开发/外包网络推广营销
  • 有多个网页的大网站如何做/网站关键词搜索