飞机大战案例
- 人员:杨标
- 时间:2019年8月28日
- 平台:HTML5
效果图
模块分析
根据效果图,我们可以把上面的游戏项目拆分成如下功能
我们根据思维导图把游戏的功能划分成如上图所示,大致的功能点也就围绕背景,飞机,子弹,爆炸以及道具这5点完成
根据基础功能分析 ,我们已经知道所有的模块都应该具备一个坐标,一个图片以及绘制自己的方法,所以,我们以面向对象的思维进行开发
基础布局
html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>飞机大战</title>
<link rel="stylesheet" href="./css/index.css" type="text/css">
</head>
<body>
<div class="main">
<canvas id="game" width="480px" height="850px"></canvas>
</div>
</body>
<script src="./js/index.js" type="text/javascript"></script>
</html>
css代码
@charset "utf-8";
*{
margin: 0px;
padding: 0px;
list-style-type: none;
}
.main{
width: 480px;
height: 850px;
border: 1px solid black;
margin: auto;
}
js代码
var game=document.querySelector("#game");
var ctx=game.getContext("2d");
基础功能的封装
基础功能主要指最基本的功能,所以的游戏对象应该都具备 坐标,图片以及绘制自己的方法,所以我们创建一个最基本的游戏对象GameObject
,以实现最基础的功能
首先先在js目录下面新建GameObject.js
的文件,完成如下代码
/**
* 游戏对象的封装
*/
function GameObject(x,y,img){
this.x=x;
this.y=y;
this.img=img;
this.draw=function(ctx){
ctx.drawImage(this.img,this.x,this.y);
}
}
游戏素材的加载
在游戏开始之前,我们要保证我们的所有素材都已经从服务器上面加载完成,所以现在,我们需要写一个方法去完成这个资源的加载
//配置一个对象
var gameConfig = {
hegiht:850,
//需要加载的资源
resource: [
"../Resources/background.png"
],
resObj: [], //加载好的资源
//加载游戏资源的方法
loadResource: function (callBack) {
var count = 0; //默认加载的资源数
for (var i = 0; i < this.resource.length; i++) {
var img = new Image();
img.src = this.resource[i];
this.resObj.push(img);
var that=this; //要保存外部的this
img.onload=function(){
count++;
if(count==that.resource.length){
//加载完成
if(typeof callBack=="function"){
callBack();
}
}
}
}
}
}
gameConfig.loadResource();
注意事项:在上面的img.onload
这个地方,在里面使用this
关键字的时候,一定要注意,它的this
指向经不在指向外边的gameConfig
,而是当前的DOM对象,所以我们要在外边加上一个var that=this
,保存外边的this
以方便在里面使用,同时还可以把上面的方法转成如下的方法
img.onload=function(){
count++;
if(count==this.resource.length){
//加载完成
if(typeof callBack=="function"){
callBack();
}
}
}.bind(this);
背景功能的完成
背景功能是游戏的第一个功能 ,我们可以在这个里面去使用面向对象的方式去封装它
/**
* @name 游戏背景对象
* @param x Number 横坐标
* @param y Number 纵坐标
* @param img Image 游戏对象图片
* @returns Object构造对象
*/
function Background(x, y, img) {
this.__proto__ = new GameObject(x, y, img)
this.speed = 3;
//我如果想让背景图移动,就必须让这个y不停的必变
this.move = function () {
this.y = this.y + this.speed;
if (this.y >= 0) {
this.y = gameConfig.height * (-1);
}
}
//当前子类方法覆盖掉了父级的drawImage
this.draw = function (ctx) {
//每次在画自己之前,让自己移动一下
this.move();
// ctx.drawImage(this.img,this.x,this.y);
this.__proto__.draw.call(this, ctx);
}
}
游戏的开始代码
gameConfig.loadResource(startGame);
//我只向外部公开这一个对象 这个容器只应该存放游戏对象
Object.defineProperty(window,"gameContainer",{
value:new Object()
})
//定义开始游戏的方法
function startGame(){
gameContainer.bg=new Background(0,-850,gameConfig.resObj[0]);
gameContainer.heroPlane=new HeroPlane(100,100,gameConfig.resObj[1]);
//当鼠标移动 玩家飞机移动
game.onmousemove = gameContainer.heroPlane.move;
setInterval(function(){
Object.keys(gameContainer).forEach(function(item,index,a){
//每一项属性名
if(typeof gameContainer[item]=="object"&&gameContainer[item] instanceof GameObject){
gameContainer[item].draw(ctx);
}
})
},50);
}
玩家飞机对象创建
我们先以简单的形式完成玩家飞机的创建过程,让这个玩家飞机继承GameObject
对象
/**
* @name 玩家飞机的对象
* @extends GameObject
* @param x Number 横坐标
* @param y Number 纵坐标
* @param img Image 游戏对象图片
* @returns Object构造对象
*/
function HeroPlane(x, y, img) {
this.__proto__ = new GameObject(x, y, img);
//我想把当前飞机的大小减少一半
this.width = this.img.width / 2;
this.height = this.img.height / 2;
this.move = function (event) {
var e = event || window.event;
var x = e.clientX - game.getBoundingClientRect().left;
var y = e.clientY - game.getBoundingClientRect().top;
if (gameContainer.heroPlane) {
gameContainer.heroPlane.x = x;
gameContainer.heroPlane.y = y;
}
}
}
思考 :玩家飞机默认是以图片大小为大小,现在如果要改变玩家飞机的大小,怎么办?
存在的问题一
-
在
gameConfig.resObj
这个数组是用于装加载好的游戏资源对象,它是通过索引取值,有一个非常大的弊端,如果索引改变,后期整个程序全部崩溃掉,所以我们不能以索引存储(如果是在后期,我们会以Set
集合来做),现在我们以对象来完成编写一个方法,转换路径名
//"../Resources/background.png" ---- background_png //你给我一个路径,我返回一个新的名子你 function getResourceName(pathName){ return pathName.split("/").pop().split(".").join("_"); }
更改loadResource方法里面的如下代码
// this.resObj.push(img); this.resObj[getResourceName(this.resource[i])]=img;
接下来再使用图片的时候,就可以直接调用属性名
function startGame(){ //......... gameContainer.bg = new Background(0, -850, gameConfig.resObj["background_png"]); gameContainer.heroPlane = new HeroPlane(100, 100, gameConfig.resObj["hero1_png"]); //....... }
-
路径的问题
当我们以JS代码为标准,进行分离代码编写的时候,会发现一个问题
index.js
里面的路径都是../Resource
这个时候,如果我们本地打开就会显示路径不正常,所以要以index.html
的路径 为标准resource: [ "./Resources/background.png", "./Resources/hero1.png" ]
本题要注意的就是
src
属性与href
属性的区别 -
玩家飞机移出边界的问题
当鼠标在移动的过程当中,我们会发现玩家飞机会移出边界,这个时候朵严格控制玩家飞机不能移出边界
现在在玩家飞机对象HeroPlane里面的移动方法
move
下面添加如下代码//在这里,我们要判断飞机的坐标是否移出去了 if(x>gameConfig.width-that.width){ x=gameConfig.width-that.width } if(y>gameConfig.height-that.height){ y=gameConfig.height-that.height; }
玩家子弹的创建
思考:现在我们已经开始了一部分功能了,当我们再次去添加新的功能的时候,应该从什么角度考虑问题或开始问题
步骤
- 创建对象
Bullet.js
- 在
index.js
的文件里面,找到resource属性,去添加你所需要加载的资源图片 - 在
index.html
文件里面用script标签去导入刚刚创建对象的JS文件 - 在
startGame
的方法里面去绘制你刚刚创建的这个对象Bullet
思考:
-
子弹是谁在操作,是谁发射的子弹?
-
屏幕上面是出现一颗子弹还是多颗子弹(集合把它装起来)?
gameContainer.bulletList=[]; //用于存放玩家飞机的子弹
-
玩家飞机的子弹是怎么发射了,是自己发射,还是需要玩家手动发射?
子弹对象
/**
* @name Bullet 玩家飞机的子弹
* @extends GameObject 游戏对象
* @param heroPlane 玩家飞机对象
* @param img 玩家子弹的图片
* @returns 玩家子弹对象
*/
function Bullet(heroPlane,img){
this.__proto__=new GameObject(heroPlane.x,heroPlane.y,img);
//修正子弹坐标
this.x=this.x+heroPlane.width/2-this.width/2;
this.speed=40;
//我应该让子弹移动起来
this.move=function(){
this.y=this.y-this.speed;
//当子弹跑到屏幕外边去了,移除这个子弹
if(this.y<=0){
gameContainer.bulletList.remove(this);
}
}
this.draw=function(ctx){
this.move();
this.__proto__.draw.call(this,ctx);
}
}
玩家飞机发射子弹
function HeroPlane(){
//....... 代码省略
that.isTwo = false; //是否是双排子弹
//玩家飞机发射子弹的
this.fire = function () {
if (that.isTwo) {
//说明经发双排子弹
var bullet_left = new Bullet(that, gameConfig.resObj["bullet2_png"]);
bullet_left.x=bullet_left.x-that.width/4;
var bullet_right = new Bullet(that, gameConfig.resObj["bullet2_png"]);
bullet_right.x=bullet_right.x+that.width/4;
gameContainer.bulletList.push(bullet_left,bullet_right);
} else {
var bullet=new Bullet(that,gameConfig.resObj["bullet2_png"]);
gameContainer.bulletList.push(bullet);
}
}
//在此处,每隔多长时间就要调用上面的方法 发射一颗子弹
this.fireId = setInterval(this.fire, 250);
}
游戏对象界面绘画子弹的方法
setInterval(function () {
Object.keys(gameContainer).forEach(function (item, index, a) {
//每一项属性名
if (typeof gameContainer[item] == "object" && gameContainer[item] instanceof GameObject) {
gameContainer[item].draw(ctx);
}
else if(typeof gameContainer[item]=="object" && gameContainer[item] instanceof Array){
gameContainer[item].forEach(function(item1,index1,a1){
if(typeof item1=="object"&&item1 instanceof GameObject){
item1.draw(ctx);
}
})
}
});
}, 50);
敌人飞机完成
分析
- 敌人飞机有三类种类型
- 使用随机数确定三种类型的飞机,大飞机概率5%,中飞机25%,小飞机70%的概率
- 敌人飞机应该比普通的飞机多一个属性类型(type),我们使用0代表小飞机,1代表中飞机,2代表大飞机
- 敌人的飞机应该也会同时出现多架,所以应该也是在容器里面用一个数组去盛放
- 如何确定敌人飞机开始的坐标,要使用随机数产生一个敌人飞机的横坐标
- 游戏界面上面最多能够允许出现多少架飞机(大多打不赢,太少打得不过瘾),以6架为准
- 敌人的飞机是如何移动的(请参考背景图的移动)
/**
* @name EnemyPlane 敌机对象
* @description 横坐标随机,类型随机,大飞机概率5%,中飞机25%,小飞机70%的概率
* @extends GameObject 游戏对象
*/
function EnemyPlane() {
Object.defineProperty(this, "type", {
set: function (v) {
this._type = v;
if (v == 0) {
this.img = gameConfig.resObj["enemy0_png"];
this.speed = 8;
} else if (v == 1) {
this.img = gameConfig.resObj["enemy1_png"];
this.speed = 5;
} else if (v == 2) {
this.img = gameConfig.resObj["enemy2_png"];
this.speed = 3;
}
},
get: function () {
return this._type;
}
})
var temp = parseInt(Math.random() * 100);
if (temp < 70) {
this.type = 0;
} else if (temp < 95) {
this.type = 1;
} else {
this.type = 2;
}
//注意:为了防止敌机的随机横坐出现在外边,所以随机数要减去自身的宽度
this.x = parseInt(Math.random() * (gameConfig.width - this.img.width));
this.__proto__ = new GameObject(this.x, this.img.height * (-1), this.img);
this.move = function () {
this.y = this.y + this.speed;
if (this.y > gameConfig.height) {
//把自己移除掉
gameContainer.enemyPlaneList.remove(this);
}
}
this.draw = function (ctx) {
this.move();
this.__proto__.draw.call(this, ctx);
}
}
在上面的代码里面,我们可以看到在定义
type
属性的时候,我们使用Object.defineProperty
的方法来访问器属性,这个做的目的是为了后期当我们去更改type
属性的值的时候,能够自动的去更改this.img
与this.speed
的值
添加敌机的方法
首先在对象gameConfig
里面添加一个属性maxEnemyPlaneCount
用于确定敌机的数量
然后定义如下方法
/**
* 检测敌机的数据
*/
function checkEnemyPlaneCount() {
//先获取敌机的数量
var count = gameContainer.enemyPlaneList.length;
if (count < gameConfig.maxEnemyPlaneCount) {
//说明敌机不够,你差多少,我就加多少
for (var i = 0; i < gameConfig.maxEnemyPlaneCount - count; i++) {
var enemyPlane = new EnemyPlane();
gameContainer.enemyPlaneList.push(enemyPlane);
}
}
}
最后在循环定时器绘制游戏对象的后面调用checkEnemyPlaneCount()
方法,以实现不停的检测敌机数量
游戏对象碰撞检测
玩家的子弹是否击中敌人的飞机其实本质就是检测两个对象是否有发生相交的行为(碰撞检测)
/**
* @name checkCrash
* @description 检测两个游戏对象是否发生相交
* @param {GameObject} a 第一个游戏对象
* @param {GameObject} b 第二个游戏对象
* @returns {Boolean} 返回是否相交的结果,true代表相交,false代表没有相交
*/
function checkCrash(a,b){
if(a.x+a.width<b.x||b.x+b.width<a.x||a.y+a.height<b.y||b.y+b.height<a.y){
//只要是这四种情况之中的任何一种,都没有发生相交
return false;
}
else{
//否则就发生了碰撞
return true;
}
}
检测玩家子弹与敌人飞机是否发生碰撞
/**
* @name checkCrashByBulletAndEnemyPlane 玩家子弹与敌人飞机是否发生碰撞
* @author 杨标
* @version 1.0
*/
function checkCrashByBulletAndEnemyPlane() {
//遍历所有的子弹
for (var i = gameContainer.bulletList.length - 1; i >= 0; i--) {
// 遍历所有的敌机
for (var j = gameContainer.enemyPlaneList.length - 1; j >= 0; j--) {
//调用checkCrash的方法去检测 子弹 与 敌机是否发生碰撞
var result = checkCrash(gameContainer.bulletList[i], gameContainer.enemyPlaneList[j]);
if (result) {
//说明子弹击中了敌人的飞机
//一颗子弹只能使用个次
gameContainer.bulletList.splice(i, 1);
gameContainer.enemyPlaneList[j].life--;
// 调用当前飞机的isDie方法,看自己死了没有
gameContainer.enemyPlaneList[j].isDie();
break; //跳出当前循环,进入下一颗子弹
}
}
}
}
在每次绘画自己之前,检测一下是否有发生碰撞,所以在startGame的循环定时器里面调用这个方法
敌人飞机的死亡
当通过isDie()的方法去检测敌机是否有死亡的时候,我们应该进行如下的操作
- 播放死亡音乐
- 记分
- 爆炸
当它没有死的时候,则应该进行如下操作
- 把飞机的图片把它换掉
//检测 自己 是否死亡
this.isDie = function () {
if (this.life <= 0) {
gameContainer.enemyPlaneList.remove(this);
//播放死亡音乐
var audio=document.createElement("audio");
audio.src="./Resources/enemy0_down.wav";
audio.play();
gameConfig.score+=this.score;
}
else{
//说明你是残血状态
this.img=this.hit_img;
}
}
敌机爆炸
在讲敌机爆炸之前是,先弄清楚二个点
- 敌机在哪里死亡,爆炸就在哪里产生;
- 你是什么飞机,你就应该是什么爆炸动画;
/**
* @name Boom 敌机爆炸动画
* @param {EnemyPlane} enemyPlane 敌机对象
*/
function Boom(enemyPlane) {
if (enemyPlane.type == 0) {
//小飞机
this.imgs = [
gameConfig.resObj["enemy0_down1_png"],
gameConfig.resObj["enemy0_down2_png"],
gameConfig.resObj["enemy0_down3_png"],
gameConfig.resObj["enemy0_down4_png"]
];
} else if (enemyPlane.type == 1) {
//中飞机
this.imgs = [
gameConfig.resObj["enemy1_down1_png"],
gameConfig.resObj["enemy1_down2_png"],
gameConfig.resObj["enemy1_down3_png"],
gameConfig.resObj["enemy1_down4_png"]
];
} else if (enemyPlane.type == 2) {
//大飞机
this.imgs = [
gameConfig.resObj["enemy2_down1_png"],
gameConfig.resObj["enemy2_down2_png"],
gameConfig.resObj["enemy2_down3_png"],
gameConfig.resObj["enemy2_down4_png"],
gameConfig.resObj["enemy2_down5_png"],
gameConfig.resObj["enemy2_down6_png"]
];
}
this.type = enemyPlane.type;
this.__proto__ = new GameObject(enemyPlane.x, enemyPlane.y, this.imgs[0]);
this.isPlay=false; //代表我自己的爆炸动画没有开始
//外边调用这个方法,我自己来画,我自己把自己画完,不要你们管
this.draw = function (ctx) {
if(this.isPlay==true){
return; //跳出当前方法,后面代码不执行
}
this.isPlay=true;
var that=this;
var count = 0;
var id = setInterval(function () {
ctx.drawImage(that.imgs[count], that.x, that.y, that.width, that.height);
count++;
if (count == that.imgs.length) {
//爆炸结束以后,移除当前对象
gameContainer.boomList.remove(that);
clearInterval(id);
}
}, 50);
}
}
道具功能的制作
道具有三种类型的道具,我们现在在这里完成如下的代码
/**
* @name Tools 游戏道具对象
* @extends GameObject 游戏对象
*/
function Tools() {
Object.defineProperty(this, "type", {
set: function (newValue) {
this._type = newValue;
if (newValue == 0) {
//双排子弹
this.img = gameConfig.resObj["prop_type_0_png"];
} else if (newValue == 1) {
//全屏爆炸
this.img = gameConfig.resObj["prop_type_1_png"];
} else if (newValue == 2) {
//加条命
this.img = gameConfig.resObj["plane_png"];
}
},
get: function () {
return this._type;
}
});
var temp = parseInt(Math.random() * 100)
if (temp < 50) {
this.type = 0;
} else if (temp < 80) {
this.type = 1;
} else {
this.type = 2;
}
//思考:现在是三种道具 ,如果后期我要添加其它道具
//随机产生横坐标
this.x = parseInt(Math.random() * (gameConfig.width - this.img.width));
this.__proto__ = new GameObject(this.x, this.img.height * (-1), this.img);
this.move = function () {
this.y =this.y + 15;
if(this.y>gameConfig.height){
gameContainer.tools.remove(this);
}
}
this.draw=function(ctx){
this.move();
this.__proto__.draw.call(this,ctx);
}
}
完成道具的代码以后,接着应该在游戏界面根据一定的游戏概念去添加道具
首先,我们先在游戏的容器里面去添加一个属性,用于存放游戏的道具gameContainer.tools=[]
接下添加一个方法,去完成下添加游戏道具的功能
/**
* @name addTools 添加道具的方法
*/
function addTools(){
var temp=parseInt(Math.random()*100);
if(temp<50&&gameContainer.tools.length<=0){
var t=new Tools();
gameContainer.tools.push(t);
}
}
最后给其一定的概率去添加道具,在startGame
里面,我们添加如下的功能代码
setInterval(function(){
addTools(); //添加道具
},10000);
上面的代码我们每隔10秒钟就检测一个随机数去添加道具
评论区