网站开发上海,wordpress+编辑器字号,公司介绍网站平台搭建设计论文,重庆建设工程信息网信息网文章目录 0 代码仓库1 需求2 AOI设计2.1 AOI算法简介2.2 AOI数据结构及实现2.2.1 玩家2.2.2 网格对象2.2.3 游戏世界矩形2.2.4 获取周围玩家的实现2.2.5 代码测试 2.3 GameRole结合AOI创建玩家2.3.1 创建游戏世界全局对象-GameRole继承AOIWorld的Player2.3.2 把玩家到游戏世界的… 文章目录 0 代码仓库1 需求2 AOI设计2.1 AOI算法简介2.2 AOI数据结构及实现2.2.1 玩家2.2.2 网格对象2.2.3 游戏世界矩形2.2.4 获取周围玩家的实现2.2.5 代码测试 2.3 GameRole结合AOI创建玩家2.3.1 创建游戏世界全局对象-GameRole继承AOIWorld的Player2.3.2 把玩家到游戏世界的加入与删除2.3.3 玩家上线时的处理新客户端连接后向自己发送ID和名称2.3.4 新客户端连接后向其发送**周围**玩家的位置2.3.5 新客户端连接后向**周围**玩家发送其位置2.3.6 游戏测试2.3.7 世界聊天2.3.7.1 创建广播2.3.7.2 发送给所有人 2.4 玩家移动处理2.4.1 视野出现和消失2.4.2 跨网格处理2.4.3 广播新位置给周围玩家 2.5 随机出生2.7 退出程序2.7.1 定时器设计2.7.2 最后一个玩家启动定时器2.7.3 初始化的完善2.7.4 主函数的完善 2.8 随机姓名的设计与实现2.8.1 姓和常用名的定义2.8.2 取名字2.8.3 还名字2.8.4 读取文件组建姓名的线性表 3 架构回顾 游戏相关的核心消息处理逻辑都是要在该类中实现的。
0 代码仓库
https://github.com/Chufeng-Jiang/TCP_Concurrent_Server_Framework_Zinx-Game_Server_Development_Project
1 需求
新客户端连接后向其发送ID和名称新客户端连接后向其发送周围玩家的位置新客户端连接后向周围玩家发送其位置收到客户端的移动信息后向周围玩家发送其新位置收到客户端的移动信息后向其发送周围新玩家位置收到客户端的聊天信息后向所有玩家发送聊天内容客户端断开时向周围玩家发送其断开的消息
关键字周围。
以上所列出的需求基本都是这样的套路在XXX的时候发送XXX给XXX。
发送时机消息内容发送对象怎样表示周围玩家
2 AOI设计
2.1 AOI算法简介
定义 获取感兴趣的区域Area Of Interest的算法。
解决的问题 形成周围的概念。在多人游戏中各个游戏客户端之间需要通过服务器向彼此更新自身状态。但对于当玩家来说我们不需要获取“太远”的玩家的信息所以在服务器端我们通过AOI算法可以获取到某个客户端“周围”的玩家进而只在该小范围内同步信息。
网格法AOI
参考游戏世界的坐标创建一个边界相同的矩形。选取适当的颗粒度将矩形分割成几×几的网格。每个客户端都要按照实际坐标添加到某个格子里。客户端所在格子的周围八个格子内的玩家就是周围玩家。
举例 世界坐标是X[20,200]Y[50,230]划分成6×6的网格为
已知玩家坐标xy该玩家在几号网格 网格编号(x-x轴起始坐标)/x轴网格宽度 (y-y轴起始坐标)/y轴宽度*x轴网格数量x轴网格宽度(x轴结束坐标-x轴起始坐标)/x轴网格数量y轴的计算方式相同 已知玩家在n号网格周围的格子(包括自己)有哪些 2.2 AOI数据结构及实现
目的获取周围玩家
模型将游戏世界的坐标分割成网格玩家属于某个网格
周围玩家所属网格周围8个相邻网格内的玩家
游戏世界矩形包含固定数量网格对象的容器
网格对象包含若干玩家的容器
玩家拥有横纵坐标的对象2.2.1 玩家
class Player {
public:virtual int GetX() 0;virtual int GetY() 0;
};2.2.2 网格对象
添加玩家的时候计算出玩家的坐标然后将坐标就添加到网格对象中。网格对象是以网格为单位的矩形里面装的是该网格范围内的所有玩家(坐标)。
class Grid {
public:std::listPlayer * m_players;
};添加玩家到网格对象
bool AOIWorld::AddPlayer(Player * _player)
{/*计算所属网格号*///网格编号(x-x轴起始坐标)/x轴网格宽度 (y-y轴起始坐标)/y轴宽度*x轴网格数量int grid_id (_player-GetX() - x_begin) / x_width (_player-GetY()-y_begin) / y_width * x_count;/*添加到该网格中*/m_grids[grid_id].m_players.push_back(_player);return true;
}删除玩家
void AOIWorld::DelPlayer(Player * _player)
{int grid_id (_player-GetX() - x_begin) / x_width (_player-GetY() - y_begin) / y_width * x_count;m_grids[grid_id].m_players.remove(_player);
}2.2.3 游戏世界矩形
class AOIWorld
{int x_begin 0;int x_end 0;int y_begin 0;int y_end 0;int x_count 0;int y_count 0;int x_width 0;int y_width 0;
public:std::vectorGrid m_grids;/*通过构造函数指定矩形的大小和分割粒度*/AOIWorld(int _x_begin, int _x_end, int _y_begin, int _y_end, int _x_count, int _y_count);virtual ~AOIWorld();/*获取周围玩家*/std::listPlayer * GetSrdPlayers(Player *_player);/*添加玩家到AOI网格*/bool AddPlayer(Player *_player);/*摘除玩家*/void DelPlayer(Player *_player);
};初始化世界并创建格子对象
AOIWorld::AOIWorld(int _x_begin, int _x_end, int _y_begin, int _y_end, int _x_count, int _y_count):x_begin(_x_begin),x_end(_x_end),y_begin(_y_begin),y_end(_y_end),x_count(_x_count),y_count(_y_count)
{//x轴网格宽度(x轴结束坐标-x轴起始坐标)/x轴网格数量y轴的计算方式相同x_width (x_end - x_begin) / x_count;y_width (y_end - y_begin) / y_count;/*创建格子们*/for (int i 0; i x_count * y_count; i){Grid tmp;m_grids.push_back(tmp);}
}2.2.4 获取周围玩家的实现 std::listPlayer* AOIWorld::GetSrdPlayers(Player * _player)
{listPlayer * ret;/*计算所属编号*/int grid_id (_player-GetX() - x_begin) / x_width (_player-GetY() - y_begin) / y_width * x_count;/*判断具体情况取出邻居网格的玩家们*///计算当前网格横着数和纵着数的个数, 当前网格在世界的坐标int x_index grid_id % x_count; //横着的坐标colint y_index grid_id / x_count; //纵向坐标rowif (x_index 0 y_index 0) //有左上角的格子{listPlayer * cur_list m_grids[grid_id - 1 - x_count].m_players;ret.insert(ret.begin(), cur_list.begin(),cur_list.end());}if (y_index 0) //正上方的格子{listPlayer * cur_list m_grids[grid_id - x_count].m_players;ret.insert(ret.begin(), cur_list.begin(), cur_list.end());}if (x_index x_count - 1 y_index 0) //右上角的格子{listPlayer * cur_list m_grids[grid_id - x_count 1].m_players;ret.insert(ret.begin(), cur_list.begin(), cur_list.end());}if (x_index 0) //左方的格子{listPlayer * cur_list m_grids[grid_id - 1].m_players;ret.insert(ret.begin(), cur_list.begin(), cur_list.end());}//自己所在位置listPlayer * cur_list m_grids[grid_id].m_players;ret.insert(ret.begin(), cur_list.begin(), cur_list.end());if (x_index x_count - 1) //右方的格子{listPlayer * cur_list m_grids[grid_id 1 ].m_players;ret.insert(ret.begin(), cur_list.begin(), cur_list.end());}if (x_index 0 y_index y_count - 1) //左下方的格子{listPlayer * cur_list m_grids[grid_id - 1 x_count].m_players;ret.insert(ret.begin(), cur_list.begin(), cur_list.end());}if (y_index y_count - 1) //正下方的格子{listPlayer * cur_list m_grids[grid_id x_count].m_players;ret.insert(ret.begin(), cur_list.begin(), cur_list.end());}if (x_index x_count - 1 y_index y_count - 1) //右下方的格子{listPlayer * cur_list m_grids[grid_id 1x_count].m_players;ret.insert(ret.begin(), cur_list.begin(), cur_list.end());}return ret;
}2.2.5 代码测试
#include GameChannel.h
#include GameMsg.h
#include msg.pb.h
#include AOIWorld.hclass myPlayer :public Player {
public:myPlayer(int _x, int _y, std::string _name) :x(_x), y(_y), name(_name) {}int x;int y;std::string name;// 通过 Player 继承virtual int GetX() override{return x;}virtual int GetY() override{return y;}
};int main()
{AOIWorld w(20, 200, 50, 230, 6, 6);myPlayer p1(60, 107, 1);myPlayer p2(91, 118, 2);myPlayer p3(147, 133, 3);w.AddPlayer(p1);w.AddPlayer(p2);w.AddPlayer(p3);auto srd_list w.GetSrdPlayers(p1);for (auto single : srd_list){std::cout dynamic_castmyPlayer*(single)-name std::endl;}ZinxKernel::ZinxKernelInit();/*添加监听通道*/ZinxKernel::Zinx_Add_Channel(*(new ZinxTCPListen(8899, new GameConnFact())));ZinxKernel::Zinx_Run();ZinxKernel::ZinxKernelFini();
}2.3 GameRole结合AOI创建玩家
2.3.1 创建游戏世界全局对象-GameRole继承AOIWorld的Player
proto文件中对应
message Position{float X1;float Y2; float Z3; float V4;int32 BloodValue5;
}#pragma once
#include zinx.h
#include AOIWorld.h
#include GameMsg.hclass GameProtocol;
class GameRole :public Irole,public Player
{float x 0;float y 0;//高float z 0;float v 0;int iPid 0;std::string szName;GameMsg *CreateIDNameLogin();GameMsg *CreataSrdPlayers();GameMsg *CreateSelfPostion();GameMsg *CreateIDNameLogoff();
public:// 通过 Player 继承virtual int GetX() override;virtual int GetY() override;
};
注意在人物角色中y是人物的高度不是二维平面的地点坐标y由前端那边设计。
/*创建游戏世界全局对象*/
static AOIWorld world(0, 400, 0, 400, 20, 20);int GameRole::GetX()
{return (int)x;
}int GameRole::GetY()
{return (int)z;
}
2.3.2 把玩家到游戏世界的加入与删除
连接到来玩家初始化时
属性pid赋值为socket值
属性name写成tom
初始坐标100,100
向自己发内容是ID和姓名的1号消息
向自己发内容是若干周围玩家信息的202号消息
向周围玩家发送内容是自己位置的200号消息bool GameRole::Init()
{/*添加自己到游戏世界*/bool bRet false;/*设置玩家ID为当前连接的fd*/iPid m_pProto-m_channel-GetFd(); //获取文件连接描述符这个是唯一的bRet world.AddPlayer(this);if (true bRet){/*向自己发送ID和名称*/auto pmsg CreateIDNameLogin();ZinxKernel::Zinx_SendOut(*pmsg, *m_pProto);/*向自己发送周围玩家的位置*/pmsg CreataSrdPlayers();ZinxKernel::Zinx_SendOut(*pmsg, *m_pProto);/*向周围玩家发送自己的位置*/auto srd_list world.GetSrdPlayers(this);for (auto single : srd_list){pmsg CreateSelfPostion();auto pRole dynamic_castGameRole *(single);// 注意第二个参数ZinxKernel::Zinx_SendOut(*pmsg, *(pRole-m_pProto));}}return bRet;
}void GameRole::Fini()
{/*向周围玩家发送下线消息*/auto srd_list world.GetSrdPlayers(this);for (auto single : srd_list){auto pMsg CreateIDNameLogoff();auto pRole dynamic_castGameRole *(single);ZinxKernel::Zinx_SendOut(*pMsg, *(pRole-m_pProto));}world.DelPlayer(this);
}2.3.3 玩家上线时的处理新客户端连接后向自己发送ID和名称
GameMsg * GameRole::CreateIDNameLogin()
{pb::SyncPid *pmsg new pb::SyncPid();pmsg-set_pid(iPid);pmsg-set_username(szName);GameMsg *pRet new GameMsg(GameMsg::MSG_TYPE_LOGIN_ID_NAME, pmsg);return pRet;
}2.3.4 新客户端连接后向其发送周围玩家的位置
设置protobuf类型消息的repeated类型 add_XXXX函数调用后会向当前消息添加一个数组成员返回数组成员的指针
GameMsg * GameRole::CreataSrdPlayers()
{pb::SyncPlayers *pMsg new pb::SyncPlayers();auto srd_list world.GetSrdPlayers(this);//周围玩家有多个for (auto single : srd_list){auto pPlayer pMsg-add_ps();auto pRole dynamic_castGameRole *(single);//设置到遍历到的玩家的信息pPlayer-set_pid(pRole-iPid); pPlayer-set_username(pRole-szName);//把子消息挂到父消息里面并返回子消息的指针auto pPostion pPlayer-mutable_p();pPostion-set_x(pRole-x);pPostion-set_y(pRole-y);pPostion-set_z(pRole-z);pPostion-set_v(pRole-v);}GameMsg *pret new GameMsg(GameMsg::MSG_TYPE_SRD_POSTION, pMsg);return pret;
}2.3.5 新客户端连接后向周围玩家发送其位置
message BroadCast{int32 Pid1;int32 Tp2;/*根据Tp不同Broadcast消息会包含聊天内容(Content)或初始位置(P)或新位置P*/oneof Data{string Content3;Position P4;/*ActionData暂时预留*/int32 ActionData5;}string Username6;
}GameMsg * GameRole::CreateSelfPostion()
{pb::BroadCast *pMsg new pb::BroadCast();pMsg-set_pid(iPid);pMsg-set_username(szName);pMsg-set_tp(2); //客户端决定的要设置成2auto pPosition pMsg-mutable_p();pPosition-set_x(x);pPosition-set_y(y);pPosition-set_z(z);pPosition-set_v(v);GameMsg *pret new GameMsg(GameMsg::MSG_TYPE_BROADCAST, pMsg);return pret;
}2.3.6 游戏测试
在程序安装文件夹下进入控制台运行客户端
client.exe 192.168.111.135 8899鼠标右键控制V 2.3.7 世界聊天 2.3.7.1 创建广播
class GameRole :public Irole,public Player
{void ProcTalkMsg(std::string _content);void ProcMoveMsg(float _x, float _y, float _z, float _v);void ViewAppear(GameRole *_pRole);void ViewLost(GameRole *_pRole);GameMsg *CreateTalkBroadCast(std::string _content);
};2.3.7.2 发送给所有人
void GameRole::ProcTalkMsg(std::string _content)
{//发给所有人auto role_list ZinxKernel::Zinx_GetAllRole();for (auto pRole : role_list){auto pGameRole dynamic_castGameRole *(pRole);auto pmsg CreateTalkBroadCast(_content);ZinxKernel::Zinx_SendOut(*pmsg, *(pGameRole-m_pProto));}
}MSG_TYPE_BROADCAST 200号消息
GameMsg * GameRole::CreateTalkBroadCast(std::string _content)
{pb::BroadCast *pmsg new pb::BroadCast();pmsg-set_pid(iPid);pmsg-set_username(szName);pmsg-set_tp(1);pmsg-set_content(_content);GameMsg *pRet new GameMsg(GameMsg::MSG_TYPE_BROADCAST, pmsg);return pRet;
}2.4 玩家移动处理 广播新位置给周围玩家若跨网格视野切换获取移动前周围玩家S1获取移动后的周围 玩家S2 新邻居互相能看见{x|x 属于S2 x 不属于S1}—发送200号消息旧邻居互相看不见{x|x属于S1 x不属于S2}—201号消息 UserData * GameRole::ProcMsg(UserData _poUserData)
{GET_REF2DATA(MultiMsg, input, _poUserData);for (auto single : input.m_Msgs){/*测试打印消息内容*/cout type is single-enMsgType endl;cout single-pMsg-Utf8DebugString() endl;auto NewPos dynamic_castpb::Position *(single-pMsg);switch (single-enMsgType){case GameMsg::MSG_TYPE_CHAT_CONTENT:ProcTalkMsg(dynamic_castpb::Talk *(single-pMsg)-content());break;case GameMsg::MSG_TYPE_NEW_POSTION:ProcMoveMsg(NewPos-x(), NewPos-y(),NewPos-z(),NewPos-v());break;default:break;}}return nullptr;
}
2.4.1 视野出现和消失
void GameRole::ViewAppear(GameRole * _pRole)
{/*向自己发参数的200消息*/auto pmsg _pRole-CreateSelfPostion();ZinxKernel::Zinx_SendOut(*pmsg, *m_pProto);/*向参数玩家发自己的200消息*/pmsg CreateSelfPostion();ZinxKernel::Zinx_SendOut(*pmsg, *(_pRole-m_pProto));
}void GameRole::ViewLost(GameRole * _pRole)
{/*向自己发送参数玩家的201消息*/auto pmsg _pRole-CreateIDNameLogoff();ZinxKernel::Zinx_SendOut(*pmsg, *m_pProto);/*向参数玩家发自己的201消息*/pmsg CreateIDNameLogoff();ZinxKernel::Zinx_SendOut(*pmsg, *(_pRole-m_pProto));
}2.4.2 跨网格处理
获取原来的邻居s1。摘出旧格子,更新坐标,添加新格子获取新邻居s2。遍历s2若元素不属于s1, 视野出现。遍历s1若元素不属于s2视野消失。
void GameRole::ProcMoveMsg(float _x, float _y, float _z, float _v)
{/*1.跨网格处理*//*获取原来的邻居s1*/auto s1 world.GetSrdPlayers(this);/*摘出旧格子*/world.DelPlayer(this);/*更新坐标,添加新格子获取新邻居s2*/x _x;y _y;z _z;v _v;world.AddPlayer(this);auto s2 world.GetSrdPlayers(this);/*遍历s2若元素不属于s1, 视野出现*/for (auto single_player : s2){if (s1.end() find(s1.begin(), s1.end(), single_player)){//视野出现ViewAppear(dynamic_castGameRole *(single_player));}}/*遍历s1若元素不属于s2视野消失*/for (auto single_player : s1){if (s2.end() find(s2.begin(), s2.end(), single_player)){//视野消失ViewLost(dynamic_castGameRole *(single_player));}}
2.4.3 广播新位置给周围玩家 /*2.广播新位置给周围玩家*/ //遍历周围玩家发送/*向周围玩家发送自己的位置*/auto srd_list world.GetSrdPlayers(this);for (auto single : srd_list){//组成待发送的报文pb::BroadCast *pMsg new pb::BroadCast();auto pPos pMsg-mutable_p();pPos-set_x(_x);pPos-set_y(_y);pPos-set_z(_z);pPos-set_v(_v);pMsg-set_pid(iPid);pMsg-set_tp(4);pMsg-set_username(szName);auto pRole dynamic_castGameRole *(single);//封装成消息发出去ZinxKernel::Zinx_SendOut(*(new GameMsg(GameMsg::MSG_TYPE_BROADCAST, pMsg)), *(pRole-m_pProto));}
}2.5 随机出生
设计 GameRole对象创建时随机生成合理范围内的坐标。
生成随机数的方法std::default_random_engine
详细资料http://www.cplusplus.com/reference/random/default_random_engine/
构造函数参数用于指定种子一般使用当前时间()操作符返回随机数无符号整形
static default_random_engine random_engine(time(NULL));GameRole::GameRole()
{szName random_name.GetName();x 100 random_engine() % 50;z 100 random_engine() % 50;
}2.7 退出程序
玩家全部退出后20s后服务器退出 创建定时任务20秒周期超时处理–》退出框架 添加时机玩家fini的时候若总玩家 1 摘除时机玩家init的时候若总玩家 0
2.7.1 定时器设计
class ExitTimer :public TimerOutProc {// 通过 TimerOutProc 继承virtual void Proc() override{ZinxKernel::Zinx_Exit();}virtual int GetTimeSec() override{return 20;}
};
static ExitTimer g_exit_timer;
2.7.2 最后一个玩家启动定时器
void GameRole::Fini()
{/*向周围玩家发送下线消息*/auto srd_list world.GetSrdPlayers(this);for (auto single : srd_list){auto pMsg CreateIDNameLogoff();auto pRole dynamic_castGameRole *(single);ZinxKernel::Zinx_SendOut(*pMsg, *(pRole-m_pProto));}world.DelPlayer(this);/*判断是否是最后一个玩家---起定时器*/if (ZinxKernel::Zinx_GetAllRole().size() 1){//起退出定时器TimerOutMng::GetInstance().AddTask(g_exit_timer);}
}2.7.3 初始化的完善
bool GameRole::Init()
{if (ZinxKernel::Zinx_GetAllRole().size() 0){TimerOutMng::GetInstance().DelTask(g_exit_timer);}
......
}2.7.4 主函数的完善
int main()
{ZinxKernel::ZinxKernelInit();/*添加监听通道*/ZinxKernel::Zinx_Add_Channel(*(new ZinxTCPListen(8899, new GameConnFact())));// 添加定时器ZinxKernel::Zinx_Add_Channel(*(new ZinxTimerChannel()));ZinxKernel::Zinx_Run();ZinxKernel::ZinxKernelFini();
}2.8 随机姓名的设计与实现
设计 在文件中存储一定量的常用姓和名GameRole创建时随机组合姓名
线性表存姓和名组成的线性表
取名字随机取姓随机取名
还名字尾部追加姓或名
读姓文件的同时读名文件边追加节点设计数据结构存储随机姓名池进程启动时构造生成随机名称取第随机个姓取第随机个名进程启动时读取文件构建姓名池玩家类构造时从姓名池获取姓名玩家类析构时释放姓名回姓名池
2.8.1 姓和常用名的定义
//姓和 名组成的线性表
class FirstName {
public:std::string m_first;//使用vectorstd::vectorstd::string m_last_list;
};class RandomName
{std::vectorFirstName * m_pool;
public:RandomName();std::string GetName();void Release(std::string _name); //还名字void LoadFile(); //通过文件来构造virtual ~RandomName();
};2.8.2 取名字
static default_random_engine rand_engine(time(NULL));std::string RandomName::GetName()
{//取姓auto num rand_engine() % m_pool.size();auto first m_pool[num]-m_first;//取名auto last m_pool[num]-m_last_list[rand_engine() % m_pool[num]-m_last_list.size()];//特殊情况若本姓的所有名都取完了把姓删掉if (m_pool[num]-m_last_list.size() 0){delete m_pool[num];m_pool.erase(m_pool.begin() num);}return first last;
}2.8.3 还名字
void RandomName::Release(std::string _name)
{//分割名字得到姓和名auto space_pos _name.find( , 0);auto first _name.substr(0, space_pos);auto last _name.substr(space_pos 1, _name.size() - space_pos - 1);bool found false;for (auto first_name : m_pool){if (first_name-m_first first) // 先找姓{found true;first_name-m_last_list.push_back(last); // 把名字追加到后面break;}}if (false found) //如果没找到姓也就是这个姓的名字用完了当前的名字是第一个归还的{auto first_name new FirstName(); //创建一个新的姓first_name-m_last_list.push_back(last); // 添加名字m_pool.push_back(first_name);}
}2.8.4 读取文件组建姓名的线性表
void RandomName::LoadFile()
{ifstream first(random_first.txt);ifstream last(random_last.txt);//读取所有名字组成一个线性表string last_name;vectorstring tmp;while (getline(last, last_name)){tmp.push_back(last_name);}//读取所有姓创建姓名池节点拷贝名字组成的线性表string first_name;while (getline(first, first_name)){auto first_name_list new FirstName();first_name_list-m_first first_name;first_name_list-m_last_list tmp;m_pool.push_back(first_name_list);}
}3 架构回顾