H5游戏一直以来,以跨平台,低体验著称,其很大缘由在于早期技术方案的不成熟和受限于H5游戏编码水平。但现今,Canvas和WebGL的渲染性能已经很好了,合理编码的状况下,体验与原生应用游戏并没有区别html
由微信小程序衍生且独立而出的 【微信小游戏】即是瞄准了Web游戏渲染,表明着这是将来游戏制做一个很大方向上的趋势。微信小游戏运行环境移除了BOM和DOM,这是一个颇有意思的方案,由于这意味着游戏开发者必须用纯canvas绘制游戏内容,这对于游戏性能的提高是巨大的git
同时,为了保留对游戏引擎的支持和减小现行大量H5游戏的迁移工做,微信小游戏官方提供了weapp-adapter适配器,经过微信小游戏官方的适配器或自行开发编写的适配器,能够兼容不少的BOM或DOM的APIgithub
由于微信小游戏平台才刚刚推出,目前网络上大量存在的,包括github上开源的微信小游戏其实都是微信小程序的网页版本,和传统页游没区别,受限于BOM和DOM,性能和体验上都并很差。本文的主旨在于从零开始,以纯Canvas的开发方式,制做一个微信小游戏上很是流行和好玩的游戏——【弹一弾】web
H5模式演示版本:cheneyweb.github.io/wxgame-elas…算法
H5模式二维码,手机扫码体验(微信扫码,浏览器扫码等均可以)编程
![]()
微信小游戏模式演示版本:须要打开微信开发者工具导入工程目录json
【弹一弾】游戏的核心在于对物理弹动的真实模拟和大量物体元素的碰撞交互,是一个很是有挑战的游戏制做canvas
任何的游戏开发开发离不开游戏引擎,由于纯原生的编码制做游戏效率是很是低下的,并且难以维护,因此工欲善其,必先利其器,在开发【弹一弾】的同时,咱们还须要先制做一个精简高效的canvas游戏引擎(称之为游戏引擎是不合适的,由于咱们不可能在短期内完成一个游戏引擎的开发,这里只是为了类比了游戏引擎的少部分功能)小程序
任何的游戏其本质必定是包含着一个或多个循环,这才会有了咱们所见的动画效果,下面先列举【弹一弾】的开发思路微信小程序
- 统一的资源定义(包括图片,音效,音乐)等资源
- 统一的资源加载(初始资源在内存中的载入)
- 统一的状态管理(全局变量数据的维护,这里说个题外话,我本人很是不喜欢状态管理之类的的全局变量方案,可是在游戏开发中,这是必须且不得不引入的,由于游戏编程对于状态变动的需求很是大,合理的使用全局变量能大大提升编码效率)
- 统一的资源渲染,绘制呈现
- 全局物理引擎,负责模拟弹性碰撞实现,实现游戏核心逻辑
- 面向对象的开发思路,以物体元素做为游戏内容单位,制定每一个物体元素的行为和逻辑
以上的1-4点就是咱们须要制做的简单高效的精简版“游戏引擎”,有了1-4的基础铺垫后,经过5的引入和6的自定义展开,咱们就能够完成【弹一弾】的制做
这里须要补充说明的是第5点,物理引擎,为了开发【弹一弾】我寻找对比了多款JS物理引擎。**目前的现状是大部分JS物理引擎都已经处于中止开发维护的状态,多款知名的JS物理引擎在github上已经多年没更新。**或许是由于物理引擎的门槛较高和H5游戏早年的发展不顺利致使。但对游戏来讲,物理引擎是很是核心且重要的一环,不少PC和Mobile上的游戏大做,之因此体验良好,就是由于有强大的物理引擎做为背后支撑,可是这些大做的物理引擎不少都是商业版本,价格高昂且不开源
不过所幸的是,有一款JS物理引擎很突出,性能和功能很强大,且目前有着持续性的维护,它就是Matter.js。这款物理引擎几乎是我制做弹一弾的惟一选择,我我的测试下来问题并很少,有部分问题能够经过了对源码的一些修改解决。须要特别说明的是Matter物理引擎也是知名游戏引擎Laya和Egret的开发常选
整个开发流程会分七步走,须要注意的是,由于文章篇幅所限,不可能展现全部代码,但全部核心流程都会有介绍说明,在文末我会附上项目的github地址,提供你们参考
相比传统游戏开发,H5游戏的开发环境十分简单轻巧,并且咱们不采用商业游戏引擎,而是纯原生开发,全部咱们只须要一个关键工具:
微信开发者工具
![]()
一个超级无敌精简版的游戏引擎须要什么功能,那就是把游戏画面渲染绘制出来。 因此理论上咱们只须要一个“画笔类”就够了,这支画笔可以绘制出咱们想要的内容。固然,除了画笔以外,咱们也还须要一些其余的关键组件 咱们命名一个文件夹——"base",而后在这个文件夹内放置咱们全部须要的游戏基础类
├── base 精简版游戏引擎
│ ├── Body.js 物理物体元素基类
│ ├── DataStore.js 全局状态管理类
│ ├── Resource.js 统一资源定义类
│ ├── ResourceLoader.js 统一资源加载类
│ ├── Sprite.js 普通物体渲染画笔类
│ └── matter.js 物理引擎
复制代码
Resource.js 这是统一资源管理类,很是简单,由于整个游戏只须要两张图片和两个音效
export const Resources = [
['background', 'res/background.png'],
['startButton', 'res/startbutton.png'],
['bgm', 'res/xuemaojiao.mp3'],
['launch', 'res/launch.mp3']
]
复制代码
ResourceLoader.js 这是统一资源加载类,一样简单,咱们只须要在资源加载后回调便可,由于微信小游戏的图片和音效资源的加载须要其官方API,这里和H5原生标准稍有不一样
//资源文件加载器,确保在图片资源加载完成后才渲染
import { Resources } from './Resource.js'
export class ResourceLoader {
constructor() {
this.imageCount = 0
this.audioCount = 0
//导入资源
this.map = new Map(Resources)
for (let [key, src] of this.map) {
let res = null
if (src.split('.')[1] == 'png' || src.split('.')[1] == 'jpg') {
this.imageCount++
// H5建立image的API
res = new Image()
// 微信建立image的API
// res = wx.createImage()
res.src = src
} else {
this.audioCount++
// H5建立audio的API
res = new Audio()
// 微信建立audio的API
// res = wx.createInnerAudioContext()
res.src = src
}
this.map.set(key, res)
}
}
// 加载完成回调
onload(cb) {
let loadCount = 0
for (let res of this.map.values()) {
// 使this指向当前的ResourceLoader
res.onload = () => {
loadCount++
if (loadCount >= this.imageCount) {
cb(this.map)
}
}
}
}
}
复制代码
Sprite.js 这是普通物体渲染画笔类,目前咱们只须要封装底层的canvas的图片绘制便可
import { DataStore } from './DataStore.js'
export class Sprite {
constructor(ctx, img, x = 0, y = 0, w = 0, h = 0, srcX = 0, srcY = 0, srcW = 0, srcH = 0, ) {
this.ctx = ctx
this.img = img
this.srcX = srcX
this.srcY = srcY
this.srcW = srcW
this.srcH = srcH
this.x = x
this.y = y
this.w = w
this.h = h
}
/** * 绘制图片 * img 传入Image对象 * srcX 要剪裁的起始X坐标 * srcY 要剪裁的起始Y坐标 * srcW 剪裁的宽度 * srcH 剪裁的高度 * x 放置的x坐标 * y 放置的y坐标 * w 要使用的宽度 * h 要使用的高度 */
draw(img = this.img,
x = this.x, y = this.y, w = this.w, h = this.h,
srcX = this.srcX, srcY = this.srcY, srcW = this.srcW, srcH = this.srcH) {
this.ctx.drawImage(img, srcX, srcY, srcW, srcH, x, y, w, h)
}
static getImage(key) {
return DataStore.getInstance().res.get(key)
}
}
复制代码
Body.js 这是物理物体元素基类,目前只须要实现引入物理引擎实例便可
// 物体基类
export class Body {
constructor(physics) {
this.physics = physics
}
}
复制代码
App.js 这是游戏的入口,也是整个游戏应用类,只须要canvas实例,以及拓展物理引擎实例做为入参,便可实例化该游戏应用
import { ResourceLoader } from './src/base/ResourceLoader.js'
import { DataStore } from './src/base/DataStore.js'
import { Director } from './src/Director.js'
/** * 游戏入口 */
export class App {
constructor(canvas, options) {
this.canvas = canvas // 画布
this.physics = { ...options, ctx: this.canvas.getContext('2d') } // 物理引擎
this.director = new Director(this.physics) // 导演
this.dataStore = DataStore.getInstance()
// 资源加载
new ResourceLoader().onload(res => {
// 持久化资源
this.dataStore.res = res
// 加载精灵
this.director.spriteLoad(res)
// 运行游戏
this.run()
})
}
/** * 运行游戏 */
run() {
// 注册事件
this.registerEvent()
// 物理渲染
this.director.physicsDirect()
// 精灵渲染
this.director.spriteDirect()
// 音乐播放
this.dataStore.res.get('bgm').autoplay = true
}
/** * 从新加载游戏 */
reload() {
// 物理渲染
this.director.physicsDirect(true)
// 精灵渲染
this.director.spriteDirect(true)
}
/** * 注册事件 */
registerEvent() {
// 移动设备触摸事件,使用=>使this指向Main类
this.canvas.addEventListener('touchstart', e => {
// 屏蔽事件冒泡
e.preventDefault()
// 若是游戏是结束状态,则从新开始
if (this.dataStore.isGameOver) {
// 从新初始化
this.dataStore.isGameOver = false
this.reload()
}
})
// PC设备点击事件
this.canvas.addEventListener('mousedown', e => {
// 屏蔽事件冒泡
e.preventDefault()
// 若是游戏是结束状态,则从新开始
if (this.dataStore.isGameOver) {
// 从新初始化
this.dataStore.isGameOver = false
this.reload()
}
})
}
}
复制代码
Director.js 这是游戏导演类,负责游戏主逻辑调度调配,以及游戏画面渲染工做
// 精灵对象
import { BackGround } from './sprite/BackGround.js'
import { StartButton } from './sprite/StartButton.js'
import { Score } from './sprite/Score.js'
// 物理引擎绘制对象
import { Block } from './body/Block.js'
import { Border } from './body/Border.js'
import { Bridge } from './body/Bridge.js'
import { Aim } from './body/Aim.js'
// 数据管理
import { DataStore } from './base/DataStore.js'
/** * 导演类,控制游戏的逻辑 */
export class Director {
constructor(physics) {
this.physics = physics
this.dataStore = DataStore.getInstance()
}
// 加载精灵对象
spriteLoad() {
this.sprite = new Map()
this.sprite['score'] = new Score(this.physics)
this.sprite['startButton'] = new StartButton(this.physics)
this.sprite['background'] = new BackGround(this.physics)
}
// 逐帧绘制
spriteDirect(isReload) {
if(isReload){
this.dataStore.scoreCount = 0
}
// 绘制前先判断是否碰撞
// this.check()
// 游戏未结束
if (!this.dataStore.isGameOver) {
// 绘制游戏内容
this.sprite['score'].draw()
// this.sprite['background'].draw()
// 自适应浏览器的帧率,提升性能
this.animationHandle = requestAnimationFrame(() => this.spriteDirect())
}
// 游戏结束
else {
// 中止物理引擎
this.physics.Matter.Engine.clear(this.physics.engine)
this.physics.Matter.World.clear(this.physics.engine.world)
this.physics.Matter.Render.stop(this.physics.render)
// 中止绘制
cancelAnimationFrame(this.animationHandle)
// 结束界面
this.sprite['score'].draw()
this.sprite['startButton'].draw()
}
}
// 物理绘制
physicsDirect(isReload) {
this.physics.Matter.Render.run(this.physics.render)
if (!isReload) {
new Aim(this.physics).draw().event()
// new Bridge(this.physics).draw()
}
new Block(this.physics).draw().event().upMove()
new Border(this.physics).draw()
}
}
复制代码
BackGround.js 今后处开始,就已经使用搭建好的游戏框架,开始正式设计和绘制游戏内容,在这里以最简单的背景类举例,这个基础物体很是简单,且只作了一件事情,那就是绘制游戏背景。剩余的基础物体还有计分器和游戏开始按钮,限于篇幅不作展开,文末会有本项目的github开源项目地址
import { Sprite } from '../base/Sprite.js'
/** * 背景类 */
export class BackGround extends Sprite {
constructor(physics) {
const image = Sprite.getImage('background')
super(
physics.ctx, image,
(physics.canvas.width - image.width) / 2,
(physics.canvas.height - image.height) / 2.5,
image.width, image.height,
0,
0,
image.width, image.height
)
}
}
复制代码
为了让matter.js这个物理引擎可以适合游戏的开发需求,咱们须要对其进行适当的修改,让其增长可以渲染文字等功能,因此咱们选择了matter.js的未压缩版本 在matter.js的Render.bodies方法中,跟着c.globalAlpha = 1;以后,增长拓展代码
c.globalAlpha = 1;
// 增长自定义渲染TEXT
if (part.render.text) {
// 30px is default font size
var fontsize = 30;
// arial is default font family
var fontfamily = part.render.text.family || "Arial";
// white text color by default
var color = part.render.text.color || "#FFFFFF";
// text maxWidth
var maxWidth = part.render.text.maxWidth
if (part.render.text.size)
fontsize = part.render.text.size;
else if (part.circleRadius)
fontsize = part.circleRadius / 2;
var content = "";
if (typeof part.render.text == "string")
content = part.render.text;
else if (part.render.text.content)
content = part.render.text.content;
c.textBaseline = "middle";
c.textAlign = "center";
c.fillStyle = color;
c.font = fontsize + 'px ' + fontfamily;
if (part.bounds) {
maxWidth = part.bounds.max.x - part.bounds.min.x;
}
c.fillText(content, part.position.x, part.position.y, maxWidth);
}
复制代码
game.js 对Matter物理引擎作一些调整以后,咱们就能够在微信小游戏的入口文件中引入,并初始化【弹一弾】游戏实例
// require('./src/base/weapp-adapter.js')
const Matter = require('./src/base/matter.js')
import { App } from './App.js'
// 同时兼容H5模式和微信小游戏模式
const canvas = typeof wx == 'undefined' ? document.getElementById('app') : wx.createCanvas()
// H5网页游戏模式
if (typeof wx == 'undefined') {
canvas.width = 375
canvas.height = 667
}
// 微信小游戏模式
else {
window.Image = () => wx.createImage()
window.Audio = () => wx.createInnerAudioContext()
}
// 初始化物理引擎
const engine = Matter.Engine.create({
enableSleeping: true
})
const render = Matter.Render.create({
canvas: canvas,
engine: engine,
options: {
width: canvas.width,
height: canvas.height,
background: './res/background.png', // transparent
wireframes: false,
showAngleIndicator: false
}
})
Matter.Engine.run(engine)
// Matter.Render.run(render)
// 初始化游戏
const physics = { Matter, engine, canvas, render }
new App(canvas, physics)
复制代码
Border.js 当基础物体渲染工做和物理引擎引入工做完成后,就能够开始利用物理引擎绘制咱们须要的物理物体元素,在【弹一弾】游戏中,总共有三种物理物体,分别是墙体,弹球,方块
在这以最简单的墙体为例,其他比较复杂的弹球和方块,代码比较长,在此限于篇幅不展开,文末会有本项目开源的github地址,能够前往进一步了解
// 边界
import { Body } from '../base/Body.js'
export class Border extends Body {
constructor(physics) {
super(physics)
}
draw() {
const physics = this.physics
let bottomHeight = 10
let leftWidth = 10
const borderBottom = physics.Matter.Bodies.rectangle(
physics.canvas.width / 2, physics.canvas.height - bottomHeight / 2,
physics.canvas.width - leftWidth * 2, bottomHeight, {
isStatic: true,
render: {
visible: true
}
})
const borderLeft = physics.Matter.Bodies.rectangle(
leftWidth / 2, physics.canvas.height / 2,
leftWidth, physics.canvas.height, {
isStatic: true,
render: {
visible: true
}
})
const borderRight = physics.Matter.Bodies.rectangle(
physics.canvas.width - leftWidth / 2, physics.canvas.height / 2,
leftWidth, physics.canvas.height, {
isStatic: true,
render: {
visible: true
}
})
physics.Matter.World.add(physics.engine.world, [borderBottom, borderLeft, borderRight])
}
}
复制代码
到此为止,整个【弹一弾】微信小游戏的制做就完成了,其实回首梳理整个流程,还算是流畅,也不复杂,可是不少时候万事开头难,在一开始个人确遇到了不少不少的问题,包括物理引擎的引入,游戏逻辑的合理安排,算是一些挑战,所幸这些问题不少都解决了,也就有了此文
固然还有一些问题我至今尚未完美解决,例如当球速过快引发的“穿墙”问题,这实际上是Matter.js物理引擎的问题,在github上有关这一问题的讨论,做者还创建了CCD算法分支尝试解决,可是遗憾的是,截止文本完成时间,这一问题仍然没有在Matter.js上获得解决,若是读者们有解决思路的,也能够联系我,不胜感激
另外,【弹一弾】整个游戏我目前为止完成了核心交互逻辑,可是比起微信上的弹一弾游戏,不少细节都尚未作,例如美术风格的完善和弹球的回收,以及多样的方块和道具,这些之后若是有时间,我会进一步完善
我我的很是追求极简和拓展,如下是【弹一弾】的工程目录结构
├── App.js 弹一弾游戏入口
├── game.js 微信小游戏入口
├── game.json
├── project.config.json
├── res 资源集合
│ ├── background.png
│ ├── launch.mp3
│ ├── startbutton.png
│ └── xuemaojiao.mp3
└── src
├── Director.js 导演
├── base 精简版游戏引擎
│ ├── Body.js 物理物体元素基类
│ ├── DataStore.js 全局状态管理类
│ ├── Resource.js 统一资源定义类
│ ├── ResourceLoader.js 统一资源加载类
│ ├── Sprite.js 普通物体渲染画笔类
│ └── matter.js 物理引擎
├── body 物理物体元素
│ ├── Aim.js 准星瞄准
│ ├── Block.js 障碍方块
│ ├── Border.js 边界墙体
└── sprite 普通物体元素
├── BackGround.js 游戏背景
├── Score.js 游戏分数
└── StartButton.js 开始按钮
复制代码
【弹一弾】的开发我选用了纯canvas的方案,一方面适合微信小游戏平台,一方面也能兼容H5网页,同时性能良好,整个游戏的大小不超过1MB,可说是很是迷你,可是麻雀虽小五脏俱全 另外,由于没有采用游戏引擎,而是搭建制做了一个精简迷你的游戏开发框架,因此我也没有采用微信官方的适配器weapp-adapter,一来能够节省53KB,二来能够提高代码执行效率,让快更快 固然,全文中我所描述制做的精简版游戏引擎,其实比起目前主流的商业游戏引擎,只是冰山一角,目的只是为了让更多初入门的玩家能对游戏引擎有个初步的概念。真正的商业游戏引擎例如Laya和Egret,功能十分强大,我后续也会出一篇文章,采用这类商业游戏引擎将【弹一弾】重作一遍 从现今微信小游戏的发展上咱们能够展望,将来H5之类的纯Web游戏极可能会占据游戏市场很大份额,使得游戏开发也完全走向真正的跨平台,热更新,高性能
感谢你的阅读,但愿本文可以给你带来帮助:)
做者:CheneyXu
Github:wxgame-elastic
关于:XServer官网