0


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

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

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


前言

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


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

一、体验视频

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

龙舟大战

二、开发流程

1.素材收集

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

在这里插入图片描述

2.游戏逻辑实现

2.1 定义游戏开发基础类

  • 相关的代码说明请看注解
import Sprite from'./sprite'import DataBus from'../databus'const databus =newDataBus()const __ ={timer:Symbol('timer'),}/**
 * 简易的帧动画类实现
 */exportdefaultclassAnimationextendsSprite{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 =newImage()
      img.src = imgSrc

      this.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 = loop

    this.index = index

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

2.3 游戏基本元素精灵类

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

/**
 * 游戏基础的精灵类
 */exportdefaultclassSprite{constructor(imgSrc ='', width =0, height =0, x =0, y =0){this.img =newImage()this.img.src = imgSrc

    this.width = width
    this.height = height

    this.x = x
    this.y = y

    this.visible =true}/**
   * 将精灵图绘制在canvas上
   */drawToCanvas(ctx){if(!this.visible)return

    ctx.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)returnfalsereturn!!(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'constENEMY_IMG_SRC='images/enemy.png'constENEMY_WIDTH=60constENEMY_HEIGHT=60const __ ={speed:Symbol('speed')}const databus =newDataBus()functionrnd(start, end){return Math.floor(Math.random()*(end - start)+ start)}exportdefaultclassEnemyextendsAnimation{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(){const frames =[]constEXPLO_IMG_PREFIX='images/explosion'constEXPLO_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'constBULLET_IMG_SRC='images/bullet.png'constBULLET_WIDTH=16constBULLET_HEIGHT=30const __ ={speed:Symbol('speed')}const databus =newDataBus()exportdefaultclassBulletextendsSprite{constructor(){super(BULLET_IMG_SRC,BULLET_WIDTH,BULLET_HEIGHT)}init(x, y, speed){this.x = x
    this.y = y

    this[__.speed]= speed

    this.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

// 玩家相关常量设置constPLAYER_IMG_SRC='images/hero.png'constPLAYER_WIDTH=80constPLAYER_HEIGHT=80const databus =newDataBus()exportdefaultclassPlayerextendsSprite{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 =0elseif(disX > screenWidth -this.width) disX = screenWidth -this.width

    if(disY <=0) disY =0elseif(disY > screenHeight -this.height) disY = screenHeight -this.height

    this.x = disX
    this.y = disY
  }/**
   * 玩家响应手指的触摸事件
   * 改变龙舟的位置
   */initEvent(){
    canvas.addEventListener('touchstart',((e)=>{
      e.preventDefault()const x = e.touches[0].clientX
      const 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].clientX
      const y = e.touches[0].clientY

      if(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.innerHeight

constBG_IMG_SRC='images/bg.jpg'constBG_WIDTH=512constBG_HEIGHT=512/**
 * 游戏背景类
 * 提供update和render函数实现无限滚动的背景功能
 */exportdefaultclassBackGroundextendsSprite{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.innerHeight

const atlas =newImage()
atlas.src ='images/Common.png'exportdefaultclassGameInfo{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

/**
 * 统一的音效管理器
 */exportdefaultclassMusic{constructor(){if(instance)return instance

    instance =thisthis.bgmAudio =newAudio()this.bgmAudio.loop =truethis.bgmAudio.src ='audio/bgm.mp3'this.shootAudio =newAudio()this.shootAudio.src ='audio/bullet.mp3'this.boomAudio =newAudio()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

/**
 * 全局状态管理器
 */exportdefaultclassDataBus{constructor(){if(instance)return instance

    instance =thisthis.pool =newPool()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 =newDataBus()/**
 * 游戏主函数
 */exportdefaultclassMain{constructor(){// 维护当前requestAnimationFrame的idthis.aniId =0this.restart()}restart(){
    databus.reset()

    canvas.removeEventListener('touchstart',this.touchHandler
    )this.bg =newBackGround(ctx)this.player =newPlayer(ctx)this.gameinfo =newGameInfo()this.music =newMusic()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 =this

    databus.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 =false
          databus.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].clientX
    const y = e.touches[0].clientY

    const area =this.gameinfo.btnArea

    if(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


本文转载自: https://blog.csdn.net/ILOVEMYDEAR/article/details/125109446
版权归原作者 沉默着忍受 所有, 如有侵权,请联系我们删除。

“用微信小游戏实现龙舟大战-打粽子”的评论:

还没有评论