背景:这个项目,我只参与项目的一部分业务代码开放(摇一摇游戏业务),只是简单来使用封装好的一些类和方法,核心实现并不是我写的,并且核心代码设计思路并没有文档,当然代码中有些注释,他人也并没有太多时间给我好好讲讲他的设计思路。
目的:分析他人的代码,完全掌握他人的设计思路,抓住重点,提取好的思路,供之后类似的项目开放 做参考!
解读他人代码/分析代码 的方法
多维度分析代码:
一: 看代码组织结构,类的继承关系
二:程序的执行流程
三: 数据设计(redis 设计,gatewayWorker 的组 UID 设计)
四:从gatewayWorker 框架 用到的方法,统计都使用了哪些功能
五:还原当时的需求,遗忘的不清晰的需求,可以自己提供合理的需求
说明: 方法一二 是大多数人常用的分析他人代码的方法。包括我也是。但是仅仅使用这两种方法,还是会云里雾里的,原因在于这里边涉及需求和他人的设计思路。方法三:是最容易忽视的,包括我也是,最初看代码的时候,没想到这个维度。数据设计也是最能体现他人设计思路的。方法五中需求:要看情况分析,如果是我正在维护的开放的,那么猜测的需求最好求证明白,如果只是拿来学习,又没人给你讲,或者是上个公司的项目,这种情况就靠自己提供合理的需求的。即便是真的和当时的需求不一样也没关系,因为我的目的是学习他人整体设计思路,这些不重要的细节就不重要了。
从 三 和 五 的维度 来分析他人代码 能 看出 他人 写的不好的地方,甚至找出bug
学习目的/想要的成果
一: 代码的组织结构,类的继承关系。 如:是不是 用到了好的设计模式?
二: 从需求 到 数据设计,如:他抽象的是否合理,数据设计的是否合理
三: 从需求 到 技术选型: 选择的技术 是否合适?
四: 代码的规范: 是否规范,易懂,有学习的地方吗?
项目需求(包含业务 ,和 技术方面的)
业务方面的: 大屏端 展示游戏界面(分为 游戏进入界面,游戏进行界面,游戏结束展示排名界面),使用手机里边的微信扫码大屏端的二维码,关注公众号后,后端会推送一个游戏链接,点击链接即可加入游戏。游戏开始时,手机端可以进行操作,以赛车游戏来说,可以向左,向右,加速,减速控制大屏中自己控制的赛车。
后端的语言框架选择,要能支持 websocket 协议, 与其他项目的联系 使用 curl 方式
先看看目录结构:
说明: 与从官网下载下来的gateWayWorker 相比目录上 只是多了一个Classes目录 ,其中Bases是基类,其他的都是子类,每一个文件对应一款游戏
那么如何加载class下面的这些类呢?可以参考下他的实现方法,代码如下:
Common/Events.php
private static function _loadGameClass(){
$fileDir = dirname(__FILE__) . '/Classes/';
$classFiles = scandir($fileDir);
foreach($classFiles AS $fKey => $fVal){
if(strpos($fVal, '.php') !== false){
require_once $fileDir . $fVal;
}
}
}
有没有更好的方法来加载呢? 当然有了,使用vendor 的加载方法,或者自己实现一个遵循PSR-4规范的方法。
todo
那么如何 路由,也就是不同的游戏来了执行不同的 类和方法?
他的思路是: 通过 消息中的 game_name字段来 ,加载不同的类;消息中的 type 来执行不同的方法。//这个处理方式可以借鉴
代码:
Events.php
public static function onMessage($client_id, $message)
{
self::_loadGameClass();
// 处理json数据
$messageData = json_decode($message, true);
if(!$messageData){
return ;
}
$gameName = (isset($messageData['game_name'])) ? ucfirst($messageData['game_name']) : 'Error';
if(class_exists($gameName, false)){
$gameName::setDefault($client_id, $messageData);
$gameName::init($client_id, $messageData);
$gameName::$messageData['type']($messageData);
} else {
self::onClose($client_id);
}
}
Bases.php 类分析:
数据设计
属性:
maxUserCount = 20 //最大用户数量 由子类初始化, 指的是可以同时参与游戏的最大玩家数量。
maxWaitUserCount = 200 //最大等待用户数量
针对gateway 定义的组名:
Online[room_id] //正在玩游戏的这个组 包括( 多个手机端)
RoomBird[room_id] //等待进入游戏的这个组。( 多个手机端)
clientUserGroup[room_id] //包含所有的client 的这个组(不管是正在玩游戏的还是 等待玩游戏的 都要加入这个组)
针对gateway 定义的uid:
Screen[room_id] //大屏端 绑定的uid
redis 的使用:
key 类型 可能的值
gameState[room_id] string 1 ; 2 ; //游戏的状态: 0 游戏未开始/大屏断连接了 1 游戏在进人状态; 2游戏进行中; 3游戏结束/游戏正在显示排名
wxOpenIdOnline[room_id] string json串; //游戏在线 openid list
gameQueueSort[room_id] int //号码牌计数 从1开始 ,大屏排 1,其他手机端 从 2开始, 依此 3, 4 。。。。
属性:
包含几类属性:
一: 大屏端 UID 名称 //值为 大屏端UID 名称
一: 各类 组名称 :// 值为 组名称
二: 各类 redis key 名称 // 值 为redis key 名称
三: 连接的 client_id
方法
init($client_id, $message)
//初始化静态属性
//获取API token
screen_login($message) 大屏登录
判断大屏是否在线 Gateway::isUidOnline
—如果已经在线 return Gateway::sendToCurrentClient “登录失败”
— 如果没有在线
—— 给该连接 绑上Screen[room_id] Gateway::bindUid
—— return Gateway::sendToCurrentClient “登录成功“
close_client() 踢掉已在线 大屏端 实际上 游戏端的js 并没有用到
scoreAdd() 同步分数 由 大屏端 发给 某一个 手机端
判断 message 中的 client_id 是否在线 Gateway::isUidOnline
— 是: 发送 消息 给该 cliend_id, 告诉他 当前这一刻他的分数。 Gateway::sendToUid
gameState() 设置游戏状态 大屏端发起:开始游戏 发一次state=1; 进入游戏场景再发一次 state=2; 进入结束页面 再发一次 state=3
// redis 操作 设置 游戏状态
//判断 state == 1 // 扫码进入阶段
// 是
// — 操作redis 删除 wxOpenIdOnline[room_id] ; 清空 “Online[room_id]” 组 , 和 RoomBird[room_id] 组
// else state ==2 //游戏立刻开始
// — 没干啥 特别有意义的事
// 发送消息 给 大屏端 说 “游戏状态 已经设置好了
gameTime 将 大屏端的 时间 组发
将 时间 发给组“Online[room_id]” Gateway::sendToGroup
login() 客户端登录
//获取游戏状态 从gameState[room_id]
//设置session
绑定UID 使用 open_ID ; Gateway::bindUid
将此client_id 加入到 组“clientUserGroup[room_id]” GateWay::joinGroup
发送消息给当前用户 ,告诉他 登录成功 : Gateway::sendToCurrentClient
判读用户openid 是否在线
— 是 即说明 客户端是 重连 ; 操作redis 从 wxOpenIdOnline[room_id] 取出 当前用户的isdead type=reGame信息(包括 游戏状态,isDead, time())发送给 当前client
发送消息 给当前用户 Gateway::sendToCurrentClient ;信息={type:gameState, state:redisGameState; time: time()}
peopleNUm 加入游戏
_setRoomGroup() //将 当前用户加入 要么 Online[room_id],要么 “RoomBird[room_id]” 组
//判断 如果 正在等待游戏的人数 > 0 or 游戏正在 进行中 or 游戏结束正在显示排名
— 是 : redis 中 获取 queueSort (int类型) ; 更新 session 中 sort 信息; redis操作 将 queueSort 自增 1; 发送消息给当前手机端“当前的排队数量”
// if 如果 没有玩家正在排队(roomCnt=0) && 游戏正在进人状态
— 是 :发送消息is Ok 给 当前client_id; 发送消息 ready 给当前大屏端。
gameFinish 游戏结束:由大屏端发送的消息
// curl 获取优惠券。。。 不关心此部分逻辑
// 从 message 中获取 to_client_id ;
//判断 此 to_client_id 是否 在线
//— 是 发送 消息 给此 用户 告诉他“他的 排名,分数,获得的奖项等” ;
//发送 消息给 大屏端,告诉他 这个(to_client_id) 用户获得的 奖项。
gameDead 用户死掉 由大屏端 发起的消息
//判读此 message 中指定的 to_client_id 是否在线 Gateway::isUidOnline
//— 是: redis 操作 wxOpenIdOnline[room_id] ;取出信息并 json_decode 下 将该用户对应的 isDead 标记为 “死亡” 含义; 代码中isDead=0 代表死亡含义。
// 发送消息给这个用户(to_client_id) 告诉他,他已经死亡了。
addNewPlayer() 从等待队列 拉一个用户 由大屏端发起; 只有 364,367游戏会发起
//获取“排队等待的/RoomBird[room_id] ”组 内的 玩家的总数: Gateway::getClientCountByGroup
//判断 如果是 玩家总数 大于 0
//—是: 获取 “RoomBird[room_id]” 组内 所有成员的详细信息(指的是存在session里的)
//— 按照 每个成员中的 信息中的 sort 对这些成员信息 进行排序
//— 循环遍历 成员信息
//— — 对第一个 成员 离开 等待组,加入 在线游戏组。给他发送消息,你进入游戏了。给 大屏发消息 这个人 成功拉进来了
//— — 对第二个 及以后的成员 ,给他发消息 你前面还有 i 个人 在排队。
op 手机端 发送给大屏端; 用于 该游戏特有的 消息,
_setRoomGroup 用户进入游戏时,将人加到正在游戏组或 等待组中
// 获取游戏状态
// 获取当前正在参与游戏中的 玩家数量
//判断 : 如果 正在参与游戏中的 玩家数量 没有超过最大用户数(maxUserCount) && 游戏正处于 进人状态
— 是 : 将当前 socketClientId 加入到 组 “ Online[room_id]” 中
redis数据操作 将 wxopenID , isDead=1 追加到 key = wxOpenIdOnline[room_id] 中
&& return true
// 获取 当前正在排队 的 用户的数量
//判断: 如果 当前正在排队的 数量 小于 最大等待游戏的 数量
— 是 : 将当前 socketCliendId 加入到 组 “RoomBird[room_id]” && return true
return false
Events.php onClose() 中 做的事
调用 Base:: socketClose()
//socketClose()
// 获取游戏的状态
// 判断 如果是 手机端(手柄端); 依据 isset($_SESSION['client_open_ID’])
//— 是 : 继续判断 游戏状态 是 == 1 正在进入状态吗
—- — 是: 操作redis 将此用户(wxOpenID) 从 wxOpenIdOnline[room_id] 中删除掉; && 发送 type=logout 的消息给 大屏端 告诉他 这个用户 断开连接了。
// — 否: 就认为是 大屏端:
操作redis 将gameState[room_id] 设置为 0 ; 发送消息给 组“clientUserGroup[room_id]” 告诉他 ,大屏掉线了 gameStatus = 0
关于消息type=addNewPlayer 的使用方式的 详细考证如下:
367: bird 愤怒的小鸟 - 大屏端
this.schedule(this.addQueuePlayer,2,262144,0)
addQueuePlayer:function(){
if(this.m_players.length<this.max_peopleNum){
WebSocketManager.getInstance()._wsObj.send('{"game_name":"bird","type":"addNewPlayer"}')
}
}
else if("ready"==a.type||"addNewPlayer"==a.type){
var c=a.client_name,
d=a.client_id;
a=a.client_image;
for(b=0;b<this.m_players.length;b++){
var e=this.m_players[b];
if(e.m_id==d){
this.m_players[b].removeFromParent();
this.m_players.splice(b,1);
this.UpdataScore();
break
}
}
this.addBird(c,d,a)
}else if("requeue"==a.type)
364: snake 大屏端:
this.schedule(this.addNewp,2,262144);
addNewp:function(){
if(this.m_players.length<(g_max_peopleNum? g_max_peopleNum:20)){
mylog("message")
WebSocketManager.getInstance()._wsObj.send('{"game_name":"snake","type":"addNewPlayer"}'
}
},
if("addNewPlayer”==b.type && !this.m_gameover){
c=b.client_name;
d=b.client_id;
b=b.client_image;
for(a=0;a<this.m_playerall.length;a++){
var e=this.m_playerall[a];
if(e.m_id==d&&e.m_name==c&&e.m_headImage==b){
this.m_playerall.splice(a,1);
this.UpdataScore();
break
}
}
this.addNewPlayer(c,b,d)
}
367: bird 愤怒的小鸟 - 手机端
if("isOk"==b.type||"addNewPlayer"==b.type) {
this.m_waitingText.setVisible(false), // 等待文字 设置为不可见
this.m_help_tip.setVisible(true), // help 提示 设置为 可见
this.m_click_button.setEnabled(true), // 点击按钮 设置为 启用状态
(this.m_click_button.setVisible(true), //点击按钮 设置为 可见
this.m_restart_button.setEnabled(false), //重新开始按钮 设置为 禁用状态
this.m_restart_button.setVisible(false); //重新开始按钮 设置为 不可见
}
364 : snake 贪吃蛇:- 手机端
手机端收到 type=addNewPlayer 的消息代表 被大屏端 拉取成功,可以参与游戏了:
if("addNewPlayer"==a.type){
cc.director.runScene(new GameScene)) //执行 游戏操作场景
}
if(”peopleNum"==a.type){
this.m_label_people.setVisible(true),
this.m_waitingPeople=a.num,
this.m_label_people.setString("还有"+this.m_waitingPeople+"人在等待"),
if(0==this.m_waitingPeople){
cc.director.runScene(new WaitingScene)
}
}
上面细节不重要,总结就是:addNewPlayer 由大屏端发起,在 小鸟和贪吃蛇的游戏中 每隔两秒就 拉人就来,一次拉一个人。
用到的GateWayWorker 的方法
Gateway::isUidOnline
Gateway::sendToCurrentClient
Gateway::bindUid
Gateway::sendToUid
Gateway::getClientIdByUid
Gateway::closeClient
Gateway::getClientCountByGroup
Gateway::getClientInfoByGroup
Gateway::leaveGroup
Gateway::joinGroup
Gateway::sendToClient
Gateway::sendToGroup
Gateway::sendToCurrentClient
Gateway::getSession
Gateway::updateSession
$_SESSION['room_id'] =
优化的方向:
组名,消息名的 命名规范!