C#服务端的微信小游戏——多人在线角色扮演(十三)

C#服务端的微信小游戏——多人在线角色扮演(十三)

面向对象,春暖花开
——茂叔

文字渲染的优化

上一篇,我们实现了文字渲染。但是,细心的朋友应该发现,尽管我们可以渲染出文字,但整个过程非常的耗时。
首先,我们需要根据内码去获得文字的区位码,这一过程需要去解析一个非常长的字符串。想想都觉得累。
其次,我们获得了区位码后,需要根据一串字节去逐位画点,也就是说,我们渲染一个汉字实际上画了很多个点,系统消耗非常大。

据实际测试,在一台性能不错的笔记本上,渲染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下载:
>>第十三篇代码连接<<

上一篇:C#服务端的微信小游戏——多人在线角色扮演(十二)
下一篇:C#服务端的微信小游戏——多人在线角色扮演(十四)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值