用原生JS写一个飞机大战小游戏

语言: CN / TW / HK

01.gif

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情 >>

前言

之前写的小游戏都是面向过程的编程思想写的 , 但是JS毕竟是一个面向对象的语言 , 于是我想用面向对象的方法写一个小游戏。es6的class语法我不是很熟练,正好借此机会熟悉一下class的语法,锻炼一下自己的编程思维。文中可能会有一些语法错误的地方,勿喷。

敌机的种类我写了两种,他们的样式和攻击方式不同。在class语法里面添加一个新的子类还是很简便的,后面我也可以在里面添加更多的敌机类型。我还写了一个升级的功能,可以改变主机的攻击方式,以及敌机的生成速度,游戏难度和等级成正比。

1.绘制游戏区域

游戏区域的元素只有背景,飞机,子弹和提示信息的弹出层。背景图可以通过CSS动画的衔接,达到一直向前滚动的效果。下面附上CSS和HTML代码

```

// HTML代码

Lv1

键盘上下左右控制
Lv1: 普通子弹
Lv2: 子弹变为两颗
Lv3: 攻击速率提升
Lv4: 子弹变为三颗

得分
0

```

背景02.jpg

222.png

333.png

444.png pngsucai_4322_0a8932.png

2.生成飞机实例

将飞机的公共样式和方法提取出来,写在父类A的属性里。然后声明两个子类B和C,分别为继承飞机公共样式的敌机和主机。B类里面会写入敌机的公共样式和方法,根据敌机的种类不同,B类下面会有对应的敌机子类B1和B2。主机因为只有一个,所以一个主机C类就够用了。

生成飞机实例的类写好之后,就可以声明一个定时器来自动生成敌机实例了。

// 飞机的公共属性 class A { constructor() { this.ele = document.createElement('div') this.box = document.querySelector('.box') this._initA() // 添加公共样式 this.fen = 0 // 初始得分 } // 公共样式 _initA() { this.ele.classList.add('ele') this.box.appendChild(this.ele) } } // B类敌机,敌机的公共属性 class B extends A { constructor() { super() airB.push(this) // 将B实例存入数组中 this._initB() // 初始化B的样式 } // B类自定义样式 _initB() { const x = Math.random() * 450 this.ele.style.left = `${x}px` // 随机位置 } } // B1类的敌机 class B1 extends B { constructor() { super() this._initB1() // 自定义样式 } _initB1() { this.ele.classList.add('eleB1') } } // B2类的敌机 class B2 extends B { constructor() { super() this._initB2() // 自定义样式 } _initB2() { this.ele.classList.add('eleB2') } } // C类飞机,主机 class C extends A { constructor() { super() this._initC() // 自定义样式 } // 自定义C类的样式 _initC() { this.ele.style.left = `200px` this.ele.style.top = `600px` this.ele.classList.add('eleC') } } let airB = [] // 集合B类飞机的所有实例 let b1 = new B1()// 实例化B类飞机 let c = new C()// 实例化C类飞机 // 自动生成B实例 let timeB function getB(date) { timeB = setInterval(function () { let x = Math.random() * 1000 x >= 400 ? new B1() : new B2() }, date) } getB(2500) // 初始生成敌机的速率 let level = 1 // 等级 02.gif

3.飞机的移动方法

飞机的移动方法写到父类A里,因为敌机和主机都会调用这个方法进行移动。移动的动画我用的requestAnimationFrame这个方法写的,用定时器来写也可以。

敌机的移动是自动且随机方向的,可以用定时器自动调用移动的方法,用随机数来随机方向。主机的移动是由键盘控制的,在键盘事件里面调用实例原型里的方法即可。

移动的方法我写了4个函数,代码就显得有点多。封装成一个函数也可以,根据传入的参数来判断往哪边移动即可。

// 飞机的公共属性 class A { constructor() { ...... } ..... //右移动 rafRight(num = 0) { let i = num requestAnimationFrame(() => { //获取元素的坐标值,要把字符串里的数字提取出来 let moveX = parseFloat(this.ele.style.left) || 0 if (moveX >= 440) return moveX += 5 this.ele.style.left = moveX + 'px' i++ if (i >= 10) return return this.rafRight(i) }) } //左移动 rafLeft(num = 0) { let i = num requestAnimationFrame(() => { let moveX = parseFloat(this.ele.style.left) || 0 if (moveX <= 0) return moveX -= 5 this.ele.style.left = moveX + 'px' i++ if (i >= 10) return return this.rafLeft(i) }) } //上移动 rafTop(num = 0) { let i = num requestAnimationFrame(() => { let moveY = parseFloat(this.ele.style.top) || 0 if (moveY <= 400) return moveY -= 5 this.ele.style.top = moveY + 'px' i++ if (i >= 10) return return this.rafTop(i) }) } //下移动 rafDown(num = 0) { let i = num requestAnimationFrame(() => { let moveY = parseFloat(this.ele.style.top) || 0 if (moveY >= 640) return moveY += 5 this.ele.style.top = moveY + 'px' i++ if (i >= 10) return return this.rafDown(i) }) } } // B类敌机,敌机的公共属性 class B extends A { constructor() { ..... this.moveTime = this._move() // 自动移动 } ..... // 定时调用父类的移动函数 _move() { return setInterval(() => { let x = Math.random() * 1000 if (x <= 150) { super.rafTop() } else if (x > 150 && x <= 300) { super.rafDown() } else if (x > 300 && x <= 650) { super.rafRight() } else { super.rafLeft() } }, 1050) } } // 键盘事件,控制C实例的移动 let life = true document.addEventListener('keydown', function (e) { if (life) { if (e.key === 'ArrowUp') { c.rafTop() } else if (e.key === 'ArrowRight') { c.rafRight() } else if (e.key === 'ArrowLeft') { c.rafLeft() } else if (e.key === 'ArrowDown') { c.rafDown() } } })

