用微信小游戏实现龙舟大战-打粽子
端午节来啦!各位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
}
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
}
drawToCanvas(ctx) {
if (!this.visible) return
ctx.drawImage(
this.img,
this.x,
this.y,
this.width,
this.height
)
}
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()
}
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
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
}
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() {
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()
}
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
|