用微信小游戏实现龙舟大战-打粽子

端午节来啦!各位c粉有没有吃粽子啊!



前言

端午节来啦!今天沉默带大家来做个关于端午节的小游戏,我的设计思路是用龙舟打粽子,类似于飞机大战,只不过我们的场景是在河中。源码在文章后获取哟!


提示:以下是本篇文章正文内容,下面案例可供参考

一、体验视频

下面是小游戏的开发效果视频:

龙舟大战

二、开发流程

1.素材收集

龙舟大战,我们需要一张龙舟的图片和粽子的图片,这里我们还需要河面的背景图片。值得注意的是,龙舟打粽子还需要子弹的图片,为了体现端午节的元素,我们将子弹设定为粽子,当子弹接触到前方的粽子时,前方粽子爆炸特效也需要通过图片生成。具体图片素材如下图:

2.游戏逻辑实现

2.1 定义游戏开发基础类

  • 相关的代码说明请看注解
import Sprite from './sprite'
import DataBus from '../databus'const databus = new DataBus()const __ = {timer: Symbol('timer'),
}/*** 简易的帧动画类实现*/
export default class Animation extends Sprite {constructor(imgSrc, width, height) {super(imgSrc, width, height)// 当前动画是否播放中this.isPlaying = false// 动画是否需要循环播放this.loop = false// 每一帧的时间间隔this.interval = 1000 / 60// 帧定时器this[__.timer] = null// 当前播放的帧this.index = -1// 总帧数this.count = 0// 帧图片集合this.imgList = []/*** 推入到全局动画池里面* 便于全局绘图的时候遍历和绘制当前动画帧*/databus.animations.push(this)}/*** 初始化帧动画的所有帧* 为了简单,只支持一个帧动画*/initFrames(imgList) {imgList.forEach((imgSrc) => {const img = new Image()img.src = imgSrcthis.imgList.push(img)})this.count = imgList.length}// 将播放中的帧绘制到canvas上aniRender(ctx) {ctx.drawImage(this.imgList[this.index],this.x,this.y,this.width * 1.2,this.height * 1.2)}// 播放预定的帧动画playAnimation(index = 0, loop = false) {// 动画播放的时候精灵图不再展示,播放帧动画的具体帧this.visible = falsethis.isPlaying = truethis.loop = loopthis.index = indexif (this.interval > 0 && this.count) {this[__.timer] = setInterval(this.frameLoop.bind(this),this.interval)}}// 停止帧动画播放stop() {this.isPlaying = falseif (this[__.timer]) clearInterval(this[__.timer])}// 帧遍历frameLoop() {this.index++if (this.index > this.count - 1) {if (this.loop) {this.index = 0} else {this.index--this.stop()}}}
}

2.2 帧动画的简易实现

const __ = {poolDic: Symbol('poolDic')
}/*** 简易的对象池实现* 用于对象的存贮和重复使用* 可以有效减少对象创建开销和避免频繁的垃圾回收* 提高游戏性能*/
export default class Pool {constructor() {this[__.poolDic] = {}}/*** 根据对象标识符* 获取对应的对象池*/getPoolBySign(name) {return this[__.poolDic][name] || (this[__.poolDic][name] = [])}/*** 根据传入的对象标识符,查询对象池* 对象池为空创建新的类,否则从对象池中取*/getItemByClass(name, className) {const pool = this.getPoolBySign(name)const result = (pool.length? pool.shift(): new className())return result}/*** 将对象回收到对象池* 方便后续继续使用*/recover(name, instance) {this.getPoolBySign(name).push(instance)}
}

2.3 游戏基本元素精灵类

(粽子.子弹.击中特效)

/*** 游戏基础的精灵类*/
export default class Sprite {constructor(imgSrc = '', width = 0, height = 0, x = 0, y = 0) {this.img = new Image()this.img.src = imgSrcthis.width = widththis.height = heightthis.x = xthis.y = ythis.visible = true}/*** 将精灵图绘制在canvas上*/drawToCanvas(ctx) {if (!this.visible) returnctx.drawImage(this.img,this.x,this.y,this.width,this.height)}/*** 简单的碰撞检测定义:* 另一个精灵的中心点处于本精灵所在的矩形内即可* @param{Sprite} sp: Sptite的实例*/isCollideWith(sp) {const spX = sp.x + sp.width / 2const spY = sp.y + sp.height / 2if (!this.visible || !sp.visible) return falsereturn !!(spX >= this.x&& spX <= this.x + this.width&& spY >= this.y&& spY <= this.y + this.height)}
}

2.4 粽子类实现过程

import Animation from '../base/animation'
import DataBus from '../databus'const ENEMY_IMG_SRC = 'images/enemy.png'
const ENEMY_WIDTH = 60
const ENEMY_HEIGHT = 60const __ = {speed: Symbol('speed')
}const 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.heightthis[__.speed] = speedthis.visible = true}// 预定义爆炸的帧动画initExplosionAnimation() {const frames = []const EXPLO_IMG_PREFIX = 'images/explosion'const EXPLO_FRAME_COUNT = 19for (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)}
}

