背景:这个项目,我只参与项目的一部分业务代码开放(摇一摇游戏业务),只是简单来使用封装好的一些类和方法,核心实现并不是我写的,并且核心代码设计思路并没有文档,当然代码中有些注释,他人也并没有太多时间给我好好讲讲他的设计思路。
目的:分析他人的代码,完全掌握他人的设计思路,抓住重点,提取好的思路,供之后类似的项目开放 做参考!
多维度分析代码:
一: 看代码组织结构,类的继承关系
二:程序的执行流程
三: 数据设计(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);
}
}
maxUserCount = 20 //最大用户数量 由子类初始化, 指的是可以同时参与游戏的最大玩家数量。
maxWaitUserCount = 200 //最大等待用户数量
Online[room_id] //正在玩游戏的这个组 包括( 多个手机端)
RoomBird[room_id] //等待进入游戏的这个组。( 多个手机端)
clientUserGroup[room_id] //包含所有的client 的这个组(不管是正在玩游戏的还是 等待玩游戏的 都要加入这个组)
Screen[room_id] //大屏端 绑定的uid
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
//初始化静态属性
//获取API token
判断大屏是否在线 Gateway::isUidOnline
—如果已经在线 return Gateway::sendToCurrentClient “登录失败”
— 如果没有在线
—— 给该连接 绑上Screen[room_id] Gateway::bindUid
—— return Gateway::sendToCurrentClient “登录成功“
判断 message 中的 client_id 是否在线 Gateway::isUidOnline
— 是: 发送 消息 给该 cliend_id, 告诉他 当前这一刻他的分数。 Gateway::sendToUid
// redis 操作 设置 游戏状态
//判断 state == 1 // 扫码进入阶段
// 是
// — 操作redis 删除 wxOpenIdOnline[room_id] ; 清空 “Online[room_id]” 组 , 和 RoomBird[room_id] 组
// else state ==2 //游戏立刻开始
// — 没干啥 特别有意义的事
// 发送消息 给 大屏端 说 “游戏状态 已经设置好了
将 时间 发给组“Online[room_id]” Gateway::sendToGroup
//获取游戏状态 从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()}
_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 给当前大屏端。
// curl 获取优惠券。。。 不关心此部分逻辑
// 从 message 中获取 to_client_id ;
//判断 此 to_client_id 是否 在线
//— 是 发送 消息 给此 用户 告诉他“他的 排名,分数,获得的奖项等” ;
//发送 消息给 大屏端,告诉他 这个(to_client_id) 用户获得的 奖项。
//判读此 message 中指定的 to_client_id 是否在线 Gateway::isUidOnline
//— 是: redis 操作 wxOpenIdOnline[room_id] ;取出信息并 json_decode 下 将该用户对应的 isDead 标记为 “死亡” 含义; 代码中isDead=0 代表死亡含义。
// 发送消息给这个用户(to_client_id) 告诉他,他已经死亡了。
//获取“排队等待的/RoomBird[room_id] ”组 内的 玩家的总数: Gateway::getClientCountByGroup
//判断 如果是 玩家总数 大于 0
//—是: 获取 “RoomBird[room_id]” 组内 所有成员的详细信息(指的是存在session里的)
//— 按照 每个成员中的 信息中的 sort 对这些成员信息 进行排序
//— 循环遍历 成员信息
//— — 对第一个 成员 离开 等待组,加入 在线游戏组。给他发送消息,你进入游戏了。给 大屏发消息 这个人 成功拉进来了
//— — 对第二个 及以后的成员 ,给他发消息 你前面还有 i 个人 在排队。
// 获取游戏状态
// 获取当前正在参与游戏中的 玩家数量
//判断 : 如果 正在参与游戏中的 玩家数量 没有超过最大用户数(maxUserCount) && 游戏正处于 进人状态
— 是 : 将当前 socketClientId 加入到 组 “ Online[room_id]” 中
redis数据操作 将 wxopenID , isDead=1 追加到 key = wxOpenIdOnline[room_id] 中
&& return true
// 获取 当前正在排队 的 用户的数量
//判断: 如果 当前正在排队的 数量 小于 最大等待游戏的 数量
— 是 : 将当前 socketCliendId 加入到 组 “RoomBird[room_id]” && return true
return false
调用 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
上面细节不重要,总结就是:addNewPlayer 由大屏端发起,在 小鸟和贪吃蛇的游戏中 每隔两秒就 拉人就来,一次拉一个人。
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'] =
组名,消息名的 命名规范!