文章目錄
- 一、游戲總體框架
- 飛機大戰
- 總體框架:
- 運行方式
- 二、游戲總體內容
- 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
})
}
})
( ?° ?? ?°) ( ?° ?? ?°) ( ?° ?? ?°) ( ?° ?? ?°)
完整版代碼呦~
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/304113.html
標籤:其他
