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

这篇具有很好参考价值的文章主要介绍了用微信小游戏实现龙舟大战-打粽子。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

端午节来啦!各位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 = 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 = false

    this.isPlaying = true
    this.loop = loop

    this.index = index

    if (this.interval > 0 && this.count) {
      this[__.timer] = setInterval(
        this.frameLoop.bind(this),
        this.interval
      )
    }
  }

  // 停止帧动画播放
  stop() {
    this.isPlaying = false

    if (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 = 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 / 2
    const spY = sp.y + sp.height / 2

    if (!this.visible || !sp.visible) return false

    return !!(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 = 60

const __ = {
  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.height

    this[__.speed] = speed

    this.visible = true
  }

  // 预定义爆炸的帧动画
  initExplosionAnimation() {
    const 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)
  }
}

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 = 30

const __ = {
  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 = 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

// 玩家相关常量设置
const PLAYER_IMG_SRC = 'images/hero.png'
const PLAYER_WIDTH = 80
const PLAYER_HEIGHT = 80

const databus = new DataBus()

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

      const x = e.touches[0].clientX
      const y = e.touches[0].clientY

      //
      if (this.checkIsFingerOnAir(x, y)) {
        this.touched = true

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

const 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 = 0

    this.render(ctx)
  }

  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
    )
  }
}

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

const screenWidth = window.innerWidth
const screenHeight = window.innerHeight

const 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 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.playBgm()
  }

  playBgm() {
    this.bgmAudio.play()
  }

  playShoot() {
    this.shootAudio.currentTime = 0
    this.shootAudio.play()
  }

  playExplosion() {
    this.boomAudio.currentTime = 0
    this.boomAudio.play()
  }
}

2.10 管控游戏状态实现

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) {
    const temp = this.enemys.shift()

    temp.visible = false

    this.pool.recover('enemy', enemy)
  }

  /**
   * 回收子弹,进入对象池
   * 此后不进入帧循环
   */
  removeBullets(bullet) {
    const temp = this.bullets.shift()

    temp.visible = false

    this.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的id
    this.aniId = 0

    this.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 = 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 += 1

          break
        }
      }
    })

    for (let i = 0, il = databus.enemys.length; i < il; i++) {
      const enemy = databus.enemys[i]

      if (this.player.isCollideWith(enemy)) {
        databus.gameOver = true

        break
      }
    }
  }

  // 游戏结束后的触摸事件处理逻辑
  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 = true
        this.touchHandler = this.touchEventHandler.bind(this)
        canvas.addEventListener('touchstart', this.touchHandler)
      }
    }
  }

  // 游戏逻辑更新主函数
  update() {
    if (databus.gameOver) return

    this.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://www.toymoban.com/news/detail-489204.html

到了这里,关于用微信小游戏实现龙舟大战-打粽子的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • python项目分享 - 坦克大战小游戏设计与实现 (源码)

    🔥 Hi,各位同学好呀,这里是L学长! 🥇今天向大家分享一个今年(2022)最新完成的毕业设计项目作品 python小游戏毕设 坦克大战小游戏设计与实现 (源码) 🥇 学长根据实现的难度和等级对项目进行评分(最低0分,满分5分) 难度系数:3分 工作量:3分 创新点:4分 项目获取:

    2024年02月01日
    浏览(50)
  • python项目分享 坦克大战小游戏设计与实现 (源码)

    🔥 Hi,各位同学好呀,这里是L学长! 🥇今天向大家分享一个今年(2022)最新完成的毕业设计项目作品 python小游戏毕设 坦克大战小游戏设计与实现 (源码) 🥇 学长根据实现的难度和等级对项目进行评分(最低0分,满分5分) 难度系数:3分 工作量:3分 创新点:4分 项目获取:

    2024年01月25日
    浏览(52)
  • python毕设分享 坦克大战小游戏设计与实现 (源码)

    🔥 Hi,各位同学好呀,这里是L学长! 🥇今天向大家分享一个今年(2022)最新完成的毕业设计项目作品 python小游戏毕设 坦克大战小游戏设计与实现 (源码) 🥇 学长根据实现的难度和等级对项目进行评分(最低0分,满分5分) 难度系数:3分 工作量:3分 创新点:4分 项目获取:

    2024年02月03日
    浏览(51)
  • 【Python游戏】超简单~Python实现植物大战僵尸小游戏,可以用于做毕业设计哟 | 附源码

    hello,大家好呀~ 今天给打击整一个植物大战僵尸 无广告版本 哈哈 现在的小游戏很多都是有广告,多少有点难受 今天给大家直接安排 遇到不懂的问题也可以私信小编或者↓ ↓ ↓ 源码. 点击蓝色字体领取哟~(备注:苏) 有很多的资源可以白嫖的哈,不定时会更新一下Pytho

    2024年02月05日
    浏览(49)
  • 端午节安康,佬们都了解端午节的哪些知识呢(附粽子大作战小游戏)

    前言: 端午节假期, 首先祝各位小伙伴儿们端午节安康 。参考了一些资料,本篇文章将和大家分享关于端午节的由来,习俗,以及关于端午节的一个代码小游戏–粽子大作战。 希望大家看完此篇文章能对端午节有收获,也希望小伙伴儿们在这个特殊的节日里包括以后开开心

    2024年02月10日
    浏览(44)
  • python——飞机大战小游戏

    目录 1、导入模块 2、窗口操作 3、事件操作 4、长按事件 5、添加游戏背景 6、添加英雄飞机 7、获取飞机的图片矩形 8、基本游戏窗口 9、添加游戏窗口图片 10、英雄飞机登场 11、英雄飞机装备子弹并发射 1、enemy_plane 2、game_main 3、game_map 4、game_score 5、hero_plane 6、plane_bullet 先安

    2024年02月03日
    浏览(62)
  • 前端小游戏——植物大战僵尸

    给大家分享一个植物大战僵尸网页游戏源代码,感兴趣的小伙伴可收藏学习 (完整源码在文末) 先来一睹“芳容” 《植物大战僵尸》 是一款极富策略性的小游戏。可怕的僵尸即将入侵,每种僵尸都有不同的特点,例如铁桶僵尸拥有极强的抗击打能力,矿工僵尸可以挖地道绕

    2024年02月03日
    浏览(44)
  • Python飞机大战小游戏

    游戏规则:键盘上下左右键控制飞机移动 游戏展示图片: 源码: 第一个py命名为:plane_main.py 第二py命名为:plane_sprites.py 素材图片image关注私信我获取!!!

    2024年02月10日
    浏览(53)
  • python人机大战小游戏代码

    2024年02月14日
    浏览(46)
  • python小游戏开发(飞机大战)

    目录 一:背景       1. pygame 模块初识 二.项目设计内容 开发一款飞机大战游戏,要求: (1)实现飞机的移动、子弹发射,对本次游戏有一个初步的编写及认识。 (2)飞机的持续按键移动和飞机自爆优化。 (3)进行基类的抽取,使代码更具有层次性和简化重复代码。 (

    2024年02月16日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包