大屏与手机互动cocos 2d游戏的后端设计与实现(基于GateWayWorker)

背景:这个项目,我只参与项目的一部分业务代码开放(摇一摇游戏业务),只是简单来使用封装好的一些类和方法,核心实现并不是我写的,并且核心代码设计思路并没有文档,当然代码中有些注释,他人也并没有太多时间给我好好讲讲他的设计思路。

目的:分析他人的代码,完全掌握他人的设计思路,抓住重点,提取好的思路,供之后类似的项目开放 做参考!

解读他人代码/分析代码 的方法

多维度分析代码:

一: 看代码组织结构,类的继承关系

二:程序的执行流程

三: 数据设计(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'] = 

 

 

优化的方向:

组名,消息名的 命名规范!

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值