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官網