4.生成子弹,以及子弹的移动

子弹我本来想用对象的方式来写,写一个子弹的类,然后在飞机的类里面调用生成子弹的实例。但是这种方式飞机和子弹的实例之间就没什么联系了,后面写飞机被击中的判断时不好下手了。于是我把生成子弹的函数作为公共方法写到了A这个飞机的父类里,子类飞机通过调用父类的方法来生成子弹元素。

两种敌机的子弹攻击方式不同,在对应的类里面也各自的样式即可。

``` // 飞机的公共属性 class A { constructor() { ..... } ..... // 生成子弹元素的方法 _fire() { let fires = document.createElement('div') // 创建子弹元素 this.box.appendChild(fires) // 添加样式 fires.classList.add('fire') let moveX = parseFloat(this.ele.style.left) || 0 let moveY = parseFloat(this.ele.style.top) || 0 fires.style.left = moveX + 25 + 'px' fires.style.top = moveY + 'px' return fires //返回这个子弹元素 } //子弹移动 _fireMove(i, item, air, n = 0) { // i控制子弹方向,item为子弹元素,air为飞机实例 requestAnimationFrame(() => { //获取元素的坐标值,要把字符串里的数字提取出来 let moveY = parseFloat(item.style.top) || 0 moveY += i * 5 item.style.top = moveY + 'px' n++ // 计数,减少计算量 if (moveY <= -10 || moveY >= 710) return item.style.display = 'none' if (n >= 50) { // 判断air参数是B实例还是C实例 let flag if (Array.isArray(air)) { for (let i = 0; i < air.length; i++) { flag = this._attack(air[i], item, i) // 击毁判定函数 if (flag) break // 达成条件就退出循环,减少计算量 } } else { const flag = this._attack(air, item) // 击毁判定函数 } if (flag) return // 子弹击毁飞机后,终止移动函数 } return this._fireMove(i, item, air, n) }) } // 击毁判定 _attack(p, f, i) { // p参数为飞机实例,f参数为子弹,i为B实例元素的下标

}

} ..... // B1类的敌机 class B1 extends B { constructor() { ..... this.timer = this.openFire() // 自动攻击 } ..... // B1类飞机子弹样式 _fireB1() { let fb = super._fire() fb.classList.add('fire1') let moveY = parseFloat(this.ele.style.top) || 0 fb.style.top = moveY + 40 + 'px' super._fireMove(1, fb, c) } // 自动攻击的方法 openFire() { return setInterval(() => { this._fireB1() }, 1000) } } // B2类的敌机 class B2 extends B { constructor() { ..... this.timer = this.openFire() // 自动攻击 } ..... // B2类飞机子弹样式 _fireB2() { let fb1 = super._fire() let fb2 = super._fire() fb1.classList.add('fire1') fb2.classList.add('fire1') let moveX = parseFloat(this.ele.style.left) || 0 let moveY = parseFloat(this.ele.style.top) || 0 fb1.style.left = moveX + 10 + 'px' fb2.style.left = moveX + 40 + 'px' fb1.style.top = moveY + 40 + 'px' fb2.style.top = moveY + 40 + 'px' super._fireMove(1, fb1, c) super._fireMove(1, fb2, c) } // 自动攻击的方法 openFire() { return setInterval(() => { this._fireB2() }, 1500) } } ..... // C类飞机,主机 class C extends A { constructor() { ..... this.openFire() // 自动攻击 } ..... // C类子弹样式 // lv1子弹 _fire1() { let fb = super._fire() fb.classList.add('fire2') super._fireMove(-1, fb, airB) } ..... // 自动射击的方法,根据等级的不同,射击的速率不同 openFire() { return setInterval(() => { if (level == 1) { this._fire1() } else if (level == 2) { this._fire2() } }, 800) } } ```

04.gif

5.等级系统

写这个功能主要是为了增加游戏的趣味性。根据游戏的时长来获取经验值,经验值满了之后就会升级,在不同的等级下,主机的子弹会产生变化,并且敌机的生成速率也会变快。经验值的增长与等级成反比,等级越高,经验值涨的越慢。