2.5 粽子子弹类实现

import Sprite from '../base/sprite'
import DataBus from '../databus'const BULLET_IMG_SRC = 'images/bullet.png'
const BULLET_WIDTH = 16
const BULLET_HEIGHT = 30const __ = {speed: Symbol('speed')
}const databus = new DataBus()export default class Bullet extends Sprite {constructor() {super(BULLET_IMG_SRC, BULLET_WIDTH, BULLET_HEIGHT)}init(x, y, speed) {this.x = xthis.y = ythis[__.speed] = speedthis.visible = true}// 每一帧更新子弹位置update() {this.y -= this[__.speed]// 超出屏幕外回收自身if (this.y < -this.height) databus.removeBullets(this)}
}

2.6 玩家类(龙舟)

import Sprite from '../base/sprite'
import Bullet from './bullet'
import DataBus from '../databus'const screenWidth = window.innerWidth
const screenHeight = window.innerHeight// 玩家相关常量设置
const PLAYER_IMG_SRC = 'images/hero.png'
const PLAYER_WIDTH = 80
const PLAYER_HEIGHT = 80const databus = new DataBus()export default class Player extends Sprite {constructor() {super(PLAYER_IMG_SRC, PLAYER_WIDTH, PLAYER_HEIGHT)// 玩家默认处于屏幕底部居中位置this.x = screenWidth / 2 - this.width / 2this.y = screenHeight - this.height - 30// 用于在手指移动的时候标识手指是否已经在龙舟上了this.touched = falsethis.bullets = []// 初始化事件监听this.initEvent()}/*** 当手指触摸屏幕的时候* 判断手指是否在龙舟上* @param {Number} x: 手指的X轴坐标* @param {Number} y: 手指的Y轴坐标* @return {Boolean}: 用于标识手指是否在龙舟上的布尔值*/checkIsFingerOnAir(x, y) {const deviation = 30return !!(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 / 2let disY = y - this.height / 2if (disX < 0) disX = 0else if (disX > screenWidth - this.width) disX = screenWidth - this.widthif (disY <= 0) disY = 0else if (disY > screenHeight - this.height) disY = screenHeight - this.heightthis.x = disXthis.y = disY}/*** 玩家响应手指的触摸事件* 改变龙舟的位置*/initEvent() {canvas.addEventListener('touchstart', ((e) => {e.preventDefault()const x = e.touches[0].clientXconst y = e.touches[0].clientY//if (this.checkIsFingerOnAir(x, y)) {this.touched = truethis.setAirPosAcrossFingerPosZ(x, y)}}))canvas.addEventListener('touchmove', ((e) => {e.preventDefault()const x = e.touches[0].clientXconst y = e.touches[0].clientYif (this.touched) this.setAirPosAcrossFingerPosZ(x, y)}))canvas.addEventListener('touchend', ((e) => {e.preventDefault()this.touched = false}))}/*** 玩家射击操作* 射击时机由外部决定*/shoot() {const bullet = databus.pool.getItemByClass('bullet', Bullet)bullet.init(this.x + this.width / 2 - bullet.width / 2,this.y - 10,10)databus.bullets.push(bullet)}
}

2.7 背景类(河面)

import Sprite from '../base/sprite'const screenWidth = window.innerWidth
const screenHeight = window.innerHeightconst BG_IMG_SRC = 'images/bg.jpg'
const BG_WIDTH = 512
const BG_HEIGHT = 512/*** 游戏背景类* 提供update和render函数实现无限滚动的背景功能*/
export default class BackGround extends Sprite {constructor(ctx) {super(BG_IMG_SRC, BG_WIDTH, BG_HEIGHT)this.top = 0this.render(ctx)}update() {this.top += 2if (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)}
}

2.8 展示分数和结算界面实现

const screenWidth = window.innerWidth
const screenHeight = window.innerHeightconst atlas = new Image()
atlas.src = 'images/Common.png'export default class GameInfo {renderGameScore(ctx, score) {ctx.fillStyle = '#ffffff'ctx.font = '20px Arial'ctx.fillText(score,10,30)}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 + 180,120, 40)ctx.fillText('重新开始',screenWidth / 2 - 40,screenHeight / 2 - 100 + 205)/*** 重新开始按钮区域* 方便简易判断按钮点击*/this.btnArea = {startX: screenWidth / 2 - 40,startY: screenHeight / 2 - 100 + 180,endX: screenWidth / 2 + 50,endY: screenHeight / 2 - 100 + 255}}
}

