文章目录
- 一、环境准备
- 1)依赖安装
- 2)源码下载和编译
- 二、生成数据信息
- 1)地图数据信息(客户端信息)
- 2)数据库信息
- 三、启动服务器
- 四、日志模块
- 五、数据库模块
- 六、场景模块
- 1)地图管理
- 2)AOI算法
- 1、静态数据讲解
- 2、动态数据:
- 3、动态数据结构NGrid解析
- 4、涉及用户登录的逻辑及grid状态转变
- 5、涉及玩家退出的逻辑及grid状态转变
- 3)地图数据驱动
- 1)网络驱动
- 2)地图定时更新做的事
- 4)动态数据管理
- (1)设计模式-访问者模式及代码表现形式
- 5)碰撞检测实现(BIH+AABB)
- 七、战斗模块
- 八、MMO常用模块(任务、副本、商店等)
一、环境准备
1)依赖安装
sudo apt-get update
sudo apt-get install git clang cmake gcc g++
libmysqlclient-dev libss-dev libbz2-dev libreadline-dev
libncurses-dev libboost-all-dev mysql-server-5.7 p7zip
sudo update-alternatives --install /usr/bin/cc cc /usr/bin/clang 100
sudo update-alternatives --install /usr/bin/c++ c++ /usr/bin/clang 100
//
sudo yum install -y clang
2)源码下载和编译
git clone -b 3.3.5 https//github.com//TrinityCore/TrinityCore.git
mkdir build cd build
cmake ../ -DCMAKE_INSTALL_PREFIX=/home/lighthouse/tinycore
-DCONF_DIR=/home/lighthouse/tinycore/bin
make -j2 (nproc看核心数来编译)
make install
二、生成数据信息
1)地图数据信息(客户端信息)
1)cd ~
2)mkdir res (根目录创建res文件夹)
3)把客户端目录的Data和Interface移动到res目录下
4)cd res
在res目录执行游戏目录bin/下面的mapextractor,
生成dbc和maps文件夹
5)mkdir vmaps
在res目录执行游戏目录bin/下面的vmap4extractor,
生成vmaps文件夹和Buildings目录
5)在res目录执行游戏目录bin/下面的vmap4assembler,
../lighthouse/bin/vmap4assembler Buildings vmaps
6) mkdir mmaps
在res目录执行游戏目录bin/下面的mmaps_generator
生成mmaps目录
2)数据库信息
- 注意
需要提前安装mysql
mysql -uroot -p password
1)在TrinityCore的sql/create目录的路径赋值,打开mysql
mysql> source ../Trinity/sql/create/create_mysql.sql
show database;
可以看到生成了auth \ character \ world
三、启动服务器
1)先进到项目的bin目录,复制authserver.conf
启动授权服务器
./authserver
2)同样步骤,复制worldserver.conf
并且改写DataDir,指定res目录
DataDir="../../res"
启动worldserver
启动授权服务器
./worldserver
四、日志模块
五、数据库模块
六、场景模块
1)地图管理
①哪些模块会用到地图模块?
1)运动模块:走、跳、飞行
2)副本
3)寻路
②地图模块要实现哪些功能?
1)AOI:管理地图地理信息、地图对象信息
2)功能:视野、数据同步、碰撞检测、寻路算法
③怎么驱动地图模块?
1)移动的网络数据驱动
2)定时更新(怪物的AI行为)
2)AOI算法
职责:
①静态数据:
《1》trinity由mapextractor生成.map文件(基础地图信息),数据包括
1) area data (区域物体信息)
2) height data (高度信息)
3) liquid data (水)
4) hole data (洞)
《2》由mmaps_generator生成可移动地图信息 .mmap,游戏中的地图移动数据(用来给navmesh寻路),也就是用recast和detour生成寻路信息
1) mmtile 索引对应具体地图的所有信息(x+y做名字前缀)
2) .mmap 索引
《3》vmapsextractor生成地图元素信息(可视场景信息:山脉、水体、建筑物等静态场景信息),用于未来做碰撞检测
,
*.m2和 *.wmo文件 静态物品包围盒信息
*.mdx 角色、物品和怪物包围盒信息
《4》vmap4assembler合并vmapsextractor和mmaps_generator生成的地图信息,vmtile信息
②动态数据
1、静态数据讲解
NGridType* i_grids[MAX_NUMBER_OF_GRIDS][MAX_NUMBER_OF_GRIDS];
//NGridType是动态数据
GridMap* GridMaps[MAX_NUMBER_OF_GRIDS][MAX_NUMBER_OF_GRIDS]; //原始数据,最新的已去除
//静态数据,64*64的格子,
//每个格子是8*8个cell,grid和cell都有自己的坐标系
//动物、角色、怪物都是在具体的cell当中
std::bitset<TOTAL_NUMBER_OF_CELLS_PER_MAP*TOTAL_NUMBER_OF_CELLS_PER_MAP> marked_cells;
2、动态数据:
玩家在哪个grid的cell,哪个grid才会被加载,否则不会被加载到内存当中 (只有玩家进入这个区域才会动态加载这个区域的资源)
3、动态数据结构NGrid解析
①数据结构
//NGridType是模板类
extern template class NGrid<MAX_NUMBER_OF_CELLS, Player, AllWorldObjectTypes, AllGridObjectTypes>;
typedef NGrid<MAX_NUMBER_OF_CELLS, Player, AllWorldObjectTypes, AllGridObjectTypes> NGridType;
AllWorldObjectTypes:(动态的obj)
player玩家、宠物、尸体、动态物品(远处target)
typedef TYPELIST_4(Player, Creature/*pets*/, Corpse/*resurrectable*/, DynamicObject/*farsight target*/)
AllWorldObjectTypes;
AllGridObjectTypes :(静态的obj)
游戏obj、除了宠物的obj、动态物体、尸体
typedef TYPELIST_7(GameObject, Creature/*except pets*/, DynamicObject, Corpse/*Bones*/, AreaTrigger, SceneObject, Conversation) AllGridObjectTypes;
②具体分析
NGrid有两个容器:i_container和i_objects
private:
uint32 i_gridId;
GridInfo i_GridInfo;
GridReference<NGrid<N, ACTIVE_OBJECT, WORLD_OBJECT_TYPES, GRID_OBJECT_TYPES> > i_Reference;
int32 i_x;
int32 i_y;
grid_state_t i_cellstate;
GridType i_cells[N][N];
bool i_GridObjectDataLoaded;
typedef Grid<Player, AllWorldObjectTypes, AllGridObjectTypes> GridType;
class Grid
private:
TypeMapContainer<GRID_OBJECT_TYPES> i_container;
TypeMapContainer<WORLD_OBJECT_TYPES> i_objects;
玩家进入某个grid的cell,地图加载数据激活推动消息推送
4、涉及用户登录的逻辑及grid状态转变
MapRefManager m_mapRefManager;//MapRefManager双向循环链表
1)根据地图ID、xyz轴、面向在地图上准备new obj
2)把玩家链接到这个地图对应的双向循环链表上
3)根据玩家坐标创建一个cell,加载玩家到对应地图的内存上,若这个cell只有一个玩家则加载cell全部地图信息
4)把玩家的包围盒和怪物、NPC、玩家用BSP tree包围盒做碰撞检测(访问者模式)
DynamicMapTree _dynamicTree;
void Balance() { _dynamicTree.balance(); }
5、涉及玩家退出的逻辑及grid状态转变
1)当定时器超时玩家没重连,那就把玩家从当前地图清除
⑤地图的状态
1)Idle
2)Active
3)InValid //无效状态
4)REMOVAL //准备从内存中卸载
- 地图状态转换说明:
①起伏创建是Invalid,
②加载具体cell到grid里面是Idle状态,
③当把玩家放进来的时候是Active状态,
④当玩家退出或掉线时候,这个cell是无人的时候,变为Idle状态
⑤当超时重连结束,玩家被从地图删除实例,则是Removal状态,下次服务器tick触发的时候把地图从内存卸载
(魔兽主线程等待子线程update地图事件,全部结束后才往下把所有map走delayUpdate,更新地图状态机,走从内存卸载删除逻辑)
typedef enum
{
GRID_STATE_INVALID = 0,
GRID_STATE_ACTIVE = 1,
GRID_STATE_IDLE = 2,
GRID_STATE_REMOVAL= 3,
MAX_GRID_STATE = 4
} grid_state_t;
3)地图数据驱动
玩家进入某个grid的cell,地图加载数据激活推动消息推送
1)网络驱动
- 网络结构说明
①创线程池A,线程主要做:
1)信号处理
2)远程连接请求
3)数据库心跳
4)生成core检查
5)异步日志
...
②主线程有主循环,按照一定频率update游戏世界逻辑,执行的逻辑比如
1)更新玩家session信息,根据玩家连接状态更改session状态
(如果不是线程安全,那就在主线程处理,后面的业务逻辑是多线程处理的)
2)主线程处理的逻辑包括:从数据库获取基础的玩家初始数据比如背包等
3)其他线程中的处理:在地图中攻击、寻路、走路
③线程池B,主要处理地图数据
读取配置中的线程数量,创建线程池,并给每条线程发更新地图的放到阻塞的任务队列,每条map也能处理来自客户端发送的数据包
void MapManager::Update(uint32 diff)
{
i_timer.Update(diff);
if (!i_timer.Passed())
return;
MapMapType::iterator iter = i_maps.begin();
while (iter != i_maps.end())
{
if (iter->second->CanUnload(diff))
{
if (DestroyMap(iter->second))
iter = i_maps.erase(iter);
else
++iter;
continue;
}
if (m_updater.activated())
m_updater.schedule_update(*iter->second, uint32(i_timer.GetCurrent()));
else
iter->second->Update(uint32(i_timer.GetCurrent()));
++iter;
}
if (m_updater.activated())
m_updater.wait();
for (iter = i_maps.begin(); iter != i_maps.end(); ++iter)
iter->second->DelayedUpdate(uint32(i_timer.GetCurrent()));
i_timer.SetCurrent(0);
}
④线程池C用来负载均衡处理客户端发包(Network)
2)地图定时更新做的事
①更新这个地图中的玩家,用访问者模式给玩家广播周围的obj(让自己在同cell里面被其他人看到),处理视野相关内容
②包围盒_dynamicTree二叉树检测物体碰撞(惰性更新,每次更新的时候进行平衡)
void Map::Update(uint32 t_diff)
{
_dynamicTree.update(t_diff); //包围盒检测
/// update worldsessions for existing players
for (m_mapRefIter = m_mapRefManager.begin(); m_mapRefIter != m_mapRefManager.end(); ++m_mapRefIter)
{
Player* player = m_mapRefIter->GetSource();
if (player && player->IsInWorld())
{
//player->Update(t_diff);
WorldSession* session = player->GetSession();
MapSessionFilter updater(session);
session->Update(t_diff, updater);
}
}
...
}
③处理人物的重生计时
/// process any due respawns
if (_respawnCheckTimer <= t_diff)
{
ProcessRespawns();
UpdateSpawnGroupConditions();
_respawnCheckTimer = sWorld->getIntConfig(CONFIG_RESPAWN_MINCHECKINTERVALMS);
}
else
_respawnCheckTimer -= t_diff;
④玩家战斗中技能释放、副本流程、人物、成就的更新
4)动态数据管理
(1)设计模式-访问者模式及代码表现形式
- 概念
①提供一个访问者类,可以改变元素类的执行算法
②传入不同的访问者,可以使用不同的执行算法 - 解决的问题
稳定的数据结构和容易改变的操作耦合问题 - 解决方法
在元素类型中提供对应不同访问者的接口 - 目的
将数据结构和数据操作进行分离 - 代码
T是算法,CONTAINER是数据结构
1)TypeContainer.h
有两种结构:双向链表和散列表,解决不同链表、不同散列表的差异
2)TypeContainerFunctions.h
解决双向链表和散列表之间的差异
3)TypeCOntainerVistor.h
解决对访问者的抽象,不同访问者实现不同的算法
template<class T, class CONTAINER>
inline void Map::Visit(Cell const& cell, TypeContainerVisitor<T, CONTAINER>& visitor)
{
const uint32 x = cell.GridX();
const uint32 y = cell.GridY();
const uint32 cell_x = cell.CellX();
const uint32 cell_y = cell.CellY();
if (!cell.NoCreate() || IsGridLoaded(GridCoord(x, y)))
{
EnsureGridLoaded(cell);
getNGrid(x, y)->VisitGrid(cell_x, cell_y, visitor);
}
}
通过下面这个函数将不同obj加入到不同的双向链表里面
1)不同双向链表:
TypeMapContainer<GRID_OBJECT_TYPES> i_container;
TypeMapContainer<WORLD_OBJECT_TYPES> i_objects;
2)访问者算法加入不同链表的算法
template<class OBJECT>
class GridReference : public Reference<GridRefManager<OBJECT>, OBJECT>
{
protected:
void targetObjectBuildLink() override
{
// called from link()
this->getTarget()->insertFirst(this);
this->getTarget()->incSize();
}
void targetObjectDestroyLink() override
{
// called from unlink()
if (this->isValid()) this->getTarget()->decSize();
}
void sourceObjectDestroyLink() override
{
// called from invalidate()
this->getTarget()->decSize();
}
public:
GridReference() : Reference<GridRefManager<OBJECT>, OBJECT>() { }
~GridReference() { this->unlink(); }
GridReference* next() { return (GridReference*)Reference<GridRefManager<OBJECT>, OBJECT>::next(); }
};
3)不同类型的访问者做不同处理,visit就是不同的算法
class TC_GAME_API ObjectGridLoader : public ObjectGridLoaderBase
{
friend class ObjectWorldLoader;
public:
ObjectGridLoader(NGridType& grid, Map* map, Cell const& cell)
: ObjectGridLoaderBase(grid, map, cell)
{ }
void Visit(GameObjectMapType &m);
void Visit(CreatureMapType &m);
void Visit(AreaTriggerMapType &m);
void Visit(CorpseMapType &) const { } //尸体obj
void Visit(DynamicObjectMapType&) const { } //动态物品,掉落装备等
void Visit(SceneObjectMapType&) const { } //场景对象
void Visit(ConversationMapType&) const { } //对话obj
void LoadN();
};
5)碰撞检测实现(BIH+AABB)
- 碰撞检测目标
对象模型的包围盒 - 数据来源
vmap4extractor提取的场景信息(建筑、山脉、水体等)、角色、怪物、装备等3d模型信息 - 逻辑流程
1)是否在视线范围内
2)碰撞检测,算命中位置(如不同楼层等高度信息)
3)获取地图信息 - 怎么实现?(一句话:射线和包围盒的碰撞)
①插入的时候会平衡一次
②然后就是定时200ms平衡一次 - 实现代码
class TC_COMMON_API DynamicMapTree
{
DynTreeImpl *impl;
public:
DynamicMapTree();
~DynamicMapTree();
bool isInLineOfSight(G3D::Vector3 const& startPos, G3D::Vector3 const& endPos, PhaseShift const& phaseShift) const;
bool getIntersectionTime(G3D::Ray const& ray, G3D::Vector3 const& endPos, PhaseShift const& phaseShift, float& maxDist) const;
bool getObjectHitPos(G3D::Vector3 const& startPos, G3D::Vector3 const& endPos, G3D::Vector3& resultHitPos, float modifyDist, PhaseShift const& phaseShift) const;
//看层高
float getHeight(float x, float y, float z, float maxSearchDist, PhaseShift const& phaseShift) const;
bool getAreaInfo(float x, float y, float& z, PhaseShift const& phaseShift, uint32& flags, int32& adtId, int32& rootId, int32& groupId) const;
void getAreaAndLiquidData(float x, float y, float z, PhaseShift const& phaseShift, uint8 reqLiquidType, VMAP::AreaAndLiquidData& data) const;
//添加、删除、看是否存在
void insert(GameObjectModel const&);
void remove(GameObjectModel const&);
bool contains(GameObjectModel const&) const;
//更新就是平衡
void balance();
void update(uint32 diff); //定时平衡,200ms平衡一次
};
- 核心算法和数据结构
数据结构是BIH树,核心算法是AABB(轴对称边界盒算法,射线和包围盒相交的算法),BIH作为二叉树是为了快速进行流程,避免费时计算
- 如何实现BIH树?
1、构建BIH树
2、光线追踪算法AABB(AABB并不是稳定的结构,可能随时会有新obj进来) - 碰撞计算逻辑
1、计算包围盒(vmap数据中进行加载)
2、在一个grid中所有对象根据中轴线构建二叉树,左边为左子树,右边为右子树,判断节点方块与光线是否相交,若相交则不断递归下去,直到满足递归停止条件(递归深度64)
3、注意:每个节点都是子树对象包围盒的并集
4、射线就是行走路径
- 优化
①初始时构建一次
②定时构建:
1、200ms构建一次
2、如果BIH树发生改变才会重新构建