// C类飞机,主机 class C extends A { constructor() { ..... } ..... // C类子弹样式 // lv1子弹 _fire1() { let fb = super._fire() fb.classList.add('fire2') super._fireMove(-1, fb, airB) } // lv2子弹 _fire2() { let fb1 = super._fire() let fb2 = super._fire() let moveX = parseFloat(this.ele.style.left) || 0 let moveY = parseFloat(this.ele.style.top) || 0 fb1.style.left = moveX + 10 + 'px' fb1.style.top = moveY + 10 + 'px' fb2.style.left = moveX + 40 + 'px' fb2.style.top = moveY + 10 + 'px' fb1.classList.add('fire2') fb2.classList.add('fire2') super._fireMove(-1, fb1, airB) super._fireMove(-1, fb2, airB) } // LV3子弹样式与LV2相同 // lv4子弹 _fire4() { let fb1 = super._fire() let fb2 = super._fire() let fb3 = super._fire() let moveX = parseFloat(this.ele.style.left) || 0 let moveY = parseFloat(this.ele.style.top) || 0 fb1.style.left = moveX + 'px' fb1.style.top = moveY + 10 + 'px' fb2.style.left = moveX + 25 + 'px' fb2.style.top = moveY + 10 + 'px' fb3.style.left = moveX + 50 + 'px' fb3.style.top = moveY + 10 + 'px' fb1.classList.add('fire2') fb2.classList.add('fire2') fb3.classList.add('fire2') super._fireMove(-1, fb1, airB) super._fireMove(-1, fb2, airB) super._fireMove(-1, fb3, airB) } // 调用开火方法,等级不同,速度也不同 openFire() { return setInterval(() => { if (level == 1) { this._fire1() } else if (level == 2) { this._fire2() } }, 800) } openFire2() { return setInterval(() => { if (level == 3) { this._fire2() } else if (level >= 4) { this._fire4() } }, 500) } } // 下面这些代码都写在全局里 let level = 1 // 等级 let wid = 0 // 经验条 // 经验值增长 setInterval(() => { // 控制经验增长速率,与等级成反比 wid += 3 / (level + 1) ex.style.width = wid + 'px' if (wid >= 500) { // wid大于500就升级 level++ levelUp() if (level == 3) { // 大于3级时,开启C类的另一个自动开火函数 clearInterval(c.openFire()) c.openFire2() } document.querySelector('h3').innerHTML = 'Lv' + level wid = 0 clearInterval(timeB) // 根据不同等级,改变B实例的生成速率,与等级成正比 getB((2500 - level * 400)) } }, 50) // 升级时显示提示弹层 function levelUp() { document.querySelector('.dialog').style.display = 'block' document.querySelector('.dialog').innerHTML = '升级了!' setTimeout(() => { document.querySelector('.dialog').style.display = 'none' }, 1000) }

05.gif

6.子弹击中飞机的判断,以及飞机的击毁

因为子弹击中的判断和飞机的击毁所有的飞机实例都会用到,所以把这两个方法写到A这个父类里,子类飞机在发射子弹的时候,就会调用这个方法来判断子弹是否击中飞机,如果击中了,就把这个飞机实例给击毁,使用父类中击毁的方法。如果被击中的是本机C实例,那么就游戏结束。

// 飞机的公共属性 class A { constructor() { ...... } ....... // 击毁判定 _attack(p, f, i) { // p参数为飞机实例,f参数为子弹,i为B实例元素的下标 let fx = parseFloat(f.style.left) + 5 || 0 let fy = parseFloat(f.style.top) + 10 || 0 let px = parseFloat(p.ele.style.left) + 30 || 0 let py = parseFloat(p.ele.style.top) + 30 || 0 // 判断坐标是否重合 if (Math.abs(fx - px) < 30 && Math.abs(fy - py) < 50) { this._delete(p, f, i) return true } } // 击毁飞机的方法 _delete(p, f, i) { p.ele.classList.add('del') // 添加飞机爆炸的样式 if (p === c) { // 主机被击中,游戏结束 life = false // 控制键盘事件的变量 //清除所有定时器 let qc = setInterval(function () { }, 1) for (let i = 0; i < qc; i++) { clearInterval(i) } document.querySelector('.dialog').style.display = 'block' document.querySelector('.dialog').innerHTML = '游戏结束!!' return } else { // 敌机被击中 clearInterval(p.timer) clearInterval(p.moveTime) airB.splice(i, 1) f.remove() this.fen++ // 得分加1,并显示分数 document.querySelector('span').innerHTML = this.fen setTimeout(() => { p.ele.remove() p = null // 清除实例 }, 200) } } }

01.gif

01.jpg

总结

以前写代码都是把步骤封装成函数,用到哪一步就调用这个函数。用对象的方式来写这个游戏,根据类的继承,可以很方便的生成各种不同类型的飞机实例,给每一种飞机类型的添加独特的子弹攻击方式也很方便。给每一个关卡写一个BOSS飞机出来也不是难事。

我对继承的理解还是比较浅,第一次使用class来写小游戏,代码不多,300多行,里面也有很多不完善的地方。主要是自己想通过写小游戏来练一练思维和语法,有些的不好的地方欢迎指正,不喜勿喷。

03.jpg

04.jpg