2.9 全局音效管理器实现

let instance/*** 统一的音效管理器*/
export default class Music {constructor() {if (instance) return instanceinstance = thisthis.bgmAudio = new Audio()this.bgmAudio.loop = truethis.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.playBgm()}playBgm() {this.bgmAudio.play()}playShoot() {this.shootAudio.currentTime = 0this.shootAudio.play()}playExplosion() {this.boomAudio.currentTime = 0this.boomAudio.play()}
}

2.10 管控游戏状态实现

import Pool from './base/pool'let instance/*** 全局状态管理器*/
export default class DataBus {constructor() {if (instance) return instanceinstance = thisthis.pool = new Pool()this.reset()}reset() {this.frame = 0this.score = 0this.bullets = []this.enemys = []this.animations = []this.gameOver = false}/*** 回收敌人,进入对象池* 此后不进入帧循环*/removeEnemey(enemy) {const temp = this.enemys.shift()temp.visible = falsethis.pool.recover('enemy', enemy)}/*** 回收子弹,进入对象池* 此后不进入帧循环*/removeBullets(bullet) {const temp = this.bullets.shift()temp.visible = falsethis.pool.recover('bullet', bullet)}
}

2.11 游戏入口主函数实现

import Player from './player/index'
import Enemy from './npc/enemy'
import BackGround from './runtime/background'
import GameInfo from './runtime/gameinfo'
import Music from './runtime/music'
import DataBus from './databus'const ctx = canvas.getContext('2d')
const databus = new DataBus()/*** 游戏主函数*/
export default class Main {constructor() {// 维护当前requestAnimationFrame的idthis.aniId = 0this.restart()}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.bindLoop = this.loop.bind(this)this.hasEventBind = false// 清除上一局的动画window.cancelAnimationFrame(this.aniId)this.aniId = window.requestAnimationFrame(this.bindLoop,canvas)}/*** 随着帧数变化的敌机生成逻辑* 帧数取模定义成生成的频率*/enemyGenerate() {if (databus.frame % 30 === 0) {const enemy = databus.pool.getItemByClass('enemy', Enemy)enemy.init(6)databus.enemys.push(enemy)}}// 全局碰撞检测collisionDetection() {const that = thisdatabus.bullets.forEach((bullet) => {for (let i = 0, il = databus.enemys.length; i < il; i++) {const enemy = databus.enemys[i]if (!enemy.isPlaying && enemy.isCollideWith(bullet)) {enemy.playAnimation()that.music.playExplosion()bullet.visible = falsedatabus.score += 1break}}})for (let i = 0, il = databus.enemys.length; i < il; i++) {const enemy = databus.enemys[i]if (this.player.isCollideWith(enemy)) {databus.gameOver = truebreak}}}// 游戏结束后的触摸事件处理逻辑touchEventHandler(e) {e.preventDefault()const x = e.touches[0].clientXconst y = e.touches[0].clientYconst area = this.gameinfo.btnAreaif (x >= area.startX&& x <= area.endX&& y >= area.startY&& y <= area.endY) this.restart()}/*** canvas重绘函数* 每一帧重新绘制所有的需要展示的元素*/render() {ctx.clearRect(0, 0, canvas.width, canvas.height)this.bg.render(ctx)databus.bullets.concat(databus.enemys).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)// 游戏结束停止帧循环if (databus.gameOver) {this.gameinfo.renderGameOver(ctx, databus.score)if (!this.hasEventBind) {this.hasEventBind = truethis.touchHandler = this.touchEventHandler.bind(this)canvas.addEventListener('touchstart', this.touchHandler)}}}// 游戏逻辑更新主函数update() {if (databus.gameOver) returnthis.bg.update()databus.bullets.concat(databus.enemys).forEach((item) => {item.update()})this.enemyGenerate()this.collisionDetection()if (databus.frame % 20 === 0) {this.player.shoot()this.music.playShoot()}}// 实现游戏帧循环loop() {databus.frame++this.update()this.render()this.aniId = window.requestAnimationFrame(this.bindLoop,canvas)}
}

3 结束语

上述主要介绍了小游戏关键的实现点,在该游戏的场景下,如果有兴趣的同学可以将粽子类在多添加几个,让粽子的种类多起来,这样可玩性很高!我主要是起一个抛砖引玉的作用,再次祝福大家端午节万事顺遂,多多吃粽子,吃好喝好!
下附小游戏源码下载地址:https://github.com/41809310102/mygames

用微信小游戏实现龙舟大战-打粽子相关推荐

