简易版飞机大战
html页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
canvas{
display: block;
margin: 100px auto;
border: 1px solid #ccc;
/* 鼠标变为手掌形状 */
cursor: pointer;
}
</style>
<!-- 引入js文件 -->
<script src="js/util.js"></script>
<script src="js/config.js"></script>
<script src="js/game.js"></script>
<script src="js/background.js"></script>
<script src="js/hero.js"></script>
<script src="js/bullet.js"></script>
<script src="js/enemy.js"></script>
<script src="js/bomb.js"></script>
<script src="js/food.js"></script>
</head>
<body>
<canvas width="512px" height="718px"></canvas>
<script>
// 在这个项目中创建和使用类的步骤: 1:创建一个JS文件在文件中创建一个类 2: 在html文件中引入这个类 3: 创建类的对象
// 创建画布和画笔
let canvas=document.querySelector("canvas")
let cxt=canvas.getContext("2d")
// 创建一个游戏的对象
let game=new Game(canvas,cxt)
//调用游戏的开始方法开始游戏
game.start()
</script>
</body>
</html>
Js页面
1.工具函数
//工具函数
function createImage(url){
//创建图片对象
let img=new Image();
img.src=url;
return img;
}
//判断任意两个组件是否发生碰撞
function collideCompent(com1,com2){
if(com1.x<=com2.x+com2.width&&com1.x+com1.width>=com2.x&&com1.y<=com2.y+com2.height&&com1.y+com1.height>=com2.y){
return true;
}
return false;
}
2.配置(定义、声明、赋值)
const WIDTH = 512
const HEIGHT = 768
// 定义游戏的状态
const STATE_START = 1; /// 开始状态
const STATE_RUNNING = 2; // 运行状态
const STATE_PAUSED = 3; // 暂停状态
const STATE_END = 4; // 结束状态
// 定义英雄的初始血量(生命值)
const HERO_HP = 3
// 定义一个子弹的默认间隔
const INTERVAL = 15; //子弹间隔次数, 值越小,越密集
// 英雄数组,可以切换英雄图片
var HEROS = [{
width: 116,
height: 92,
imgX: 393,
imgY: 102,
background: createImage("img/img_plane_main.png")
}, {
width: 118,
height: 100,
imgX: 393,
imgY: 0,
background: createImage("img/img_plane_main.png")
}, {
width: 116,
height: 94,
imgX: 127,
imgY: 107,
background: createImage("img/img_plane_main.png")
}, {
width: 130,
height: 106,
imgX: 137,
imgY: 0,
background: createImage("img/img_plane_main.png")
}];
// 定义子弹对象
var BULLET = {
width: 14,
height: 32,
speed: [4, 10],
imgX: 335,
imgY: 171,
background: createImage("img/img_bullet.png")
}
// 定义敌机数组
var ENEMYS = [{
width: 98,
height: 76,
speed: 3,
score: 100,
imgX: 267,
imgY: 474,
background: createImage("img/img_plane_enemy.png"),
hp: 1,
interval: 200
}, {
width: 104,
height: 76,
speed: 3,
score: 100,
imgX: 162,
imgY: 474,
background: createImage("img/img_plane_enemy.png"),
hp: 1,
interval: 300
}, {
width: 114,
height: 82,
speed: 8,
score: 1000,
imgX: 367,
imgY: 440,
background: createImage("img/img_plane_enemy.png"),
hp: 2,
interval: 450
}, {
width: 104,
height: 76,
speed: 3,
score: 200,
imgX: 162,
imgY: 474,
background: createImage("img/img_plane_enemy.png"),
hp: 2,
interval: 1100
}, {
width: 175,
height: 133,
speed: 2,
score: 500,
imgX: 190,
imgY: 340,
background: createImage("img/img_plane_enemy.png"),
hp: 5,
interval: 2300
}, {
width: 260,
height: 196,
speed: 2,
score: 2000,
imgX: 0,
imgY: 2,
background: createImage("img/img_plane_enemy.png"),
hp: 20,
interval: 4700
}];
// 爆炸图片
var BOMBIMGS = [
createImage("img/wsparticle_01.png"),
createImage("img/wsparticle_02.png"),
createImage("img/wsparticle_03.png"),
createImage("img/wsparticle_04.png"),
createImage("img/wsparticle_05.png"),
createImage("img/wsparticle_06.png"),
createImage("img/wsparticle_07.png"),
createImage("img/wsparticle_08.png"),
createImage("img/wsparticle_09.png"),
createImage("img/wsparticle_10.png")
];
const FOODS_HP = 1
const FOODS_SPEED = 2
const FOODS_BULLET = 3
// 食物
var FOODS = [
{
background: createImage("img/add_hp.png"),
type: FOODS_HP,
width: 60,
height: 35
}, {
background: createImage("img/drop_0.png"),
type: FOODS_SPEED,
width: 60,
height: 35
}
, {
background: createImage("img/drop_1.png"),
type: FOODS_BULLET,
width: 60,
height: 35
}
]
//得分图片数组,数字0-9
var scoreImgs = [
createImage("img/number_0.png"),
createImage("img/number_1.png"),
createImage("img/number_2.png"),
createImage("img/number_3.png"),
createImage("img/number_4.png"),
createImage("img/number_5.png"),
createImage("img/number_6.png"),
createImage("img/number_7.png"),
createImage("img/number_8.png"),
createImage("img/number_9.png"),
]
3.游戏类(总类)
// 创建一个游戏类
class Game {
//传入画布和画笔
constructor(canvas,cxt){
this.canvas=canvas;
this.cxt=cxt;
//创建属性
this.init()
}
init(){
//背景属性,也就是需要一个背景对象
this.bg=new Background(cxt);
//开始游戏的图片
this.startBgImg=createImage("img/start.png");
//结束游戏的图片
this.restartBgImg=createImage("img/restart.png");
//hp图片
this.hpImg=createImage("img/hp_0.png");
this.hpWidth=100;
this.hpHeight=10;
//创建英雄对象
this.hero=new Hero(cxt)
//敌机数组
this.enemyList=[];
//调用创建敌机方法的计数器
this.enemyIndex=0;
//爆炸对象数组
this.bombList=[];
//食物数组
this.foodList=[]
//16秒创建一个食物
this.foodInterval=800
//计数器
this.foodIndex=0
//游戏状态的属性
this.state=STATE_START;
//总分
this.totalScore=0;
}
//开始游戏的方法
start(){
//给画布添加点击事件
this.canvas.onclick=()=>{
//如果游戏是开始状态
if(this.state==STATE_START){
//游戏状态修改为运行状态
this.state=STATE_RUNNING
}else if(this.state==STATE_PAUSED){
//游戏状态修改为运行状态
this.state=STATE_RUNNING
//如果游戏是结束状态
}else if(this.state==STATE_END){
//重新创建属性,重新开始游戏
this.init()
//游戏状态修改为运行状态
this.state=STATE_RUNNING
}
}
//给画布添加一个鼠标移动事件,从e中可以获取到鼠标坐标
this.canvas.onmousemove = e => {
this.hero.move(e)
}
//添加监听键盘事件
window.onkeyup=(e)=>{
if(e.keyCode==32){//按下的是空格键
//修改游戏的状态为暂停状态
this.state=STATE_PAUSED;
}
}
window.setInterval(() => {
// 根据不同的游戏状态,画的界面也不相同
// 如果游戏的状态是开始状态,则背景只画出,不移动
if(this.state == STATE_START) { // 游戏开始状态
// 画出背景
this.bg.draw()
//画出hp图片
this.drawHp()
// 还要画一个开始游戏的图片
this.cxt.drawImage(this.startBgImg, (WIDTH - this.startBgImg.width)/ 2, HEIGHT - 200)
} else if(this.state == STATE_RUNNING) { // 游戏运行状态
// 画出背景
this.bg.update()
this.drawHp()
//游戏得分
this.drawScore()
// 画出英雄
this.hero.update()
// 调用创建敌机的方法
this.createEnemy()
//批量画出所有组件方法
this.batDrawCompents()
//各种组件碰撞检测
this.collideCompents()
//调用创建食物的方法
this.createFood()
//英雄死掉之后,游戏结束
if(!this.hero.live){
this.state=STATE_END
}
} else if(this.state == STATE_PAUSED) { // 游戏暂停状态
// 暂停状态什么都不要做,保持原来的界面状态就可以
}else if(this.state == STATE_END) { // 游戏结束状态
this.drawEnd()
}
}, 20)
}
//创建敌机,每隔20毫秒执行一次
createEnemy(){
//计数器
this.enemyIndex++;
//循环敌机数组
ENEMYS.forEach((enemyObj,index)=>{
//每隔不同敌机的间隔时间(enemyObj.interval)创建敌机
if(this.enemyIndex%enemyObj.interval==0){
//拿到循环到的敌机的索引,创建敌机对象
let enemy=new Enemy(this.cxt,index)
//将创建好的敌机对象push到敌机数组中
this.enemyList.push(enemy)
}
})
}
// 批量画出所有的组件 :敌机, 食物道具,爆炸效果
batDrawCompents() {
//批量画出敌机
this.enemyList.forEach((enemy,index)=>{
//死掉的和出界的敌机需要执行不同的操作,出界的敌机直接移除就可以,死掉的敌机(先产生爆炸效果,游戏得分后)移除
if(enemy.outOfBounds()){//出界
//移除
this.enemyList.splice(index,1)
}
if(!enemy.live){//死掉
//先爆炸再移除,产生爆炸
//创建爆炸对象
let bomb=new Bomb(enemy);
this.bombList.push(bomb);
//得分
this.totalScore+=enemy.score;
this.enemyList.splice(index,1)
}
//调用敌机的画出和移动方法
enemy.update()
})
// 批量画出食物
this.foodList.forEach((food,index)=>{
//移除出界的和死掉的食物
if(food.outOfBounds()||!food.live){
this.foodList.splice(index,1)
}
//调用食物的画出和移动方法
food.update()
})
// 批量画出爆炸效果
this.bombList.forEach((bomb,index)=>{
//先将死掉的爆炸从列表中移除
if(!bomb.live){
this.bombList.splice(index,1)
}
//调用爆炸的画出方法
bomb.draw()
})
}
//各种组件的碰撞检测方法 子弹和敌机,英雄和敌机,英雄和食物
collideCompents(){
//子弹和敌机的碰撞检测,画布上有子弹和敌机列表也就是有很多子弹和敌机
this.enemyList.forEach((enemy,enemyIndex)=>{
//和子弹列表中每个子弹判断是否碰撞
this.hero.bulletList.forEach((bullet,bulletIndex)=>{
//双方都存活的状态下发生碰撞
if(collideCompent(enemy,bullet)&&enemy.live&&bullet.live){//敌机和子弹发生碰撞
//发生碰撞后
bullet.collided()
enemy.collided()
}
})
//英雄和敌机碰撞
if(collideCompent(enemy,this.hero)&&enemy.live&&this.hero.live){
enemy.collided()
this.hero.collided()
}
})
//英雄和食物数组中食物的碰撞检测
this.foodList.forEach((food,index)=>{
if(collideCompent(food,this.hero)&&this.hero.live&&food.live){
//英雄吃掉食物(食物活着)
this.hero.eat(food)
//食物碰撞之后(死掉)
food.collided()
}
})
}
drawEnd(){
// 先画出背景
this.bg.draw()
// 透明度
this.cxt.fillStyle="rgba(0,0,0,0.5)"
//填充矩形
this.cxt.fillRect(0,0,WIDTH,HEIGHT)
//重新开始图片
this.cxt.drawImage(this.restartBgImg, (WIDTH - this.restartBgImg.width)/ 2, 300)
}
//创建食物
createFood(){
//计数器
this.foodIndex++
//间隔时间
if(this.foodIndex%this.foodInterval==0){
//创建食物对象
let food=new Food(this.cxt)
//将食物push进食物数组中
this.foodList.push(food)
}
}
//画出hp
drawHp(){
//hp图片
this.cxt.drawImage(this.hpImg,10,10,30,20)
//填充矩形和描边矩形
this.cxt.strokeStyle='red'
this.cxt.fillStyle='red'
this.cxt.strokeRect(50,15,this.hpWidth,this.hpHeight)
this.cxt.fillRect(50,15,this.hpWidth*this.hero.hp/HERO_HP,this.hpHeight)
}
//画出游戏得分
drawScore(){
//转为字符串格式,便于循环拿到每个值
let scoreStr=String(this.totalScore)//2700 "2700"
for(let i=0;i<scoreStr.length;i++){
//获取指定索引的字符
let c=scoreStr.charAt(i);
//将c作为图片数组的索引
let img=scoreImgs[c];
//画出得分
this.cxt.drawImage(img,400+15*i,15,15,25)
}
}
}
4.背景类
class Background {
//传入画笔
constructor(cxt){
this.cxt=cxt;
this.bgImg=createImage("img/img_bg_level_1.jpg")
//背景的y轴坐标
this.y1=0;
this.y2=-HEIGHT;
}
//画出背景
draw(){
//从画布的(0,0)位置开始画
this.cxt.clearRect(0,0,WIDTH,HEIGHT)//橡皮擦,擦除一张画布的面积
this.cxt.drawImage(this.bgImg,0,this.y1)
this.cxt.drawImage(this.bgImg,0,this.y2)
}
//移动背景
move(){
this.y1++
this.y2++;
if(this.y2==0){
this.y1=0;
this.y2=-HEIGHT;
}
}
update(){
this.draw()
this.move()
}
}
5.英雄类(己方)
// 创建一个英雄类
class Hero {
//构造函数只会调用一次
constructor(cxt){
this.cxt=cxt;
// 定义英雄类别的属性 : 属性的值有四个:0: 1排子弹 1:2排子弹 2:3排 3: 4排子弹
this.index=0;
//调用英雄图片方法,画出不同索引的英雄
this.init()
// 英雄坐标
this.x=(WIDTH-this.width)/2;
this.y=HEIGHT-this.height-50;
//英雄的存活状态
this.live=true;
//生命值
this.hp=HERO_HP;
//弹夹,实际上就是个子弹的数组,放子弹对象
this.bulletList=[];
// 创建子弹的间隔,也就是每调用多少次shoot方法,创建一个子弹
this.interval=INTERVAL;
// 需要一个递增的变量,没执行一次shoot方法,这个值就加1,相当于一个计数器,记录调用了多少次shoot方法
this.intervalCount=0;
//增加子弹速度
this.bulletSpeed=0
}
//英雄图片
init(){
this.bg=HEROS[this.index].background
this.imgX = HEROS[this.index].imgX
this.imgY = HEROS[this.index].imgY
this.width = HEROS[this.index].width
this.height = HEROS[this.index].height
}
//画出自己
draw(){
if(this.live)this.cxt.drawImage(this.bg, this.imgX, this.imgY, this.width, this.height, this.x, this.y, this.width, this.height)
}
//移动
move(e){
if(this.live){
// 通过修改英雄的坐标,只要英雄的坐标发生了修改,等下个20毫秒之后就会按照新的坐标进行绘制
//鼠标在飞机中心点
this.x=e.offsetX-this.width/2;
this.y=e.offsetY-this.height/2;
}
}
eat(food){
//判断吃掉的是什么类型的食物,不同类型的食物英雄要做的操作不同
if(food.type==FOODS_BULLET){
//增加子弹行数
if(this.index<3){
this.index++;
//画出索引增加后的英雄图片
this.init()
}
}else if(food.type==FOODS_HP){//加生命
if(this.hp<HERO_HP){
this.hp++;
}
}else if(food.type==FOODS_SPEED){//加速度
//子弹速度数组只有两个值0和1
this.bulletSpeed=1;
}
}
//发生碰撞后
collided(){
this.hp--;
if(this.hp==0){
this.live=false;
}
}
//射击方法, 每隔20毫秒执行1次, 一秒钟会产生50个子弹 300毫秒
shoot(){
if(this.live){
//下面创建子弹的代码,每执行15次shoot方法创建1次
this.intervalCount++;
if(this.intervalCount%this.interval==0){
//通过this.index判断要发射几排子弹
if(this.index==0){//1排子弹
//创建一个子弹对象
let bullet=new Bullet(this)
//将子弹扔到弹夹中
this.bulletList.push(bullet)
}else if(this.index==1){//2排
//创建两个子弹对象就行,这两个子弹对象的y轴坐标相同,x轴坐标不相同
let b1=new Bullet(this)
b1.x-=16;
let b2=new Bullet(this)
b2.x+=16;
this.bulletList.push(b1)
this.bulletList.push(b2)
}else if(this.index == 2) { // 3排
let b1 = new Bullet(this)
let b2 = new Bullet(this)
b2.x -= 32
let b3 = new Bullet(this)
b3.x += 32
this.bulletList.push(b1)
this.bulletList.push(b2)
this.bulletList.push(b3)
}else if(this.index == 3) { // 4排
let b1 = new Bullet(this)
b1.x -= 16
let b2 = new Bullet(this)
b2.x += 16
let b3 = new Bullet(this)
b3.x -= 48
let b4 = new Bullet(this)
b4.x += 48
this.bulletList.push(b1)
this.bulletList.push(b2)
this.bulletList.push(b3)
this.bulletList.push(b4)
}
}
}
}
//画出弹夹中所有子弹的方法
drawBulletList(){
//英雄存活状态下
if(this.live){
//通过循环拿到弹夹中的所有子弹对象, 挨个调用draw和move方法
this.bulletList.forEach((bullet,index)=>{
if(bullet.outOfBounds()||!bullet.live){//出界和死掉的子弹都要移除
//移除弹夹中的子弹
this.bulletList.splice(index,1)
}
//画出和移动子弹
bullet.update()
})
}
}
update(){
//画出英雄
this.draw()
//射击方法
this.shoot()
//画出弹夹中所有子弹
this.drawBulletList()
}
}
6.子弹类(随英雄产生)
//创建一个子弹类
class Bullet {
//子弹在英雄中产生
constructor(hero){
//传入英雄的画笔
this.cxt=hero.cxt;
this.bg=BULLET.background;
this.imgX = BULLET.imgX
this.imgY = BULLET.imgY
this.width = BULLET.width
this.height = BULLET.height
// 子弹的速度,得用变量(在英雄中)
this.speed = BULLET.speed[hero.bulletSpeed]
//子弹的坐标
this.x=hero.x + hero.width / 2 - this.width / 2
this.y=hero.y - this.height
//子弹的存活状态
this.live=true;
}
//画出子弹
draw() {
if(this.live) this.cxt.drawImage(this.bg, this.imgX, this.imgY, this.width, this.height, this.x, this.y, this.width, this.height)
}
//移动
move() {
// 修改子弹y轴的坐标
if(this.live) this.y -= this.speed
}
//发生碰撞后
collided(){
this.live=false;
}
update(){
this.draw()
this.move()
}
// 判断子弹是否出界
outOfBounds() {
if(this.y <= -this.height) {
return true
}
return false
}
}
7.敌机类
//创建敌机类
class Enemy {
//需要一个索引值确定画的是哪种敌机
constructor(cxt, index) {
this.cxt = cxt;
this.imgX = ENEMYS[index].imgX
this.imgY = ENEMYS[index].imgY
this.width = ENEMYS[index].width
this.height = ENEMYS[index].height
this.bg = ENEMYS[index].background
//敌机坐标,x轴坐标通过随机数获取
this.x = parseInt(Math.random() * (WIDTH - this.width))
this.y = -this.height
//敌机的速度
this.speed = ENEMYS[index].speed
//间隔时间
this.interval = ENEMYS[index].interval
this.hp = ENEMYS[index].hp // 敌机的初始生命值
this.score = ENEMYS[index].score // 敌机死亡后的分数
//敌机存活状态
this.live = true
}
//画出敌机
draw() {
if(this.live) this.cxt.drawImage(this.bg, this.imgX, this.imgY, this.width, this.height, this.x, this.y, this.width, this.height)
}
//移动敌机
move() {
if(this.live) this.y += this.speed
}
//发生碰撞后
collided(){
this.hp--;
if(this.hp==0){
this.live=false;
}
}
update() {
this.draw()
this.move()
}
// 判断敌机是否出界
outOfBounds() {
if(this.y >= HEIGHT) {
return true
}
return false
}
}
8.食物(工具)类
class Food{
constructor(cxt){
this.cxt=cxt;
//创建一个0--2的随机数,这个随机数可以从食物数组中获取一个食物对象
this.index=parseInt(Math.random()*3)
this.bg=FOODS[this.index].background
this.width=FOODS[this.index].width
this.height=FOODS[this.index].height
this.type=FOODS[this.index].type
//创建食物对象的坐标
this.x=parseInt(Math.random()*(WIDTH-this.width))
this.y=-this.height;
//食物存活状态
this.live=true;
//x轴速度2--6的随机数
this.speedX=2+parseInt(Math.random()*5)
//y轴2--5的随机数
this.speedY=2+parseInt(Math.random()*4)
}
//画出食物
draw(){
if(this.live)this.cxt.drawImage(this.bg,this.x,this.y,this.width,this.height)
}
//移动食物
move(){
//超出x轴速度取反
if(this.x<=0||this.x>=WIDTH-this.width){
this.speedX=-this.speedX;
}
this.x+=this.speedX;
this.y+=this.speedY;
}
//碰撞之后
collided(){
this.live=false;
}
update(){
this.draw()
this.move()
}
//判断食物是否出界
outOfBounds() {
if(this.y >= HEIGHT) {
return true
}
return false
}
}
9.爆炸类(敌机与子弹碰撞处产生)
class Bomb{
//爆炸在敌机与子弹碰撞的位置产生
constructor(enemy){
//传入敌机的画笔
this.cxt=enemy.cxt;
this.enemy=enemy;
//获取爆炸图片对象数组
this.imgs=BOMBIMGS;
//计数器,表示第几次调用draw方法
this.index=0;
//爆炸的存活状态
this.live=true;
}
//画出爆炸效果,需要以20毫秒为间隔画出十张图片,才算一次爆炸效果
//画完10张图后爆炸效果结束
draw(){
//敌机碰撞位置不固定,所以爆炸的x轴y轴位置在方法中设置
//爆炸存在 共10张图片
if(this.live&&this.index<this.imgs.length){
//画出第index张爆炸图片
//爆炸的坐标
this.x=this.enemy.x+(this.enemy.width-this.imgs[this.index].width)/2;
this.y=this.enemy.y+(this.enemy.height-this.imgs[this.index].height)/2;
this.cxt.drawImage(this.imgs[this.index],this.x,this.y)
this.index++;
}else{
this.live=false;
}
}
}