文章目录
- 一、游戏总体框架
- 二、游戏总体内容
- 1.面向对象方案
- 2.游戏主函数的入口Game.js
- 3.这样一来,我们就需要这样一个文件 proto.js
- 4.上面配置都出来了,怎么可能游戏没有背景呢 Background.js
- 5.背景都有啦,咱们开始造玩家吧 Player.js
- 6.来给我方飞机来个'皮肤' playerConfig.js
- 7.咱也不可能打仗不带枪啊 Bullet.js
- 8.敌机来喽 Enemy.js
- 9.子弹爆裂图片 Boom.js
- 10.来看个分数吧 Score.js
- 11.继续努力吧(死亡重新开始喽) 弹出层 DialogModal.js
- 12.数据管理中心 不管是项目的尺寸还是碰撞,都在我这里哦Status.js
- 13.飞机碰撞 tools.js
- 三、说在后头
一、游戏总体框架
飞机大战
总体框架:
* 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";
//初始化游戏
//自动化做好所有事情 => 游戏初始化 (重启游戏)
//type="module" 所有代码的作用域都是在script中,不会去全局内
// let game = new Game(document.querySelector("body"))
//于是用以下方法导出到全局中
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";
/*
* 游戏主函数的入口,用来初始化项目(canvas生成,事件的生成,项目尺寸数据的生成)
* */
class Game{
constructor(container) {
this.container = container
//游戏的暂停状态,没有暂停的
this.paused = false
this.gameOver = false
//绑定this
this.render = this.render.bind(this)
this.pause = this.pause.bind(this)
this.continue = this.continue.bind(this)
//操控dom,初始化函数,当游戏实例化时,执行constructor,并走到init()中
this.initCanvas()
/*
重启游戏,包含第一次启动
*/
this.restartGame()
}
/*
* 初始化canvas标签,并设置尺寸,仅执行一次
* */
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)
// console.log("render")
// requestAnimationFrame(()=>{this.render()}) //也可以采用箭头函数来实现
if(this.paused) return
//把笔迹擦干净
// this.ctx.clearRect(0,0,this.size.w,this.size.h)
this.ctx.clearRect(0,0,...this.size)
/*首先 更新数据*/
status.update()
// 背景是 渲染在最底层的所以最先写
status.render()
// console.log(status.gameOver)
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(){
//将这个player之前添加的事件移除
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)
}
//TODO 游戏暂停(事件系统会关闭)『全局』
pause(){
clearInterval(this.fireTimer)
this.paused = true
}
//TODO 游戏继续
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
/*
拓展一些原型的方法
可以去遍历的对象属性值,我们用在...中
...解构对象
@returns {IterableIterator<*>}
*/
Object.prototype[Symbol.iterator] = function* (){
for (let i in this){
yield this[i]
}
}
Function.prototype.onceBind = (function (){
//这里的闭包只有1个,我们的bindMap只有属性名,属性值两个东西
//针对于不同的函数,绑定不同的内容
const bindMap = new Map()
return function (obj){
//需要对obj和函数进行关联 关联两个对象
if(!bindMap.get(obj)){
bindMap.set(obj,new Map())
}
//查询obj里面的map函数对应关系,每个函数 对应着 => 那个函数相同的bind返回的函数
if(!bindMap.get(obj).get(this)){
bindMap.get(obj).set(this,this.bind(obj))
}
return bindMap.get(obj).get(this)
}
})();
/*
* 以上 onceBind:
* 用来相同函数bind相同对象的时候,返回的函数与绑定之后的函数是完全一致的
*
* 例如:
* var obj = {};
* var foo = function(){
*
* }
* var foo1 = foo.onceBind(obj)
* var foo2 = foo.onceBind(obj)
* foo1 == foo2
* // true
*
* 函数 => 对象 => 唯一的绑定结果
*
* */
/* 一:
* 1.fn1.onceBind(obj1) => bindFn11 生成,存储,返回 第一次执行
* 2.再次执行 fn1.onceBind(obj1) 如果obj1已经在bindMap中,那么我就返回之前生成已存储的值
* 二:
* 3.fn2.onceBind(obj1) => 最开始并没找到,那我们就生成fn2 生成,存储,返回
* 三:
* 4:fn2.onceBind(obj2) => 最开始还是没有 我们创建 bindFn22
* 四:
* 5:fn1.onceBind(obj2) => bindFn12
* */
//一
// let bindMap = {
// obj1:{
// fn1:"bindFn11"
// }
// }
// //二
// let bindMap = {
// obj1:{
// fn1:"bindFn11",
// fn2:"bindFn21"
// }
// }
// //三
// let bindMap = {
// obj1:{
// fn1:"bindFn11",
// fn2:"bindFn21"
// },
// obj2:{
// fn2:"bindFn22"
// }
// }
// //四
// let bindMap = {
// obj1:{
// fn1:"bindFn11",
// fn2:"bindFn21"
// },
// obj2:{
// fn2:"bindFn22",
// fn1:"bindFn12"
// }
// }
/*
* Map是es6的方法:类似于对象,但比对象还要强大,键值对都可以是对象
* */
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,0,0,status.size.w,status.size.h)
// this.ctx.drawImage(this.img,0,0,...status.size)
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
//边界判断,瞬间归0
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
}
//如果处于boom的时候
if(this.booming && this.boomingCount < boomImgList.length){
this.img = boomImgList[this.boomingCount++]
}
if(this.boomingCount === boomImgList.length){
this.dead = true
}
}
//初始化当前飞机的事件
initEvent(dom){
//先说几个交互任务
//点是否在this.rect中 => 元素对象之间的交互(
// 1.点和矩形是否重合 => 矩形和点是否有重合区域
// 2.子弹和飞机交互 => 矩形和矩形是否有重合区域
// 3.敌机和我方飞机的交互 => 矩形和矩形是否有重合区域
// )
// 函数bind方法每次都返回的是一个全信的函数,自己封装一个基于函数和绑定主题的唯一的函数bind结果
// console.log("注册drag事件")
playerConfig.forEach(item=>{
item.handleList.forEach(fn =>{
dom.addEventListener(item.type,fn.onceBind(this))
})
})
}
//移除之前注册事件
//我们的bind每一次都返回新的函数,所以没有办法移除
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){
// console.log("touch")
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()
}
//如果是处于booming的状态,那就修改this.img
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
//杀死敌机了 杀死了
// this.dead = 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(){
// console.log(this.getMsg(), status.size.w - 50, 0, 50)
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.bullet.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
/*
* @param rectA :第一个矩形
* @param rectB :第二个矩形
* 计算这两个矩形 是否有重合的地方
* @return Number 表示是否重合
* */
function rectCollide(rectA,rectB){
//计算矩形的公有面积,如果面积大于0,那么就相交,否则就不相交
//理论应该是左上位置的值
//两个矩形左上角x坐标中的最大值
const xMin = Math.max(rectA.x,rectB.x)
//两个矩形左上角y坐标中的最大值
const yMin = Math.max(rectA.y,rectB.y)
//理论应该是右下位置的值
//两个矩形右下角x坐标中的最小值
const xMax = Math.min(rectA.x + rectA.w,rectB.x + rectB.w)
//两个矩形右下角y坐标中的最小值
const yMax = Math.min(rectA.y + rectA.h,rectB.y + rectB.h)
//计算宽高
const width = xMax - xMin
const height = yMax - yMin
//如果有面积,就可以返回大于0的数,否则返回0
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
})
}
})
( ͡° ͜ʖ ͡°) ( ͡° ͜ʖ ͡°) ( ͡° ͜ʖ ͡°) ( ͡° ͜ʖ ͡°)
完整版代码呦~