  1. 微信小游戏 demo 飞机大战 代码分析(四)(enemy.js, bullet.js, index.js)

    微信小游戏 demo 飞机大战 代码分析(四)(enemy.js, bullet.js, index.js) 微信小游戏 demo 飞机大战 代码分析(一)(main.js) 微信小游戏 demo 飞 ...

  2. 微信小游戏制作坦克大战(四)添加敌方坦克,敌方坦克可以随机移动

    微信小游戏制作坦克大战(四)添加敌方坦克,敌方坦克可以随机移动 首先导入敌方坦克素材 重命名为敌方坦克1 敌方坦克也移动到屏幕外面,后面使用克隆体来显示. 我们给敌方坦克添加事件 好的,现在敌方坦克已 ...

  3. 微信小游戏制作坦克大战(六)碰撞检测,主角坦克碰到敌方坦克、炮弹爆炸

    微信小游戏制作坦克大战(六)碰撞检测,主角坦克碰到敌方坦克.炮弹爆炸 导入坦克爆炸效果的图片和声音素材 给主角坦克添加事件 给爆炸动画添加事件 当主角坦克碰到敌方坦克或者炮弹时显示爆炸效果 下一篇文章 ...

  4. 微信小游戏制作坦克大战(五)敌方坦克可以发射炮弹

