飞机大战(微信小游戏)
依旧是熟悉的期末作业,是微信小游戏官网的Demo来着,然后我边百度边对它做了一些改动
说起来好久没更新博客了......最近在用unity做游戏准备参加计算机设计大赛,花了好久终于做完初稿了哈哈哈哈嗝
顺祝看到这篇博客的各位新年快乐~
PS:完整工程下载及相关说明:https://download.csdn.net/download/weixin_41918712/10954323
用例图
效果图
步骤
PS:图片/音频的素材下载:https://pan.baidu.com/s/1NrSonSJnSjm3VxiEUD4iVw 提取码: q7ya
- 打开微信开发者工具,新建一个微信小游戏Demo,然后进入编辑界面。
- 打开项目文件夹,将素材中的两个文件夹复制进项目文件夹里(替换)。
- 我们先把runtime里的3个脚本做些改动 ,分别是背景、文字UI显示和音乐控制。脚本详见下面【源码】。
- player文件夹里的index.js需要做些改动,主要是添加双发子弹的相关方法。
- npc文件夹需要增加的内容就有些多了,主要是增加了血包、双发子弹的生成方法,以及分数达到88时会开始产生能够发射子弹的敌人。这些新加的脚本就在npc文件夹 右键 - 新建JS 就行。
- 弄完上述这些,就可以改main.js、databus.js以及添加菜单脚本menu.js
- 设置游戏默认入口为菜单页
- 版本号
源码
runtime文件夹
background.js
import Sprite from '../base/sprite' const screenWidth = window.innerWidth const screenHeight = window.innerHeight const BG_IMG_SRC = 'images/bg.jpg' const BG_WIDTH = 512 const BG_HEIGHT = 768 /** * 游戏背景类 * 提供update和render函数实现无限滚动的背景功能 */ export default class BackGround extends Sprite { constructor(ctx) { super(BG_IMG_SRC, BG_WIDTH, BG_HEIGHT) this.render(ctx) this.top = 0 } update() { this.top += 2 if ( this.top >= screenHeight ) this.top = 0 } /** * 背景图重绘函数 * 绘制两张图片,两张图片大小和屏幕一致 * 第一张漏出高度为top部分,其余的隐藏在屏幕上面 * 第二张补全除了top高度之外的部分,其余的隐藏在屏幕下面 */ render(ctx) { ctx.drawImage( this.img, 0, 0, this.width, this.height, 0, -screenHeight + this.top, screenWidth, screenHeight ) ctx.drawImage( this.img, 0, 0, this.width, this.height, 0, this.top, screenWidth, screenHeight ) } }
gameinfo.js
const screenWidth = window.innerWidth const screenHeight = window.innerHeight let atlas = new Image() atlas.src = 'images/Common.png' /*面板组件构成*/ let menupic = new Image() menupic.src = 'images/menu.jpg' /*菜单构成*/ export default class GameInfo { renderGameScore(ctx, score) { ctx.fillStyle = "#ffffff" ctx.font = "20px Arial" ctx.fillText( '???? ', 10, 30 ) ctx.fillText( score, 40, 30 ) } //画出血量 renderGameHp(ctx, hp) { ctx.fillStyle = "#ff0000" ctx.font = "20px Arial" ctx.fillText( '❤ ', 13, 70 ) ctx.fillText( hp, 40, 70 ) ctx.fillText( ' / 3', 50, 70 ) } renderGameOver(ctx, score) //游戏结束 { ctx.drawImage(atlas, 0, 0, 119, 108, screenWidth / 2 - 150, screenHeight / 2 - 100, 300, 300) ctx.fillStyle = "#ffffff" ctx.font = "20px Arial" ctx.fillText( '游戏结束', screenWidth / 2 - 40, screenHeight / 2 - 100 + 50 ) ctx.fillText( '歼敌数: ' + score, screenWidth / 2 - 40, screenHeight / 2 - 100 + 130 ) /*面板绘制---红火火恍恍惚惚*/ ctx.drawImage( atlas, 120, 6, 39, 24, screenWidth / 2 - 60, screenHeight / 2 - 100 + 150, 120, 40 ) ctx.fillText ( '重新开始', screenWidth / 2 - 40, screenHeight / 2 - 100 + 175 ) ctx.drawImage( atlas, 120, 6, 39, 24, screenWidth / 2 - 60, screenHeight / 2 - 100 + 200, 120, 40 ) //返回菜单的ui布局 ctx.fillText( '返回菜单', screenWidth / 2 - 40, screenHeight / 2 - 100 + 225 ) /** * 重新开始按钮区域 * 方便简易判断按钮点击 */ this.btnArea_restart = { startX: screenWidth / 2 - 40, startY: screenHeight / 2 - 100 + 150, endX : screenWidth / 2 + 50, endY : screenHeight / 2 - 100 + 175 } //返回菜单的按钮区域判断 this.btnArea_remean = { startX: screenWidth / 2 - 40, startY: screenHeight / 2 - 100 + 200, endX: screenWidth / 2 + 50, endY: screenHeight / 2 - 100 + 225 } } renderMenu(ctx) //k开始菜单 { ctx.drawImage(menupic, 0, 0, screenWidth, screenHeight) /* * * 重新开始按钮区域 * 方便简易判断按钮点击 */ this.btnArea_start = { startX: screenWidth / 2 - 100, startY: screenHeight / 2 - 100 + 225, endX: screenWidth / 2 + 100, endY: screenHeight / 2 - 100 + 400 } } }
music.js
let instance //变量定义 /** * 统一的音效管理器 */ export default class Music { constructor() { if ( instance ) return instance instance = this /**音效 */ this.bgmAudio = new Audio() this.bgmAudio.loop = true this.bgmAudio.src = 'audio/bgm.mp3' this.shootAudio = new Audio() this.shootAudio.src = 'audio/bullet.mp3' this.boomAudio = new Audio() this.boomAudio.src = 'audio/boom.mp3' this.dieAudio = new Audio() this.dieAudio.src = 'audio/die.mp3' this.menuAudio = new Audio() this.menuAudio.loop = true this.menuAudio.src = 'audio/menu.mp3' this.bossAudio = new Audio() this.bossAudio.src = 'audio/bossbuttle.mp3' this.playBgm() } playBgm() { this.bgmAudio.currentTime = 0 this.bgmAudio.play() this.dieAudio.pause() this.menuAudio.pause() } playmenu() //播放菜单音乐 { this.menuAudio.currentTime = 0 this.menuAudio.play() this.bgmAudio.pause() this.dieAudio.pause() } stopBgm() { this.bgmAudio.pause() } playShoot() { this.shootAudio.currentTime = 0 this.shootAudio.play() } playboss() { this.bossAudio.currentTime = 0 this.bossAudio.play() } playExplosion() { this.boomAudio.currentTime = 0 this.boomAudio.play() } playDie() { this.dieAudio.currentTime = 0 this.dieAudio.play() } }
runtime文件夹
index.js
import Sprite from '../base/sprite' import Bullet from './bullet' import DataBus from '../databus' //import main from '../main' const screenWidth = window.innerWidth const screenHeight = window.innerHeight let skill = 0; //获取15发双弹头 // 玩家相关常量设置 const PLAYER_IMG_SRC = 'images/hero.png' const PLAYER_WIDTH = 80 const PLAYER_HEIGHT = 80 let databus = new DataBus() //let main1=new main() export default class Player extends Sprite { constructor() { super(PLAYER_IMG_SRC, PLAYER_WIDTH, PLAYER_HEIGHT) // 玩家默认处于屏幕底部居中位置 this.x = screenWidth / 2 - this.width / 2 this.y = screenHeight - this.height - 30 // 用于在手指移动的时候标识手指是否已经在飞机上了 this.touched = false this.bullets = [] // 初始化事件监听 this.initEvent() } /** * 当手指触摸屏幕的时候 * 判断手指是否在飞机上 * @param {Number} x: 手指的X轴坐标 * @param {Number} y: 手指的Y轴坐标 * @return {Boolean}: 用于标识手指是否在飞机上的布尔值 */ checkIsFingerOnAir(x, y) { const deviation = 30 return !!( x >= this.x - deviation && y >= this.y - deviation && x <= this.x + this.width + deviation && y <= this.y + this.height + deviation ) } /** * 根据手指的位置设置飞机的位置 * 保证手指处于飞机中间 * 同时限定飞机的活动范围限制在屏幕中 */ setAirPosAcrossFingerPosZ(x, y) { let disX = x - this.width / 2 let disY = y - this.height / 2 if ( disX < 0 ) disX = 0 else if ( disX > screenWidth - this.width ) disX = screenWidth - this.width if ( disY <= 0 ) disY = 0 else if ( disY > screenHeight - this.height ) disY = screenHeight - this.height this.x = disX this.y = disY } /** * 玩家响应手指的触摸事件 * 改变战机的位置 */ initEvent() { canvas.addEventListener('touchstart', ((e) => { //封装的指控方法 e.preventDefault() //匿名方法 let x = e.touches[0].clientX let y = e.touches[0].clientY // if ( this.checkIsFingerOnAir(x, y) ) { this.touched = true this.setAirPosAcrossFingerPosZ(x, y) } }).bind(this)) canvas.addEventListener('touchmove', ((e) => { //封装的指控方法 e.preventDefault() //匿名方法 let x = e.touches[0].clientX let y = e.touches[0].clientY if ( this.touched ) this.setAirPosAcrossFingerPosZ(x, y) }).bind(this)) canvas.addEventListener('touchend', ((e) => { //封装的指控方法 e.preventDefault() //匿名方法 this.touched = false }).bind(this)) } /** * 玩家射击操作 * 射击时机由外部决定 */ diffbuttle() //双弹头每秒消耗一个 { if(skill>0) skill--; } addbuttle() //增加双弹头 { skill+=15; } defaultbuttle() { skill=0; } shoot() //射击 { let bullets = [] let bulletNum; if (skill > 0) { bulletNum = 2; //子弹数 } else bulletNum = 1; for (let i = 0; i < bulletNum; i++) bullets.push(databus.pool.getItemByClass('bullet', Bullet)) bullets.forEach((bullet, index) => { bullet.init( this.x + this.width * (index + 1) / (bulletNum + 1) - bullet.width / 2, this.y - 10, 10 ) databus.bullets.push(bullet) }) } }
npc文件夹
enemy.js
import Animation from '../base/animation' import DataBus from '../databus' const ENEMY_IMG_SRC = 'images/enemy.png' const ENEMY_WIDTH = 60 const ENEMY_HEIGHT = 60 /** 速度初始 */ const __ = { speed: Symbol('speed') } let databus = new DataBus() function rnd(start, end){ return Math.floor(Math.random() * (end - start) + start) } export default class Enemy extends Animation { constructor() { super(ENEMY_IMG_SRC, ENEMY_WIDTH, ENEMY_HEIGHT) this.initExplosionAnimation() } /** 速度值初始化 */ init(speed) { this.x = rnd(0, window.innerWidth - ENEMY_WIDTH) this.y = -this.height this[__.speed] = speed this.visible = true } // 预定义爆炸的帧动画 initExplosionAnimation() { let frames = [] const EXPLO_IMG_PREFIX = 'images/explosion' const EXPLO_FRAME_COUNT = 19 for ( let i = 0;i < EXPLO_FRAME_COUNT;i++ ) { frames.push(EXPLO_IMG_PREFIX + (i + 1) + '.png') } this.initFrames(frames) } // 每一帧更新敌人位置 update() { this.y += this[__.speed] // 对象回收 if ( this.y > window.innerHeight + this.height ) databus.removeEnemey(this) } }
getskill.js
import Animation from '../base/animation' import DataBus from '../databus' const SKILL_IMG_SRC = 'images/upbull.png' const SKILL_WIDTH = 30 const SKILL_HEIGHT = 40 /** 速度初始 */ const __ = { speed: Symbol('speed') } let databus = new DataBus() function rnd(start, end) { return Math.floor(Math.random() * (end - start) + start) } export default class Getskill extends Animation { constructor() { super(SKILL_IMG_SRC, SKILL_WIDTH, SKILL_HEIGHT) //初始化技能 } /** 速度值初始化 */ init(speed) { this.x = rnd(0, window.innerWidth - SKILL_WIDTH) this.y = -this.height this[__.speed] = speed this.visible = true } // 每一帧更新敌人位置 update() { this.y += this[__.speed] // 对象回收 if (this.y > window.innerHeight + this.height) databus.removeSkill(this) } }
blood.js
import Animation from '../base/animation' import DataBus from '../databus' const BLOOD_IMG_SRC = 'images/blood.png' const BLOOD_WIDTH = 60 const BLOOD_HEIGHT = 60 /** 速度初始 */ const __ = { speed: Symbol('speed') } let databus = new DataBus() function rnd(start, end) { return Math.floor(Math.random() * (end - start) + start) } export default class Blood extends Animation { constructor() { super(BLOOD_IMG_SRC, BLOOD_WIDTH, BLOOD_HEIGHT) //初始化技能 } /** 速度值初始化 */ init(speed) { this.x = rnd(0, window.innerWidth - BLOOD_WIDTH) this.y = -this.height this[__.speed] = speed this.visible = true } // 每一帧更新敌人位置 update() { this.y += this[__.speed] // 对象回收 if (this.y > window.innerHeight + this.height) databus.removeblood(this) } }
smallboss.js
import Animation from '../base/animation' import DataBus from '../databus' import BoBullet from './BoBullet' const SMB_IMG_SRC = 'images/boss.png' const SMB_WIDTH = 80 const SMB_HEIGHT = 80 /** 速度初始 */ const __ = { speed: Symbol('speed') } let databus = new DataBus() //let isdie=false function rnd(start, end) { return Math.floor(Math.random() * (end - start) + start) } export default class SBoss extends Animation { constructor() { super(SMB_IMG_SRC, SMB_WIDTH, SMB_HEIGHT) this.initSMBAnimation() } /** 速度值初始化 */ init(speed) { this.x = rnd(0, window.innerWidth - SMB_WIDTH) this.y = -this.height this[__.speed] = speed this.visible = true } // 预定义爆炸的帧动画 initSMBAnimation() { let frames = [] const SMB_IMG_PREFIX = 'images/explosion' const SMB_FRAME_COUNT = 19 for (let i = 0; i < SMB_FRAME_COUNT; i++) { frames.push(SMB_IMG_PREFIX + (i + 1) + '.png') } this.initFrames(frames) } // 每一帧更新敌人位置 update() { this.y += this[__.speed] // 对象回收 if (this.y > window.innerHeight + this.height) databus.removeSBoss(this) } bossshoot() //敌人攻击 { let bullet = databus.pool.getItemByClass('bobullet', BoBullet) bullet.init //初始化子弹 ( this.x + this.width / 2 - bullet.width / 2, this.y + 20, 10 ) databus.bossbullets.push(bullet) } }
BoBullet.js
import Sprite from '../base/sprite' import DataBus from '../databus' const BossBULLET_IMG_SRC = 'images/bossbu.png' const BossBULLET_WIDTH = 20 const BossBULLET_HEIGHT = 34 const __ = { speed: Symbol('speed') } let databus = new DataBus() export default class BoBullet extends Sprite { constructor() { super(BossBULLET_IMG_SRC, BossBULLET_WIDTH, BossBULLET_HEIGHT) // this.initSMBAnimation() } init(x, y, speed) { this.x = x this.y = y this[__.speed] = speed this.visible = true } /*// 预定义爆炸的帧动画 initSMBAnimation() { let frames = [] const SMB_IMG_PREFIX = 'images/explosion' const SMB_FRAME_COUNT = 19 for (let i = 0; i < SMB_FRAME_COUNT; i++) { frames.push(SMB_IMG_PREFIX + (i + 1) + '.png') } this.initFrames(frames) }*/ // 每一帧更新子弹位置 update() { this.y += this[__.speed] // 超出屏幕外回收自身 if (this.y > window.innerHeight + this.height) databus.removeBoBullets(this) } }
主函数
databus.js
import Pool from './base/pool' let instance /** * 全局状态管理器 */ export default class DataBus { constructor() { if ( instance ) return instance instance = this this.pool = new Pool() this.reset() } reset() { this.frame = 0 this.score = 0 this.bullets = [] this.enemys = [] this.animations = [] this.gameOver = false } /** * 回收敌人,进入对象池 * 此后不进入帧循环 */ removeEnemey(enemy) { let temp = this.enemys.shift() temp.visible = false this.pool.recover('enemy', enemy) } /** * 回收子弹,进入对象池 * 此后不进入帧循环 */ removeBullets(bullet) { let temp = this.bullets.shift() temp.visible = false this.pool.recover('bullet', bullet) } }
main.js
import Player from './player/index' import Sboss from './npc/smallboss' import Enemy from './npc/enemy' import GetSkill from './npc/getskill' import Blood from './npc/blood' import BackGround from './runtime/background' import GameInfo from './runtime/gameinfo' import Music from './runtime/music' import DataBus from './databus' import BoBullet from './npc/BoBullet' import Menu from './menu' let ctx = canvas.getContext('2d') let databus = new DataBus() //let skill=0 //获取双弹头 /** * 游戏主函数 */ export default class Main { constructor() { this.restart() } /**开始ui绘制 */ /**重新开始的初始化 */ restart() { databus.reset() canvas.removeEventListener( 'touchstart', this.touchHandler ) this.bg = new BackGround(ctx) this.player = new Player(ctx) this.gameinfo = new GameInfo() this.music = new Music() this.music.playBgm() this.player.defaultbuttle() window.requestAnimationFrame( this.loop.bind(this), canvas ) } /** * 随着帧数变化的敌机生成逻辑 * 帧数取模定义成生成的频率 */ enemyGenerate() { //每秒30帧出现一次 if (databus.frame % 30 === 0) //databus.frame % 30 === 0 { let enemy = databus.pool.getItemByClass('enemy', Enemy) enemy.init(5) //speed初始化 databus.enemys.push(enemy) } } sbossGenerate() { //每秒30帧1/3概率出现一次大怪 if (databus.score == 88) wx.showToast({ title: '难度提高...' }); if (databus.score > 88) { if (databus.frame % 30 === 0) //databus.frame % 30 === 0 { let num = Math.floor(Math.random() * 2) //0--3 if (num == 1) { let sboss = databus.pool.getItemByClass('sboss', Sboss) sboss.init(4) //speed初始化 databus.sboss.push(sboss) } } } } //技能生成 skillGenerate() { let num = Math.floor(Math.random() * 1000) //0--10 if (num < 2) { let skill = databus.pool.getItemByClass('skill', GetSkill) skill.init(6) databus.skills.push(skill) } } //血块生成 bloodGenerate() { let num = Math.floor(Math.random() * 1000) //0--10 if (num < 1) { let blood = databus.pool.getItemByClass('blood', Blood) blood.init(6) databus.bloods.push(blood) } } // 全局碰撞检测 collisionDetection() { let that = this databus.bullets.forEach((bullet) => { for (let i = 0, il = databus.enemys.length; i < il; i++) //子弹打小怪 { let enemy = databus.enemys[i] if (!enemy.isPlaying && enemy.isCollideWith(bullet)) { enemy.playAnimation() that.music.playExplosion() bullet.visible = false databus.score += 1 break } } for (let i = 0, il = databus.sboss.length; i < il; i++)//子弹打大怪 { let sboss = databus.sboss[i] if (!sboss.isPlaying && sboss.isCollideWith(bullet)) { sboss.playAnimation() that.music.playExplosion() bullet.visible = false databus.score += 2 databus.removeSBoss(sboss) break } } for (let i = 0, il = databus.bossbullets.length; i < il; i++) //子弹打子弹 { let enemy = databus.bossbullets[i] if (enemy.isCollideWith(bullet)) { that.music.playExplosion() bullet.visible = false enemy.visible = false break } } }) //获得技能 for (let i = 0, il = databus.skills.length; i < il; i++) { let skill = databus.skills[i] if (this.player.isCollideWith(skill)) { this.player.addbuttle() skill.visible = false wx.showToast({ title: '获得双倍火力!' }) break } } //获得血块 for (let i = 0, il = databus.bloods.length; i < il; i++) { let blood = databus.bloods[i] if (this.player.isCollideWith(blood)) { blood.visible = false if (databus.hp < 3) { wx.showToast({ title: '获得生命回复!' }) databus.hp += 1 } break } } /**游戏失败 */ for (let i = 0, il = databus.enemys.length; i < il; i++) { let enemy = databus.enemys[i] if (this.player.isCollideWith(enemy)) { enemy.playAnimation() that.music.playExplosion() databus.hp -= 1 if (databus.hp <= 0) databus.gameOver = true break } } for (let i = 0, il = databus.bossbullets.length; i < il; i++) //被敌人子弹打到 { let enemy = databus.bossbullets[i] if (this.player.isCollideWith(enemy)) { //enemy.playAnimation() that.music.playExplosion() databus.hp -= 1 enemy.visible = false; if (databus.hp <= 0) databus.gameOver = true break } } for (let i = 0, il = databus.sboss.length; i < il; i++) { let sboss = databus.sboss[i] if (this.player.isCollideWith(sboss)) { sboss.playAnimation() that.music.playExplosion() databus.hp -= 1 databus.removeSBoss(sboss) if (databus.hp <= 0) databus.gameOver = true break } } } //游戏结束后的触摸事件处理逻辑 touchEventHandler(e) { e.preventDefault() let x = e.touches[0].clientX let y = e.touches[0].clientY //获取结束时按钮面板信息 let area = this.gameinfo.btnArea_restart let area_mean = this.gameinfo.btnArea_remean //按钮事件监听 if (x >= area.startX && x <= area.endX && y >= area.startY && y <= area.endY) this.restart() if (x >= area_mean.startX && x <= area_mean.endX && y >= area_mean.startY && y <= area_mean.endY) { canvas.removeEventListener( 'touchstart', this.touchHandler ) new Menu() } } /** * canvas重绘函数 * 每一帧重新绘制所有的需要展示的元素 */ render() { ctx.clearRect(0, 0, canvas.width, canvas.height) this.bg.render(ctx) databus.bullets .concat(databus.enemys) .concat(databus.skills) .concat(databus.bloods) .concat(databus.sboss) .concat(databus.bossbullets) .forEach((item) => { item.drawToCanvas(ctx) }) this.player.drawToCanvas(ctx) databus.animations.forEach((ani) => { if (ani.isPlaying) { ani.aniRender(ctx) } }) this.gameinfo.renderGameScore(ctx, databus.score) this.gameinfo.renderGameHp(ctx, databus.hp) } // 游戏逻辑更新主函数 update() { this.bg.update() databus.bullets .concat(databus.enemys) .concat(databus.skills) .concat(databus.bloods) .concat(databus.bossbullets) .concat(databus.sboss) .forEach((item) => { item.update() }) this.enemyGenerate() this.skillGenerate() this.bloodGenerate() this.collisionDetection() this.sbossGenerate() } // 实现游戏帧循环 loop() { databus.frame++ this.update() this.render() if (databus.frame % 20 === 0) { this.player.shoot() this.player.diffbuttle() this.music.playShoot() for (let i = 0, il = databus.sboss.length; i < il; i++) { //if (databus.sboss[i].IsDie()) continue; let num = Math.floor(Math.random() * 4) //0--3 if (num == 1) { let sboss = databus.sboss[i] sboss.bossshoot() this.music.playboss() } } } // 游戏结束停止帧循环 if (databus.gameOver) { this.gameinfo.renderGameOver(ctx, databus.score) this.music.playDie() this.music.stopBgm() this.touchHandler = this.touchEventHandler.bind(this) canvas.addEventListener('touchstart', this.touchHandler) return } window.requestAnimationFrame( this.loop.bind(this), canvas ) } }
menu.js
import GameInfo from './runtime/gameinfo' import Music from './runtime/music' import Main from './main' let ctx = canvas.getContext('2d') /** * 游戏主函数 */ export default class Menu { constructor() { this.remenu() } /**菜单绘制 */ remenu() { this.music = new Music() this.gameinfo = new GameInfo() this.music.playmenu() this.gameinfo.renderMenu(ctx) this.touchHandler = this.touchEventHandler.bind(this) canvas.addEventListener('touchstart', this.touchHandler) } /**菜单按钮监听 */ //游戏结束后的触摸事件处理逻辑 touchEventHandler(e) { e.preventDefault() let x = e.touches[0].clientX let y = e.touches[0].clientY //获取结束时按钮面板信息 let area = this.gameinfo.btnArea_start //按钮事件监听 if (x >= area.startX && x <= area.endX && y >= area.startY && y <= area.endY) { canvas.removeEventListener( 'touchstart', this.touchHandler ) new Main() } } }
入口
game.js
import './js/libs/weapp-adapter' import './js/libs/symbol' import Main from './js/main' import Menu from './js/menu' new Menu()
✎﹏﹏₯㎕《晴天》花落的那一天...﹍﹍﹍﹍﹍﹍