面向对象,春暖花开
——茂叔
文字渲染的优化
上一篇,我们实现了文字渲染。但是,细心的朋友应该发现,尽管我们可以渲染出文字,但整个过程非常的耗时。
首先,我们需要根据内码去获得文字的区位码,这一过程需要去解析一个非常长的字符串。想想都觉得累。
其次,我们获得了区位码后,需要根据一串字节去逐位画点,也就是说,我们渲染一个汉字实际上画了很多个点,系统消耗非常大。
据实际测试,在一台性能不错的笔记本上,渲染5000个汉字,居然耗时300毫秒以上。作为游戏,这是不可接受的,一秒3帧,还玩个P啊!
因此,我们需要对文字的渲染算法进行优化。
既然实事解析内码去获得区位码非常耗时,那我们可以把内码与区位码之间的关系放进一个Map
对象,通过映射关系快速查找到内码所对应的区位码。
对于逐一描点这种累人的活,我们可以提前将所有汉字点都描出来,在运行时只需要根据区位码找到对应的图像,然后截取出来就可以了。
于是,我们对han.js
做了一些“小”改动:
var fontImg = wx.createImage()
exports.install = (callback) => {
fontImg.src = "images/han.png";
fontImg.onload = callback
}
GameGlobal.drawText = (ctx, str, sx, sy) => {
var cx = 0
for (var i = 0; i < str.length; i++) {
var charCode = str.charCodeAt(i)
var quwei = CodeMap.get(charCode)
ctx.drawImage(fontImg, quwei.wei * 12, quwei.qu * 12, 12, 12, sx + cx, sy, 12, 12)
cx += 14
}
}
var CodeMap = new Map([[12288, { "qu": 0, "wei": 0 }], [12289, { "qu": 0, "wei": 1 }], [12290, { "qu": 0, "wei": 2 }], [183, { "qu": 0, "wei": 3 }], [713, { "qu": 0, "wei": 4 }], [711, {……
//代码不齐,太长了,两次让我的浏览器停止响应,所以请到GitHub去下载完整代码吧,我也不想这样
其中,CodeMap
是根据那个很长很长的字符串解析出来的,具体解析过程略……重点就是每个内码对应一个区位对象。
而han.png
是按区位码排列的8000多个汉字点阵图,居然比二进制字库文件还小……
每行94个字符,一共94行,正好对应区位码。
所以,使用
ctx.drawImage(fontImg, quwei.wei * 12, quwei.qu * 12, 12, 12, sx + cx, sy, 12, 12)
这一句就很方便的把图中对应区位码的图像画到我们需要的地方了。
经过这两个修改,我们渲染5000个汉字的时间只需要不到50毫秒,嗯20帧,凑活玩了。
实际使用中,以我们的界面大小,不会有一帧超过200个汉字的需求,而如果渲染200个汉字,只需要5毫秒,一秒200帧,愉快了……
按钮?窗体?
Windows系列系统中,窗体、按钮、控件什么的,都是从一个基类派生出来的,有完善的消息机制和渲染策略。
我们的小程序中也可以采用这样的策略来组织我们的界面。
具体的思路是,一切需要渲染到设备上的界面元素,从主界面到一个标签、一个按钮、都从一个基本渲染对象派生而来,封装好的对象可以在写码时方便的实例化,从而使界面代码更加有序和高效。
首选,我们在根路径新建一个UIOBJECT,js
文件,在里面定义一个基本对象:
function baseUIObj(father, x = 0, y = 0, w = 0, h = 0){
this.father = father || undefined
this.x = x
this.y = y
this.width = w
this.height = h
}
这个对象作为我们所有界面元素的基类。
我们只需要使用:
var newUIObj=new baseUIObj()
console.log(newUIObj.width)//输出:0
var newUIObj2=new baseUIObj(newUIObj,0,0,100,30)
console.log(newUIObj2.width)//输出:100
就可以获得这样一个界面元素,创建时可以输入它的父元素,或者说容器,以及在父元素中位置和尺寸。
然后添加它自己的canvas
对象:
this.canvas = wx.createCanvas()
this.canvas.width = w
this.canvas.height = h
this.gCtx = this.canvas.getContext('2d')
在定义一个渲染的方法:
baseUIObj.prototype.redraw = function(caller) {
if (caller.isTouch)
caller.gCtx.fillStyle = caller.colorOnTouch
else
caller.gCtx.fillStyle = caller.color
caller.gCtx.fillRect(0, 0, caller.width, caller.height)
if (caller.background)
caller.gCtx.drawImage(caller.background, 0, 0, caller.width, caller.height)
if (caller.backgroundTouch&&caller.isTouch)
caller.gCtx.drawImage(caller.backgroundTouch, 0, 0, caller.width, caller.height)
if (caller.borderWidth > 0) {
caller.gCtx.strokeStyle = '#fff'
caller.gCtx.lineWidth = caller.borderWidth
caller.gCtx.strokeRect(caller.borderWidth / 2, caller.borderWidth / 2, caller.width - caller.borderWidth, caller.height - caller.borderWidth)
}
}
以后直接调用这个方法就可以在它的canvas
上渲染它所代表的界面了。
当然,还有很多其他的成员和方法,都是管理界面元素所需要的,详细自己读代码吧,最后的代码是这样的:
function baseUIObj(father, x = 0, y = 0, w = 0, h = 0) {
this.text = "baseUIObj"
this.name = "base"
this.me = this
this.children = []
this.father = father || undefined
if (this.father) {
this.father.children.push(this)
}
this.x = x
this.y = y
this.width = w
this.height = h
this.isHidden = false
this.borderWidth = 2
this.canvas = wx.createCanvas()
this.canvas.width = w
this.canvas.height = h
this.gCtx = this.canvas.getContext('2d')
this.isRedrawing = false
this.background
this.backgroundTouch
this.color = "#333"
this.colorOnTouch = "#333"
this.isTouch = false
this.onTouch = () => {
console.log(this.name + " is touched")
}
this.onClick = () => {
console.log(this.name + " is clicked")
}
this.isTouchable=false
}
baseUIObj.prototype.redraw = function(caller) {
if (caller.isTouch)
caller.gCtx.fillStyle = caller.colorOnTouch
else
caller.gCtx.fillStyle = caller.color
caller.gCtx.fillRect(0, 0, caller.width, caller.height)
if (caller.background)
caller.gCtx.drawImage(caller.background, 0, 0, caller.width, caller.height)
if (caller.backgroundTouch&&caller.isTouch)
caller.gCtx.drawImage(caller.backgroundTouch, 0, 0, caller.width, caller.height)
if (caller.borderWidth > 0) {
caller.gCtx.strokeStyle = '#fff'
caller.gCtx.lineWidth = caller.borderWidth
caller.gCtx.strokeRect(caller.borderWidth / 2, caller.borderWidth / 2, caller.width - caller.borderWidth, caller.height - caller.borderWidth)
}
}
baseUIObj.prototype.show = function(caller = this) {
caller.isHidden = false
if (caller.father)
caller.father.redraw()
else
GameWindow.redraw()
}
baseUIObj.prototype.hide = function(caller = this) {
caller.isHidden = true
if (caller.father)
caller.father.redraw()
else
GameWindow.redraw()
}
baseUIObj.prototype.render = function(caller = this) {
let devicePosition = caller.getDevicePosition()
ctxDevice.drawImage(caller.canvas, devicePosition.x, devicePosition.y, devicePosition.width, devicePosition.height)
}
baseUIObj.prototype.startTouch = function(caller = this) {
caller.isTouch = true
caller.onTouch()
caller.redraw()
}
baseUIObj.prototype.endTouch = function(caller = this) {
caller.isTouch = false
console.log(caller.name + " is untouched")
caller.redraw()
}
baseUIObj.prototype.getGameXY = function(caller = this) {
if (!caller.father || !caller.father.father)
return {
x: caller.x,
y: caller.y
}
else {
let fatherXY = caller.father.getGameXY()
return {
x: caller.x + fatherXY.x,
y: caller.y + fatherXY.y
}
}
}
baseUIObj.prototype.getDevicePosition = function(caller = this) {
var gameXY = caller.getGameXY()
var deviceX = gameXY.x * _g.DEVICEWIDTH / GameWindow.width
var deviceY = gameXY.y * UI_HEIGHT / GameWindow.height + UI_VPOS
var deviceWidth = caller.width * _g.DEVICEWIDTH / GameWindow.width
var deviceHeight = caller.height * UI_HEIGHT / GameWindow.height
return {
x: deviceX,
y: deviceY,
width: deviceWidth,
height: deviceHeight
}
}
然后我们从这个基类派生一个游戏主界面类以及一个按钮类出来:
exports.UIGame = UIGame
function UIGame(x = 0, y = 0, w = 188, h = 300) {
baseUIObj.call(this, undefined, x, y, w, h)
this.text = "UIGame"
this.name = "Game"
this.borderWidth = 0
}
UIGame.prototype = new baseUIObj()
UIGame.prototype.constructor = UIGame
UIGame.prototype.redraw = function(needrender = true, caller = this) {
if (caller.isRedrawing) return
caller.isRedrawing = true
if (!caller.isHidden) {
baseUIObj.prototype.redraw(caller)
caller.children.forEach(function(child, index, array) {
if (!child.isHidden) {
child.redraw(false)
caller.gCtx.drawImage(child.canvas, child.x, child.y, child.width, child.height)
}
})
}
if (needrender)
caller.render()
caller.isRedrawing = false
}
exports.UIButton = UIButton
function UIButton(father, x = 0, y = 0, w = 100, h = 30) {
baseUIObj.call(this, father, x, y, w, h)
this.text = "UIButton"
this.name = "Button"
this.colorOnTouch = "#666"
this.isTouchable = true
}
UIButton.prototype = new baseUIObj()
UIButton.prototype.constructor = UIButton
UIButton.prototype.redraw = function (needrender = true,caller = this) {
if (caller.isRedrawing) return
caller.isRedrawing = true
if (caller.isHidden) {
if (caller.father)
caller.father.redraw(caller.father)
} else {
baseUIObj.prototype.redraw(caller)
if (caller.text && caller.text.length > 0)
drawText(caller.gCtx, caller.text, caller.width / 2 - caller.text.length * 7, (caller.height - 12) / 2)
caller.children.forEach(function(child, index, array) {
if (!child.isHidden) {
child.redraw(false)
caller.gCtx.drawImage(child.canvas, child.x, child.y, child.width, child.height)
}
})
}
if (needrender)
GameWindow.redraw()
caller.isRedrawing = false
}
注意,游戏主界面类以和按钮类需要被其他文件调用,所以要exports
,这样就可以了,然后我们做一个简单的封面出来,我是用Photoship
做的,当然,专业的美工会做得更好。
然后修改我们的游戏启动文件,就是那个game.js
:
var globaldata = require('global.js')
var ziku = require('han.js')
var net = require('net.js')
globaldata.initialize()
wx.getSystemInfo({
success(res) {
_g.DEVICEWIDTH = res.windowWidth;
_g.DEVICEHEIGHT = res.windowHeight;
GameGlobal.UIObj = require('UIOBJECT.js')
wx.getSetting({
success(res) {
if (!res.authSetting['scope.userInfo']) {
let button = wx.createUserInfoButton({
type: 'text',
text: '请允许我们使用您的基本资料',
style: {
left: _g.DEVICEWIDTH / 2 - 150,
top: 200,
width: 300,
height: 40,
lineHeight: 40,
backgroundColor: '#ff0000',
color: '#ffffff',
textAlign: 'center',
fontSize: 16,
borderRadius: 4
}
})
button.onTap((res) => {
if (res.errMsg == "getUserInfo:ok") {
button.hide()
loadziku()
}
})
} else {
loadziku()
}
}
})
}
})
function loadziku() {
ziku.install(
function() {
let image = wx.createImage();
image.src = "images/cover.png"
image.onload = () => {
GameGlobal.GameWindow = new UIObj.UIGame()
GameWindow.background = image
var btn = new UIObj.UIButton(GameWindow)
btn.x = 44
btn.y = 180
btn.text = "开始游戏"
GameWindow.show()
}
})
}
调试一下,看到效果没?
我们只用了了几行代码就渲染出了一个中规中矩的界面。
……
GameGlobal.GameWindow = new UIObj.UIGame()
GameWindow.background = image
var btn = new UIObj.UIButton(GameWindow)
btn.x = 44
btn.y = 180
btn.text = "开始游戏"
GameWindow.show()
……
试一下添加一个新按钮呢,把以上代码改成:
……
GameWindow.background = image
var btn = new UIObj.UIButton(GameWindow)
btn.x = 44
btn.y = 180
btn.text = "开始游戏"
var btn2 = new UIObj.UIButton(GameWindow)
btn2.x = 44
btn2.y = 220
btn2.text = "退出游戏"
GameWindow.show()
……
So easy……妈妈再也不用担心我们添加按钮了。
当然,也可以用类似的方法设计更多的界面元素,例如标签(Label)、确认按钮(CheckBox)……有兴趣可以自己试试,我们这里随着后面的开发过程也会陆续添加更多的界面元素。
好吧,我不喜欢退出按钮,删掉。
但是,我们按钮点击以后没反应啊……现在,我们来完成用户触摸屏幕的响应部分。
微信提供了以下几个API,用以管理用户触摸:
参见官方文档:https://developers.weixin.qq.com/minigame/dev/api/base/app/touch-event/wx.onTouchStart.html
我们这里暂时只用到两个:
wx.onTouchStart
wx.onTouchEnd
首先,在根路径下新建一个文件gesture.js
。添加以下代码:
var rate_X = GameWindow.width / _g.DEVICEWIDTH
var rate_Y = GameWindow.height / (_g.DEVICEHEIGHT - 2 * UI_VPOS)
wx.onTouchStart(
onTouch
)
wx.onTouchEnd(
onTouchEnd
)
function findUIObj(x = 0, y = 0, obj = GameWindow) {
var ret = undefined
let GameXY = obj.getGameXY()
if (!obj.isHidden&&obj.isTouchable&&GameXY.x <= x && GameXY.y <= y && (GameXY.x + obj.width) > x && (GameXY.y + obj.height) > y) ret = obj
obj.children.forEach(function(child, index, array) {
let childRet = findUIObj(x, y, child)
if (childRet)
ret = childRet
})
return ret
}
var TouchingObj
function onTouch(res) {
let X = Math.round((res.changedTouches[0].clientX) * rate_X)
let Y = Math.round((res.changedTouches[0].clientY - UI_VPOS) * rate_Y)
TouchingObj = findUIObj(X, Y)
if (TouchingObj)
TouchingObj.startTouch()
}
function onTouchEnd(res) {
let X = Math.round((res.changedTouches[0].clientX) * rate_X)
let Y = Math.round((res.changedTouches[0].clientY - UI_VPOS) * rate_Y)
if (TouchingObj)
{
TouchingObj.endTouch()
if (findUIObj(X, Y)===TouchingObj)
{
TouchingObj.onClick()
}
TouchingObj=undefined
}
}
这段代码非常简单,主要就是对两个个动作进行处理,按下,寻找在该位置最后创建的界面元素对象,然后触发该对象的onTouch事件,离开,触发按下时触发的对象的离开事件endTouch
,如果按下和离开都在同一个对象上,则触发点击事件onClick
。
最后,在game.js
里面主界面渲染出来的地方加上引用gesture.js
:
GameWindow.background = image
var btn = new UIObj.UIButton(GameWindow)
btn.x = 44
btn.y = 180
btn.text = "开始游戏"
GameWindow.show()
require("gesture.js")
现在,我们的按钮对触摸有了响应:
把按钮的点击事件指向我们的net.initialize
:
var btn = new UIObj.UIButton(GameWindow)
btn.x = 44
btn.y = 180
btn.text = "开始游戏"
btn.onClick=net.initialize
再把net.js
修改成这样:
exports.initialize = () => {
GameWindow.children[0].hide()
wx.connectSocket({
url: _g.WEBSOCKET_URL,
method: "GET",
success(res) {
console.log("Socket successed:")
console.log(res)
wx.onSocketOpen(
(res) => {
console.log("Socket opened:")
console.log(res)
}
)
wx.onSocketClose(
(res) => {
console.log("Socket closed:")
console.log(res)
}
)
wx.onSocketError((res) => {
console.log("Socket error:")
console.log(res)
})
wx.onSocketMessage((res) => {
console.log(res)
parseMessage(JSON.parse(res.data))
})
},
fail(res) {
console.log("Socket failed:")
console.log(res)
}
})
}
function parseMessage(msg) {
if (msg.command == 1) //这是初次连接成功传回的结果
{
_g.COMMAND = JSON.parse(msg.data);
login()
} else {
switch (msg.command) //其他命令的处理结果
{
case _g.COMMAND.LOGIN:
_g.Player=JSON.parse(msg.data)
wx.getUserInfo({
success(res){
_g.Player.nickName = res.userInfo.nickName
_g.Player.avatarUrl = res.userInfo.avatarUrl
console.log(_g.Player)
GameWindow = new UIObj.UIGame()
let image = wx.createImage();
image.src = "images/UI.png"
image.onload = () => {
GameWindow.background = image
GameWindow.show()
}
}
})
break;
}
}
}
function login() {
wx.login({
success(res) {
console.log(res)
var data = {
command: _g.COMMAND.LOGIN,
data: res.code
}
wx.sendSocketMessage({
data: JSON.stringify(data)
})
}
})
}
把服务端启动起来试试,嗯……点击之后,按钮被隐藏,如果连接成功,则可以转到游戏主界面,失败……失败就卡住了……不行,还需要把各个界面都保存起来,方便随时切换。
修改game.js
:
……
GameGlobal.GameCover = new UIObj.UIGame()
GameCover.background = image
var btn = new UIObj.UIButton(GameCover)
btn.x = 44
btn.y = 180
btn.text = "开始游戏"
btn.onClick=net.initialize
GameGlobal.GameWindow=GameCover
GameWindow.show()
require("gesture.js")
……
然后在net.js
里面让失败的时候就回到封面,记得把按钮显示出来:
……
wx.onSocketClose(
(res) => {
console.log("Socket closed:")
GameWindow = GameCover
GameWindow.show()
GameWindow.children[0].show()
console.log(res)
}
)
wx.onSocketError((res) => {
console.log("Socket error:")
GameWindow = GameCover
GameWindow.show()
GameWindow.children[0].show()
console.log(res)
})
……
创建主界面时也保存下来,这样j就可以很方便的来回切换了:
……
wx.getUserInfo({
success(res) {
_g.Player.nickName = res.userInfo.nickName
_g.Player.avatarUrl = res.userInfo.avatarUrl
console.log(_g.Player)
if (GameGlobal.GameMainUI === undefined) {
GameGlobal.GameMainUI = new UIObj.UIGame()
let image = wx.createImage();
image.src = "images/UI.png"
image.onload = () => {
GameMainUI.background = image
var btn = new UIObj.UIButton(GameMainUI)
btn.x = 44
btn.y = 150
btn.text = "退出游戏"
btn.onClick = () => {
wx.closeSocket()
}
GameWindow = GameMainUI
GameWindow.show()
}
} else {
GameWindow = GameMainUI
GameWindow.show()
}
}
})
……
顺便添加一个退出的按钮,这样整个操作线路就畅通了,调试一下看看:
结果非常棒……这下用户交互的框架也完成了,下一步,我们就可以开始完成创建角色的功能了。呵呵,创建完角色就是进入游戏咯~~~~
截止这一篇的全部代码,请大家去我的GitHub下载:
>>第十三篇代码连接<<