一、游戏总体框架
飞机大战
总体框架:
* index.html 入口界面
* static 项目的素材等内容
* |_ src 代码资源文件夹
* | |_ mod 模块文件夹
* | | |_ Background.js 背景模块
* | | |_ Player.js 我方飞机
* | | |_ Boom.js 爆炸图片
* | | |_ Bullet.js 子弹
* | | |_ DialogModal.js 弹出层(死亡后重新开启游戏)
* | | |_ Enemy.js 敌机
* | | |_ playerConfig.js 飞机配置事件
* | | |_ Score.js 得分
* | |_ lib 封装的库
* | | |_ proto.js 对象添加迭代器属性,实现对象解构赋值
* | |_ Game.js 游戏主函数的入口
* | |_ Status.js 数据管理中心
* | |_ tool.js 计算矩形的公有面积是否碰撞
* |_ images 图片文件夹
运行方式
? 运行index.html文件,然后按F12打开控制台,切换至移动端,刷新页面后,即可.
二、游戏总体内容
1.面向对象方案
本飞机大战游戏,采用面向对象的方式,通过模块化编程去编写… 同时,我们仅对外暴露出一个接口,一个Game的构造函数 此为游戏的入口文件 index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>飞机大战</title>
<style>
*{
margin: 0;
}
html,body{
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<script type="module">
import Game from "./static/src/Game.js";
window.game = new Game(document.querySelector("body"))
</script>
</body>
</html>
2.游戏主函数的入口Game.js
代码如下:
import "./lib/proto.js"
import status from "./Status.js";
import DialogModal from "./mod/DialogModal.js";
class Game{
constructor(container) {
this.container = container
this.paused = false
this.gameOver = false
this.render = this.render.bind(this)
this.pause = this.pause.bind(this)
this.continue = this.continue.bind(this)
this.initCanvas()
this.restartGame()
}
initCanvas(){
this.canvas = document.createElement('canvas')
this.canvas.style.display = 'block'
this.canvas.width = this.container.getBoundingClientRect().width
this.canvas.height = this.container.getBoundingClientRect().height
this.ctx = this.canvas.getContext('2d')
this.container.appendChild(this.canvas)
this.restartDialog = new DialogModal(this.ctx)
status.init(this.canvas)
this.container.onblur = this.pause
this.container.onfocus = this.continue
this.size = {
w:status.size.w,
h:status.size.h
}
}
restartGame(){
cancelAnimationFrame(this.frame)
this.removeEvent()
this.initEvent()
status.reset()
this.continue()
this.render()
}
render(){
this.frame = requestAnimationFrame(this.render)
if(this.paused) return
this.ctx.clearRect(0,0,...this.size)
status.update()
status.render()
if(status.gameOver){
this.removeEvent()
clearInterval(this.fireTimer)
cancelAnimationFrame(this.frame)
this.renderRestartRect()
return
}
}
renderRestartRect(){
this.restartDialog.render()
this.restartDialog.bindEvent()
this.restartDialog.handle(() => {
this.restartGame()
})
}
removeEvent(){
status.player.removeEvent(this.canvas)
}
initEvent(){
window.onresize = e => {
status.setSize(window.innerWidth, window.innerHeight)
}
window.addEventListener("keydown", e => {
if(e.key.toLowerCase() === "k"){
status.enemyList.forEach( enemy => {
enemy.dead = true
})
}
})
status.player.initEvent(this.canvas)
}
pause(){
clearInterval(this.fireTimer)
this.paused = true
}
continue(){
clearInterval(this.fireTimer)
this.fireTimer = setInterval(status.fire.bind(status),1000/8)
this.paused = false
}
}
export default Game
看到this.ctx.clearRect(0,0,this.size.w,this.size.h) 的时候,会不会觉得有点复杂, 那么我该怎样使用一个可以去遍历的对象属性值,通过...
this.ctx.clearRect(0,0,...this.size)
怎样变成这样呢?
3.这样一来,我们就需要这样一个文件 proto.js
Object.prototype[Symbol.iterator] = function* (){
for (let i in this){
yield this[i]
}
}
Function.prototype.onceBind = (function (){
const bindMap = new Map()
return function (obj){
if(!bindMap.get(obj)){
bindMap.set(obj,new Map())
}
if(!bindMap.get(obj).get(this)){
bindMap.get(obj).set(this,this.bind(obj))
}
return bindMap.get(obj).get(this)
}
})();
4.上面配置都出来了,怎么可能游戏没有背景呢 Background.js
import status from "../Status.js";
export default class Background {
constructor(ctx) {
this.ctx = ctx
this.vy = 2
this.rect1 = {
x:0,
y:0,
...status.size
}
this.rect2 = {
x:0,
y:this.rect1.y - status.size.h,
...status.size
}
this.init()
}
init(){
this.img = new Image()
this.img.src = "static/images/bg.jpg"
}
reset(){
this.rect1.y = 0
}
render(){
this.ctx.drawImage(this.img,...this.rect1)
this.ctx.drawImage(this.img,...this.rect2)
}
update(){
this.rect1.y += this.vy
this.rect2.y = this.rect1.y - status.size.h
if(this.rect1.y >= status.size.h){
this.rect1.y = 0
}
}
}
5.背景都有啦,咱们开始造玩家吧 Player.js
import status from "../Status.js";
import playerConfig from "./playerConfig.js";
import boomImgList from "./Boom.js";
export default class Player {
constructor(ctx) {
this.ctx = ctx
this.draged = false
this.rect = {
x:status.size.w / 2 - 49,
y:status.size.h - 65,
w:98,
h:65
}
this.booming = false
this.boomingCount = 0
this.vip = 1
this.init()
this.level = 1
}
init(){
this.playerImg = new Image()
this.img = this.playerImg
this.img.src = "static/images/hero.png"
}
reset(){
this.dead = false
this.booming = false
this.boomingCount = 0
this.img = this.playerImg
this.rect = {
x:status.size.w / 2 - 49,
y:status.size.h - 65,
w:98,
h:65
}
}
render(){
this.ctx.drawImage(this.img,...this.rect)
}
update(){
if(this.rect.x < 0){
this.rect.x = 0
}
if(this.rect.x > status.size.w - this.rect.w){
this.rect.x = status.size.w - this.rect.w
}
if(this.rect.y < 0){
this.rect.y = 0
}
if(this.rect.y > status.size.h - this.rect.h){
this.rect.y = status.size.h - this.rect.h
}
if(this.booming && this.boomingCount < boomImgList.length){
this.img = boomImgList[this.boomingCount++]
}
if(this.boomingCount === boomImgList.length){
this.dead = true
}
}
initEvent(dom){
playerConfig.forEach(item=>{
item.handleList.forEach(fn =>{
dom.addEventListener(item.type,fn.onceBind(this))
})
})
}
removeEvent(dom){
playerConfig.forEach(item=>{
item.handleList.forEach(fn =>{
dom.removeEventListener(item.type,fn.onceBind(this))
})
})
}
kill(){
this.booming = true
}
}
6.来给我方飞机来个’皮肤’ playerConfig.js
import rectCollide from "../tools.js";
export default [
{
type: 'touchstart',
handleList:[
function(e){
const mouseRect = {
x : e.changedTouches[0].clientX - 5,
y : e.changedTouches[0].clientY - 5,
w : 10,
h : 10
}
if(rectCollide(mouseRect,this.rect)){
this.draged = true
}
}
]
},{
type: 'touchmove',
handleList:[
function(e){
if(!this.draged){
return
}
this.rect.x = e.changedTouches[0].clientX - this.rect.w /2
this.rect.y = e.changedTouches[0].clientY - this.rect.h /2
}
]
},
{
type: 'touchend',
handleList:[
function(e){
this.draged = false
}
]
}
]
7.咱也不可能打仗不带枪啊 Bullet.js
import status from "../Status.js";
export default class Bullet{
constructor(ctx) {
this.ctx = ctx
this.dead = false
this.rect = {
x:0,
y:0,
w:18,
h:27
}
this.vy = -3
this.init()
}
setPosition(rect){
this.rect.x = rect.x + (rect.w - this.rect.w)/2
this.rect.y = rect.y - this.rect.h/2
}
init(){
this.img = new Image()
this.img.src = "static/images/bullet.png"
}
render(){
this.ctx.drawImage(this.img,...this.rect)
}
update(){
this.vy -= 0.1
this.rect.y += this.vy
if(this.rect.y < - this.rect.h){
this.kill()
}
}
kill(){
this.dead = true
}
}
8.敌机来喽 Enemy.js
import status from "../Status.js";
import boomImgList from "./Boom.js";
export default class Enemy{
constructor(ctx) {
this.ctx = ctx
this.rect = {
x:Math.random() * (status.size.w-60),
y:-40,
w:60,
h:40
}
this.boomingCount = 0
this.lives = 2
this.booming = false
this.dead = false
this.vy = Math.random() * 2 + 1
this.init()
}
init(){
this.img = new Image()
this.img.src = "static/images/enemy.png"
}
render(){
this.ctx.drawImage(this.img,...this.rect)
}
update(){
this.rect.y += this.vy
if(this.rect.y > status.size.h + this.rect.h){
this.kill()
}
if(this.booming && this.boomingCount<boomImgList.length){
this.img = boomImgList[this.boomingCount++]
}
if(this.boomingCount === boomImgList.length){
this.dead = true
}
}
kill(count = 1){
this.lives -= count;
if(this.lives <= 0){
this.booming = true
}
}
}
9.子弹爆裂图片 Boom.js
let length = 19
let boomImgList = []
for(let i = 0; i < length; i++){
let img = new Image()
img.src = `static/images/explosion${i+1}.png`
boomImgList.push(img)
}
export default boomImgList
10.来看个分数吧 Score.js
import status from "../Status.js"
export default class Score {
constructor(ctx){
this.ctx = ctx
this.ctx.font = "20px serif"
this.ctx.fillStyle = "#ffffff"
this.ctx.fontWeight = "bold"
this.count = 0
}
add(){
this.count ++
}
getMsg(){
this.msg = `击杀敌机${this.count}`
return this.msg
}
reset(){
this.count = 0
}
render(){
this.ctx.beginPath()
this.ctx.fillText(this.getMsg(), status.size.w - 100, 20, 100);
}
}
11.继续努力吧(死亡重新开始喽) 弹出层 DialogModal.js
import Status from "../Status.js"
import rectCollide from "../tools.js"
export default class DialogModal {
constructor(ctx){
this.ctx = ctx
this.rect = {
x: Status.size.w / 4,
y: Status.size.h / 4,
w: Status.size.w / 2,
h: Status.size.h / 2,
}
this.init()
}
init(){
this.img = new Image()
this.img.src = "static/images/restart.png"
}
render(){
this.rect = {
x: Status.size.w / 4,
y: Status.size.h / 4,
w: Status.size.w / 2,
h: Status.size.w / 2,
}
this.ctx.drawImage(this.img, ...this.rect)
}
click (e) {
let touchRect = {
x: e.touches[0].clientX - 5,
y: e.touches[0].clientY - 5,
w: 10,
h: 10
}
if(rectCollide(touchRect, this.rect)){
this.removeEvent()
this.fn()
}
}
handle(fn){
this.fn = fn
}
bindEvent(){
Status.canvas.addEventListener("touchstart", this.click.onceBind(this))
}
removeEvent(){
Status.canvas.removeEventListener("touchstart", this.click.onceBind(this))
}
}
12.数据管理中心 不管是项目的尺寸还是碰撞,都在我这里哦Status.js
import Background from "./mod/Background.js";
import Player from "./mod/Player.js";
import Bullet from "./mod/Bullet.js";
import Enemy from "./mod/Enemy.js";
import Score from "./mod/Score.js";
import rectCollide from "./tools.js";
class Status {
constructor() {
this.size = {
w:0,
h:0
}
this.gameOver = false
}
init(canvas){
this.canvas = canvas
this.ctx = this.canvas.getContext('2d')
this.size.w = canvas.width
this.size.h = canvas.height
this.bg = new Background(this.ctx)
this.player = new Player(this.ctx)
this.score = new Score(this.ctx)
this.bulletList = []
this.enemyList = []
}
fire(){
let bullet = null
switch (this.player.level) {
case 1:
bullet = new Bullet(this.ctx)
bullet.setPosition(this.player.rect)
this.bulletList.push(bullet)
break
case 2:
bullet = new Bullet(this.ctx)
let rect1 = {
x: this.player.rect.x - 10,
y: this.player.rect.y,
w: this.player.rect.w,
h: this.player.rect.h
}
bullet.setPosition(rect1)
this.bulletList.push(bullet)
bullet = new Bullet(this.ctx)
let rect2 = {
x: this.player.rect.x + 10,
y: this.player.rect.y,
w: this.player.rect.w,
h: this.player.rect.h
}
bullet.setPosition(rect2)
this.bulletList.push(bullet)
break
}
}
update(){
this.bg.update()
this.player.update()
if(this.player.dead){
this.gameOver = true
return
}
this.bulletList.forEach( bullet => {
bullet.update()
})
this.bulletList = this.bulletList.filter( bullet => !bullet.dead)
if(Math.random() < 0.06){
this.enemyList.push(new Enemy(this.ctx))
}
this.enemyList.forEach( enemy => {
enemy.update()
})
this.enemyList = this.enemyList.filter( enemy => !enemy.dead)
if(this.score.count > 20){
this.player.level = 2
}
this.bulletList.forEach( bullet => {
this.enemyList.forEach( enemy => {
if(rectCollide(bullet.rect, enemy.rect)){
bullet.kill()
if(!enemy.booming){
enemy.kill(this.player.vip)
if(enemy.lives <= 0){
this.score.add()
}
}
}
})
})
this.enemyList.forEach( enemy => {
if(rectCollide(enemy.rect, this.player.rect) && !enemy.booming){
enemy.kill(this.player.vip)
this.player.kill()
}
})
}
render(){
this.bg.render()
this.player.render()
this.bulletList.forEach(bullet =>{
bullet.render()
})
this.enemyList.forEach(enemy=>{
enemy.render()
})
this.score.render()
}
reset(){
this.gameOver = false
this.bg.reset()
this.player.reset()
this.score.reset()
this.bulletList = []
this.enemyList = []
}
setSize(w, h){
console.log("set")
this.size.w = w
this.size.h = h
}
}
export default new Status()
13.飞机碰撞 tools.js
function rectCollide(rectA,rectB){
const xMin = Math.max(rectA.x,rectB.x)
const yMin = Math.max(rectA.y,rectB.y)
const xMax = Math.min(rectA.x + rectA.w,rectB.x + rectB.w)
const yMax = Math.min(rectA.y + rectA.h,rectB.y + rectB.h)
const width = xMax - xMin
const height = yMax - yMin
if(width > 0 && height > 0){
return width * height
}else{
return 0
}
}
export default rectCollide
三、说在后头
在Player.js中 ,我写了个this.vip = 1 ,大家能想到什么嘛 (??ヮ?) ? (?ヮ??),只有充钱才能变得强大. 当然,身为一个作者,怎么能连自己的飞机都打不完呢?不不不,这样绝对不可以.( ?° ?? ?°)于是偶在Game.js 中,加入了作弊按钮哦,毕竟你可以用浏览器打开嘛~
window.addEventListener("keydown", e => {
if(e.key.toLowerCase() === "k"){
status.enemyList.forEach( enemy => {
enemy.dead = true
})
}
})
( ?° ?? ?°) ( ?° ?? ?°) ( ?° ?? ?°) ( ?° ?? ?°) 完整版代码呦~
|