    微信小游戏制作坦克大战(五)敌方坦克可以发射炮弹 在资源管理器中复制炮弹,重命名为敌人坦克的炮弹. 修改敌方坦克的积木 给敌方坦克炮弹添加事件 现在,敌方坦克已经可以自动发射炮弹啦. 下一篇文章:微信 ...

  5. 微信小游戏制作坦克大战(九)切换场景,游戏重新开始

    微信小游戏制作坦克大战(九)切换场景,游戏重新开始 新建一个游戏结束场景 主角坦克爆炸后切换到游戏结束场景 添加背景音乐 好了,至此坦克大战小游戏基本做好,小伙伴们可以继续完善哈. 体验地址:

  6. 微信小游戏制作坦克大战(八)统计得分

    微信小游戏制作坦克大战(八)统计得分 导入数字图片素材 新建得分变量 给数字添加事件 敌方坦克发生爆炸时,数字增加1 实现效果 下一篇文章:微信小游戏制作坦克大战(九)切换场景,游戏重新开始

  7. 微信小游戏制作坦克大战(七)碰撞检测,敌方坦克碰到主角坦克炮弹爆炸

    微信小游戏制作坦克大战(七)碰撞检测,敌方坦克碰到主角坦克炮弹爆炸 导入发生炮弹的音效素材 主角坦克发射炮弹或者敌方坦克发射炮弹时播放音效 修改敌方坦克积木 4.效果: 敌方坦克碰到主角坦克炮弹爆炸 ...

  8. 微信小游戏之飞机大战解析

    一.从抄官方代码开始 1.1 首先是game.js,具体代码如下: import './js/libs/weapp-adapter' import './js/libs/symbol'import M ...

  9. 微信小游戏实战——飞机大战demo笔记完整篇(函数实现)

    1. 目录结构: 2. game.js:入口文件 //game.js文件完整代码: import Main from "./src/mian.js" new Main() 3. g ...

最新文章

  1. java非阻塞 串口读数据_串口阻塞与非阻塞
  2. Codeforces Round #253 (Div. 1) A. Borya and Hanabi 暴力
  3. 日记-2017-7-24-cp-css-django/media
  4. AS安装APK失败的两种情况
  5. SAP Spartacus category navigation按钮之间的间隔问题
  6. 盘点大型分布式网站术语
  7. linux必备工具,Linux装机必备工具
  8. Linux进程管理:进程调度之完全公平调度算法
  9. libsvm——数据格式的转换及使用
  10. 在进行Forms身份验证时如何将此信息映射到GenericPrincipal 和 FormsIdentity 对象?
  11. 华为身处“创新者的窘境”,而浑然不觉(转载)
  12. 07 - 雷达发射机的主要质量指标
  13. morning 是字符串的内容变成good_小洁详解《R数据科学》--第十章 使用stringr处理字符串(上)...
  14. SQL Server 中“dbo”到底是什么
  15. GIMP( GNU IMAGE MANIPULATION PROGRAM)
  16. 库存数量控制中储备定额方法的改进与实现
  17. Android进阶之路——Flurry的使用
  18. Sysweld笔记:利用稳态算法加速算法模拟焊接过程的残余应力
  19. D3D中2D图片的绘制两种方法
  20. vue-2.5.16.js:597 [Vue warn]: Unknown custom element: ocean - did you register the component corre

热门文章

  1. 概率论与数理统计学习笔记——第14讲——大数定律(1.切比雪夫不等式及切比雪夫大数定律)
  2. Eclipse报错DataIntegrityViolationException异常解决办法
  3. Python3 计算空气质量指数AQI
  4. python如何计算概率事件_145、Python实现概率分布
  5. Πολιτική απορρήτου
  6. 如何改编一首吉他曲的和弦?
  7. 【办公必备软件】万彩办公大师教程丨PDF转HTML工具
  8. 全差分运算放大器浅析
  9. Linux系统账号安全和登录控制(安全很重要)
  10. Flying-Saucer使用HTML或者FTL(Freemarker模板)生成PDF