頭腦王者答題對戰源碼分析教程
接到業務需求:用微信小程序開發一個答題對戰類的游戲,借此機會呢把小程序好好研究一下(小程序出來很長時間了現在才看)。前段時間超級火爆的“頭腦王者”就是我最好的研究對象。在研究階段呢,沒有配置前端、設計。所以樣式基本是仿照的。廢話不多說,進入正題。
我們實現最核心的玩法就行:單人答題、雙人pk答題、排行榜(好友/世界)。
知識儲備:涉及到實時通訊,在nodejs和workman之間,選擇了workman。需要過一遍微信小程序官方文檔。如果有vue基礎那么上手就更快了。最重要的一點:ES6語法要熟悉。
一、用戶信息
在小程序內,因為多個頁面都需要用戶信息,所以用戶信息的獲取放在app.js里,做一個本地存儲。小程序在啟動后是有兩個不同步的線程:view thread 和 appservice thread。為了解決線程不同步造成用戶信息獲取出現的問題,用promise+callback進行了改造。獲取用戶openid的過程我們放在了第一步。獲取用戶信息:要判斷是否有緩存、是否有授權等情況。

1 const promise = new Promise(function (resolve, reject) { 2 var openid = wx.getStorageSync('openid'); 3 if (openid == "" || openid == null) { 4 wx.login({ 5 success: res => { 6 wx.request({ 7 url: 'https://fotonpickup.risingad.com/wxapi2.php', 8 data: { 'code': res.code }, 9 success(res) { 10 wx.setStorage({ 11 key: "openid", 12 data: res.data.openid 13 }); 14 resolve(res.data.openid); 15 }, 16 fail(res) { 17 reject(res); 18 } 19 }); 20 } 21 }) 22 } else { 23 resolve(openid); 24 } 25 }); 26 promise.then((openid) => { 27 return new Promise((resolve, reject) => { 28 //先從本地取用戶信息 29 var wxinfo = wx.getStorageSync('wxinfo'); 30 if (wxinfo) { 31 resolve(wxinfo); 32 } else { 33 wx.getSetting({ 34 success: res => { 35 if (res.authSetting['scope.userInfo']) { 36 // 已經授權,可以直接調用 getUserInfo 獲取頭像昵稱,不會彈框 37 wx.getUserInfo({ 38 success: res => { 39 resolve(res.userInfo); 40 } 41 }) 42 } else { 43 //沒有授權的情況,不知道正式版本是什么樣子的。 44 wx.getUserInfo({ 45 success: res => { 46 resolve(res.userInfo); 47 },fail:res=>{ 48 //resolve(false); 49 //沒有授權的預覽版本,用open-type獲取,並跳轉統一頁面,其他頁面默認為有用戶授權的狀態 50 wx.navigateTo({ 51 url: '/pages/scope/scope' 52 }); 53 } 54 }) 55 } 56 } 57 }) 58 } 59 }).then((wxinfo) => { 60 if (wxinfo) { 61 //將用戶信息上傳服務器保存一份 62 this.uploadUserInfo(wxinfo); 63 //本地存儲用戶cookie 64 wx.setStorage({ 65 key: "wxinfo", 66 data: wxinfo 67 }); 68 } 69 this.globalData.userInfo=wxinfo; 70 if(this.userInfoReadyCallback){ 71 this.userInfoReadyCallback(wxinfo); 72 } 73 }) 74 }) 75 },
注意看,第74行。此處注入了一個回調函數。是為了解決view thread 和app thread不同步的時候出現的問題。我們看一下怎么使用app中獲取的用戶信息

1 onLoad: function () { 2 wx.showLoading({ 3 title: '加載中', 4 }); 5 if (app.globalData.userInfo) { 6 this.setData({ 7 userInfo: app.globalData.userInfo 8 }); 9 wx.hideLoading(); 10 11 } else { 12 app.userInfoReadyCallback = res => { 13 if (res) { 14 this.setData({ 15 userInfo: res 16 }); 17 wx.hideLoading(); 18 } 19 }; 20 } 21 22 }
注意:本地調試的,小程序不再提供彈框的方式授權。需要使用button組件,讓用戶點擊彈出授權。我在獲取用戶信息的時候,如果沒有授權,會跳轉到一個專門的授權頁面。
1 <button wx:if="{{!scopeUserinfo}}" open-type="getUserInfo"bindgetuserinfo="getUserInfo">測試版:請同意授權 2 </button>
二、workman實時通訊
這個workman可以查看官方文檔,配置起來很簡單。不過需要注意的一點就是:小程序只支持wss的協議,必須是帶證書的https的請求。我們需要改一下workman中的代碼。進入start_gateway.php這個文件,配置下證書參數。transport改為ssl
1 // gateway 進程,這里使用Text協議,可以用telnet測試 2 // $gateway = new Gateway("tcp://0.0.0.0:8282"); 3 $context = array( 4 // 更多ssl選項請參考手冊 http://php.net/manual/zh/context.ssl.php 5 'ssl' => array( 6 // 請使用絕對路徑 7 'local_cert' => 'E:/www/GatewayWorker-for-win/1535601_wxapp.x-nev.com.pem', // 也可以是crt文件 8 'local_pk' => 'E:/www/GatewayWorker-for-win/1535601_wxapp.x-nev.com.key', 9 'verify_peer' => false, 10 // 'allow_self_signed' => true, //如果是自簽名證書需要開啟此選項 11 ) 12 ); 13 $gateway = new Gateway("Websocket://0.0.0.0:1451",$context); 14 $gateway->transport = 'ssl';
在小程序的開發工具中要配置,wss域名:1451 。這個端口是在workman中配置,可以試任意端口。至此workman和小程序的連接打通了。
邀請好友pk,就是開一個房間(在workman中對應組的概念),然后在分享的鏈接中加入roomid這個參數。在用戶登入之后,通過workman通知對方。
分享按鈕對應的代碼:

1 onShareAppMessage: function (res) { 2 if (res.from === 'button') { 3 var timestamp = new Date().getTime(); 4 var r = parseInt(Math.random() * (100000 - 5000 + 1) + 5000, 10); 5 var roomid = timestamp + '' + r; 6 var s_endtime = timestamp + 600000; 7 var s_openid = wx.getStorageSync('openid'); 8 return { 9 title: '來啊,狗蛋!對戰吧', 10 path: '/pages/dz/dz?pkroom=' + roomid+'&endtime='+s_endtime+'&fqzid='+s_openid, 11 success: function () { 12 var datastr= {pkroom: roomid, endtime: s_endtime, fqzid: s_openid}; 13 wx.setStorage({ 14 key: 'pkinfo', 15 data:datastr 16 }) 17 wx.navigateTo({ 18 url: '/pages/dz/dz' 19 }); 20 } 21 22 } 23 } else { 24 return { 25 title: '來啊,狗蛋!', 26 path: '/pages/index/index', 27 imageUrl: '/imgs/yang.jpg' 28 } 29 } 30 }
用戶通過鏈接進入對戰頁面初始化代碼:

1 onLoad: function (options) { 2 3 inituserinfo = new Promise(function (resolve, reject) { 4 if (app.globalData.userInfo) { 5 resolve(options); 6 } else { 7 app.userInfoReadyCallback = res => { 8 resolve(options); 9 }; 10 } 11 }); 12 inituserinfo.then(options => { 13 if (options.pkroom) { 14 //有參數,判斷是否有效期內,判斷pkinfo,對比openid 15 if (options.endtime > new Date().getTime()) { 16 //有效期內。 17 var pkinfo = wx.getStorageSync('pkinfo') || {}; 18 pkinfo.endtime = options.endtime; 19 pkinfo.fqzid = options.fqzid; 20 pkinfo.pkroom = options.pkroom; 21 wx.setStorage({ 22 key: 'pkinfo', 23 data: { 'pkroom': pkinfo.pkroom, 'endtime': pkinfo.endtime, 'fqzid': pkinfo.fqzid } 24 }) 25 this.intime(pkinfo); 26 } else { 27 //url上房間過期 28 this.overtime(); 29 } 30 } else { 31 //沒有參數,則判斷有無pkinfo; 32 var pkinfo = wx.getStorageSync('pkinfo'); 33 console.log("沒有參數:取pkinfo:" + pkinfo.endtime); 34 console.log("沒有參數:當前時間:" + new Date().getTime()); 35 if (pkinfo && (pkinfo.endtime > new Date().getTime())) { 36 //在有效期內,更新計時器。對比openid,加載用戶信息 37 this.intime(pkinfo); 38 //開始監聽 39 } else { 40 //沒有,或則過期,清空cookie 41 this.overtime(); 42 } 43 } 44 }); 45 },
拿到url參數,我們要判斷,這個房間誰是房主,誰是挑戰者。 要判斷房間是否在有效期內,房主是否關閉房間。因為用戶進入挑戰頁的方式太多,不同方式對應這不同的情況。

1 //有效期內 2 intime: function (pkinfo) { 3 //在有效期內,更新計時器。對比openid,加載用戶信息 4 //計算剩余時間 5 var stime = pkinfo.endtime - new Date().getTime(); 6 endtime = parseInt(stime / 1000); 7 var s_openid = wx.getStorageSync('openid'); 8 var userInfo = { 9 'name': app.globalData.userInfo.nickName, 10 'img': app.globalData.userInfo.avatarUrl, 11 'openid': s_openid 12 }; 13 if (s_openid == pkinfo.fqzid) { 14 this.setData({ 15 fqz: userInfo, 16 type: 'fqz', 17 pkroom: pkinfo.pkroom 18 }) 19 } else { 20 this.setData({ 21 tzz: userInfo, 22 type: 'tzz', 23 pkroom: pkinfo.pkroom 24 }) 25 } 26 }, 27 //房間過期 28 overtime: function () { 29 wx.showToast({ 30 title: '對戰房間已過期', 31 icon: 'info', 32 duration: 1000 33 }); 34 wx.removeStorage({ 35 key: 'pkinfo' 36 }) 37 setTimeout(() => { 38 wx.redirectTo({ 39 url: '/pages/index/index' 40 }) 41 }, 1800); 42 },
現在最關鍵的地方來了:創建socket連接及監聽!!!!!
小程序只允許同時創建不超過兩個socket連接。在頁面創建連接的時候尤其得注意,頁面跳轉、退出、隱藏、顯示的過程中,要保護好線程。所以我們統一在onunload的時候,主動關閉socket連接。下次進來的時候根據儲存到本地的房間信息重新創建連接。我封裝了一個sockethelper.js,因為這個init/open的過程都是異步的,所以使用了promise封裝了一下。(class、constructor是es6的語法,不清楚的可以先看一下es6的語法)

1 class webSocket { 2 3 constructor(obj = {}) { 4 this.hasConn = false 5 this.hasOpen = false 6 this.msghandle = obj; 7 this.msghandle['ping'] = function (e, _this) { 8 var data = { 'type': 'pong' }; 9 _this.SendMsg(data); 10 } 11 var _this = this; 12 this.SocketTask = wx.connectSocket({ 13 url: 'wss://wxapp.x-nev.com:1451', 14 header: { 15 'content-type': 'application/json' 16 }, 17 method: 'post', 18 success: function (res) { 19 _this.hasConn = true; 20 }, 21 fail: function (err) { 22 wx.showToast({ 23 title: '網絡異常!', 24 }) 25 }, 26 }); 27 } 28 StartListen() { 29 var _this = this; 30 return new Promise((resolve, reject) => { 31 32 this.SocketTask.onOpen(function (res) { 33 console.log("page:socket:open"); 34 _this.hasOpen = true; 35 resolve(res); 36 }) 37 this.SocketTask.onClose(function (res) { 38 console.log("page:socket:close") 39 }) 40 41 this.SocketTask.onError(function (res) { 42 console.log("page:socket:error-"); 43 // reject(res); 44 }) 45 46 this.SocketTask.onMessage(function (onMessage) { 47 var data = JSON.parse(onMessage.data); 48 var msgtype = data['type']; 49 if (msgtype in _this.msghandle) { 50 _this.msghandle[msgtype](data, _this); 51 } 52 }) 53 54 }); 55 56 } 57 SendMsg(msg, callback) { 58 if (this.hasOpen && this.hasConn) { 59 this.SocketTask.send({ 60 data: JSON.stringify(msg), 61 success: (e) => { if (callback) { callback(e) } } 62 }) 63 } else { 64 console.log("沒有open,調用一下"); 65 this.StartListen().then(() => { 66 this.SocketTask.send({ 67 data: JSON.stringify(msg), 68 success: (e) => { if (callback) { callback(e) } } 69 }) 70 }); 71 } 72 73 } 74 } 75 76 module.exports = { webSocket };
用戶進入房間后,初始化參數后,要通知對方。

1 onReady: function () { 2 3 var info = {}; 4 var s_openid = wx.getStorageSync('openid'); 5 info.openid = s_openid; 6 info.pkroom = this.data.pkroom; 7 if (this.data.type == "fqz") { 8 info.mark = "fqz"; 9 info.name = this.data.fqz.name; 10 info.img = this.data.fqz.img; 11 } else { 12 info.mark = "tzz" 13 info.name = this.data.tzz.name; 14 info.img = this.data.tzz.img; 15 } 16 //如果從分享挑戰頁面進來,要注冊獲取用戶信息的回調,注冊socket 17 this.registResponse(info); 18 app.globalData.stask.SendMsg({ 19 'type': 'pk', 20 'mark': info.mark, 21 'openid': info.openid, 22 'name': info.name, 23 'img': info.img, 24 'pkroom': info.pkroom 25 }); 26 this.formattime(); 27 t2 = setInterval(() => { 28 this.formattime(); 29 }, 1000); 30 }, 31 formattime: function () { 32 endtime--; 33 var m = parseInt(endtime / 60); 34 var s = parseInt(endtime % 60); 35 m = m.toString().length > 1 ? m : '0' + m; 36 s = s.toString().length > 1 ? s : '0' + s; 37 var tem = `${m}:${s}`; 38 this.setData({ 39 time: tem 40 }); 41 },
當雙方都進入房間后。房主出發start事件,開始答題。start事件請求后台分配對應等級的題目,並通知對方題目

1 start: function () { 2 //請求對應等級的題目, 3 wx.request({ 4 url: 'https://fotonpickup.risingad.com/wxxldev/index.php?c=WXSignApi&a=Question', 5 data: { 'roomid': this.data.pkroom, 'one': this.data.fqz.openid, 'two': this.data.tzz.openid }, 6 success: (result) => { 7 this.setData({ 8 qlist: result.data.row, 9 recordid: result.data.flag, 10 current: 0 11 }); 12 //開始答題通知workman,提供題目id。跳轉對戰頁面。 13 app.globalData.stask.SendMsg({ 14 'type': 'ready', 15 'recordid': result.data.flag, 16 'uid': this.data.tzz.openid 17 }); 18 } 19 }); 20 21 },
雙方開始答題,配置響應事件。這個就不一一列舉了,上代碼

1 registResponse: function (pkinfo) { 2 var obj = { 3 'pk': (res, _this) => { 4 //拿到用戶信息,綁定用戶頭像。 5 if (res.content.length == 1) { 6 if (res.content[0].mark == "fqz") { 7 this.setData({ 8 'fqz': res.content[0] 9 }); 10 } else { 11 this.setData({ 12 'tzz': res.content[0] 13 }); 14 } 15 } else { 16 if (res.content[0].mark == "fqz") { 17 this.setData({ 18 'fqz': res.content[0], 19 'tzz': res.content[1] 20 }); 21 } else { 22 this.setData({ 23 'fqz': res.content[1], 24 'tzz': res.content[0] 25 }); 26 } 27 } 28 }, 29 'ready': (res, _this) => { 30 //此處注冊的監聽,只有tzz可以捕獲到。后端是單獨推送的 31 //挑戰者請求加載相應的題目,並准備好界面。 32 console.log("拉取題:" + res.id); 33 wx.request({ 34 url: 'https://fotonpickup.risingad.com/wxxldev/index.php?c=WXSignApi&a=GetQuestion', 35 data: { 'id': res.id }, 36 success: (result) => { 37 this.setData({ 38 qlist: result.data, 39 current: 0 40 }); 41 //通知服務器tzz准備好了, 42 app.globalData.stask.SendMsg({ 'type': 'start' }); 43 } 44 }); 45 }, 46 'start': (res, _this) => { 47 //開始各自准備pk界面。 48 this.setData({ 49 start: true 50 }); 51 //在這里開啟當前計時器。 52 console.log("1.開啟計時器"); 53 t1 = setInterval(() => { 54 55 if (this.data.time2 > 0) { 56 this.setData({ 57 time2: this.data.time2 - 1 58 }); 59 if (this.data.myself && this.data.opposite) { 60 //都已答題,如果不是最后一題,進入下一題。並積分,初始化狀態 61 console.log("2.都已答題"); 62 if (this.data.current == this.data.qlist.length - 1) { 63 //結束,出成績。 64 clearInterval(t1); 65 console.log("9.結束計時器"); 66 this.setData({ 67 isend: true 68 }); 69 this.next(true); 70 } else { 71 console.log("3.調用下一題"); 72 this.next(); 73 } 74 } 75 } else { 76 //時間到了,未答題。展示正確答案。進入下一題。 77 //初始化 答題狀態、時間、樣式。 78 console.log("4.時間到了有人未答題"); 79 if (this.data.current == this.data.qlist.length - 1) { 80 //結束,出成績。 81 clearInterval(t1); 82 this.setData({ 83 isend: true 84 }); 85 this.next(true); 86 console.log("10.結束計時器"); 87 } else { 88 this.data.classlist[this.data.qlist[this.data.current].ropt - 1] = 'right'; 89 setTimeout(() => { 90 console.log("5.時間到了有人未答題,顯示正確答案,進入下一題"); 91 this.next(); 92 }, 1000); 93 94 } 95 96 } 97 }, 1000); 98 }, 99 'dt': (res, _this) => { 100 console.log("7." + res.mark + "選擇了" + res.opts); 101 var isright = res.opts == this.data.qlist[this.data.current].ropt ? true : false; 102 if (res.mark != this.data.type) { 103 this.setData({ 104 opposite: true,//對方已答題。 105 oppositeright: isright, 106 oppositeopt: res.opts 107 }) 108 } else { 109 this.setData({ 110 myself: true,//自己已答題 111 myselfright: isright 112 }); 113 if (isright) { 114 //答對 115 this.data.classlist[res.opts - 1] = 'right'; 116 } else { 117 this.data.classlist[res.opts - 1] = 'error'; 118 } 119 } 120 if (this.data.myself && this.data.opposite) { 121 //都已答題 122 this.data.classlist[this.data.qlist[this.data.current].ropt - 1] = 'right'; 123 this.data.classlist[this.data.oppositeopt - 1] = this.data.oppositeright ? 'right' : 'error'; 124 } 125 this.setData({ 126 'classlist': this.data.classlist 127 }); 128 }, 129 'gameover': (res, _this) => { 130 wx.showToast({ 131 title: '房主關閉房間', 132 icon: 'info', 133 duration: 1000 134 }); 135 wx.removeStorage({ 136 key: 'pkinfo', 137 complete: function (e) { 138 console.log(e); 139 } 140 }); 141 setTimeout(() => { 142 wx.redirectTo({ 143 url: '/pages/index/index' 144 }) 145 }, 2000); 146 } 147 } 148 //開啟監聽 149 app.globalData.stask = new webSocket(); 150 //注冊監聽響應事件 151 Object.assign(app.globalData.stask.msghandle, obj); 152 153 },
在上一下workman都監聽的代碼

1 /** 2 * 當客戶端發來消息時觸發 3 * @param int $client_id 連接id 4 * @param mixed $message 具體消息 5 */ 6 public static function onMessage($client_id, $message) 7 { 8 // 向所有人發送 9 10 $message_data = json_decode($message, true); 11 $now = date('y-m-d H:i:s', time()); 12 $filepath="E:\\www\\GatewayWorker-for-win\\msg.txt"; 13 file_put_contents($filepath, "time:$now,client_id:".$client_id.':msg:'.$message.PHP_EOL, FILE_APPEND); 14 if (!$message_data) { 15 return ; 16 } 17 18 // 根據類型執行不同的業務 19 switch ($message_data['type']) { 20 21 // 客戶端回應服務端的心跳 22 case 'pong': 23 return; 24 // 客戶端登錄 message格式: {type:login, name:xx, room_id:1} ,添加到客戶端,廣播給所有客戶端xx進入聊天室 25 case 'login': 26 // 判斷是否有房間號 27 if (!isset($message_data['room_id'])) { 28 return ''; 29 } 30 // 把房間號昵稱放到session中 31 $room_id = $message_data['room_id']; 32 //昵稱 33 $client_name = htmlspecialchars($message_data['client_name']); 34 //頭像 35 $client_img= $message_data['client_img']; 36 // 設置當前用戶的sesion可直接設置。等同於 Gateway::setSession(string $client_id, array $session); 37 $_SESSION['room_id'] = $room_id; 38 $_SESSION['client_name'] = $client_name; 39 $_SESSION['client_img'] = $client_img; 40 // 轉播給當前房間的所有客戶端,xx進入聊天室 message {type:login, client_id:xx, name:xx} 41 $new_message = array('type'=>$message_data['type'], 'client_id'=>$client_id, 'client_name'=>htmlspecialchars($client_name), 'time'=>date('Y-m-d H:i:s')); 42 Gateway::sendToGroup($room_id, json_encode($new_message)); 43 Gateway::joinGroup($client_id, $room_id); 44 // 獲取房間內所有用戶列表 45 $clients_list = Gateway::getClientSessionsByGroup($room_id); 46 foreach ($clients_list as $tmp_client_id=>$item) { 47 $clients_list[$tmp_client_id] = $item['client_name']; 48 } 49 // 給當前用戶發送用戶列表 50 $new_message['client_list'] = $clients_list; 51 Gateway::sendToCurrentClient(json_encode($new_message)); 52 return; 53 54 // 客戶端發言 message: {type:say, to_client_id:xx, content:xx} 55 case 'say': 56 // 非法請求 57 if (!isset($_SESSION['room_id'])) { 58 throw new \Exception("\$_SESSION['room_id'] not set. client_ip:{$_SERVER['REMOTE_ADDR']}"); 59 } 60 $room_id = $_SESSION['room_id']; 61 $client_name = $_SESSION['client_name']; 62 $client_img = $_SESSION['client_img']; 63 // 私聊 64 if ($message_data['to_client_id'] != 'all') { 65 $new_message = array( 66 'type'=>'say', 67 'from_client_id'=>$client_id, 68 'from_client_name' =>$client_name, 69 'to_client_id'=>$message_data['to_client_id'], 70 'content'=>nl2br(htmlspecialchars($message_data['content'])), 71 'time'=>date('Y-m-d H:i:s'), 72 ); 73 Gateway::sendToClient($message_data['to_client_id'], json_encode($new_message)); 74 return Gateway::sendToCurrentClient(json_encode($new_message)); 75 } 76 77 $new_message = array( 78 'type'=>'say', 79 'from_client_id'=>$client_id, 80 'from_client_name' =>$client_name, 81 'to_client_id'=>'all', 82 'client_img'=> $client_img , 83 'content'=>nl2br(htmlspecialchars($message_data['content'])), 84 'time'=>date('Y-m-d H:i:s'), 85 ); 86 return Gateway::sendToGroup($room_id, json_encode($new_message)); 87 case 'pk': 88 //{type:pk, pkroom:xx,name:'xxx',img:'xxx'} 89 $pkroom= $message_data['pkroom']; 90 $_SESSION['pkroom']= $pkroom; 91 $_SESSION['name']= $message_data['name']; 92 $_SESSION['img']= $message_data['img']; 93 $_SESSION['mark']= $message_data['mark']; 94 $_SESSION['openid']= $message_data['openid']; 95 //綁定openid到client_id; 96 Gateway::bindUid($client_id, $message_data['openid']); 97 //開一個新房間 98 Gateway::joinGroup($client_id, $pkroom); 99 //返回當前對戰房間的人員 100 101 $clients_list = Gateway::getClientSessionsByGroup($pkroom); 102 foreach ($clients_list as $tmp_client_id=>$item) { 103 $pklist[]=$item; 104 } 105 $new_message = array( 106 'type'=>'pk', 107 'content'=>$pklist 108 ); 109 return Gateway::sendToGroup($pkroom, json_encode($new_message)); 110 case 'dt': 111 $opts=$message_data['opts']; 112 $mark= $_SESSION['mark']; 113 $pkroom= $_SESSION['pkroom']; 114 $new_message = array( 115 'type'=>'dt', 116 'opts'=>$opts, 117 'mark'=>$mark 118 ); 119 return Gateway::sendToGroup($pkroom, json_encode($new_message)); 120 case 'ready': 121 $recordid= $message_data['recordid']; 122 $uid= $message_data['uid'];//tzz的uid 123 $new_message = array( 124 'type'=>'ready', 125 'id'=>$recordid 126 ); 127 //只給tzz響應 128 return Gateway::sendToUid($uid, json_encode($new_message)); 129 case 'start': 130 $pkroom= $_SESSION['pkroom']; 131 $new_message = array( 132 'type'=>'start' 133 ); 134 return Gateway::sendToGroup($pkroom, json_encode($new_message)); 135 case 'gameover': 136 $pkroom= $_SESSION['pkroom']; 137 $new_message = array( 138 'type'=>'gameover' 139 ); 140 Gateway::sendToGroup($pkroom, json_encode($new_message)); 141 return Gateway::ungroup($pkroom); 142 } 143 }
答題基本的思路就是這樣。里面有個聊天室也是基於workman開發的,見圖5,樣式是模仿微信。有空會重新補充一下細節。現在上一下圖。本人是后台程序,前端不太擅長,一些細節沒有優化,請見諒。有問題的可以聯系我:mian_wu@qq.com