今天来写一下游戏中,场景服务器的实现思路,游戏场景服务器在服务端,一直都是必不可少的一项。一般像稍微重度的游戏,手游都少不了一个场景服。当然,除了那些乱七八糟的棋牌,斗地主什么的不需要场景,直接就进房间了。
SenceServer在后端中一直都是一个大项,名不用一样,有些可能会叫一个什么GameServer的东西,可以暂时把ta当成是一个场景服务器来理解,等你具体的搞明白了,再具体的细分。为什么需要这样的一个服务器?理解到这里,也就差不多快搞明白这样的一套框架了。能做场景的服务器,大致都在中上等水平了。既然这帮老狗,吝啬着自己两毛钱的技术,不愿意说出来。我就简单帮ta们实现下),在MMORPG里,场景上所有的表现几乎都在这里实现,玩家的上线下线,切换场景,战斗各种消息广播都要在这里表现。场景上的唯一管理者,还要统一的去管理这些东西。不能出现严重的数据上的错误。这里花费的时间远比其它的多的多。
一个完整的场景服需要具备那些逻辑:我觉得应该有那么几个大的模块是必须要实现的。
1. AOI(Area Of Interest 视野发现,玩家感兴趣区域):
首先,要考虑到在场景上玩家需要一个相互发现的逻辑。当然,不是说我们只要实现一个同步就OK了。如果玩家堆在场景上越来越多,后期需要遍历的也越多,玩家就会不断卡顿,体验很差,造成性能上的损失和服务器宽带费用的浪费。所以就需要一个合适的AOI视野发现,即,只需要遍历“我”周围的玩家就行了,不用给全地图发一遍。这样一下就看省下不少带宽和内存上的浪费玩家也可以有个很好的体验。 常见的一些AOI九宫格视野发现,会根据手机的屏幕来划分这个九宫格,实现这个视野发现算法。 这个是最为常见的一种。还有很多灯塔,十字链表的这种实现。效果也很不错。具体的,又可以讲一大堆。各有各的优点,最后就看那个比较合适。IO的问题,还是要看场景上具体会塞多少人,人是否集中,地图大小等。
处理完这些,场景上的地图,还需要划分格子,格子大小又决定了AOI会是怎样的一个遍历,太大不准确,太小影响算力。用十字链表的话就不需要考虑这个了 。 后期玩家的位置,怪物,技能的释放都会根据地图上的格子 中的来做。需要放各种范围式的技能,都是根据格子来做的。 这里扯的有点多,写到这里,大概你就需要实现两大项了
一个具体的 AOI算法,一个地图格子管理的实现。
class Grid; //这里写你格子的实现
Class GridManager
{
public:
//AOI可以放在这里实现
}
大概有个500行左右的实现。不需要很多,足够简单明了就行。
2.一个适合的寻路算法(A*)
这个区分一些游戏, 有些是不需要寻路的。只做了一些简单的判断,只要玩家走距离的范围 怪物就会跑过去攻击玩家。常见的一些MMO类 有很多这种类似的操作。比如,吃鸡,这些。
那么这个寻路算法应该怎么写?也有很多。目前最为常见的A*, 也是至今在很多MMO游戏里被应用的。至于深度优先,广度优先这种, 就看是什么类型的游戏了。虽然都是寻路,但其中寻找的方法不同,还是要区别选择。
一个寻路算法的大致结构代码:
class AStar
{
struct NODE // node structure
{
long f, h;
int g, tmpg;
int x, y;
int NodeNum;
NODE* Parent;
NODE* Child[8]; // a node may have upto 8+(NULL) children.
NODE* NextNode; // for filing purposes
};
struct STACK // the stack structure
{
NODE* pNode;
STACK* pNextStack;
};
public:
AStar(void);
~AStar();
bool InitAstarMap(short* pMap, int w, int h);
bool NewPath(int sx, int sy, int dx, int dy);
bool IsReached(void);
bool PathNextNode(void);
int NodeGetX();
int NodeGetY();
int GetTileNum(int x, int y);
int IsTileAviable(int x, int y); 为真表示不能通过
private:
void FreeNodes(void);
void FindPath(int sx, int sy, int dx, int dy);
NODE* GetBestNode(void);
void GenerateSuccessors(NODE* BestNode, int dx, int dy);
void GenerateSucc(NODE* BestNode, int x, int y, int dx, int dy);
NODE* CheckOPEN(int tilenum);
NODE* CheckCLOSED(int tilenum);
void Insert(NODE* Successor);
void PropagateDown(NODE* Old);
void Push(NODE* Node); // stack functions
NODE* Pop(void);
NODE* m_pOpenList; // the node list pointers
NODE* m_pClosedList;
NODE* m_pCurPath; // pointer to the best path
STACK* m_pStack;
int m_nRowCnt; // tilemap data members, need to be initialisize
int m_nColCnt; // with current map's width and height
int m_nTotalTiles; // to allocate memory for the
short* m_pTileMap; // pointer to the A* own tilemap data array
private:
};
这里有一篇文章也是讲寻路的,不过原文章并不是ta的。只不过拿来翻译了一下,用来骗取流量的。痛斥一下,这帮不要碧莲的老狗!!
最快速的寻路算法 Jump Point Search - 知乎
3. 一个强大的消息驱动 来推送场景中的各种消息事件 (Broadcast )
提到消息,又不得说下这个Portobuffer的消息传输格式。毕竟都在用,也非常的成熟,访问效率和内存使用上远由于其它那些古老的 xml,Json格式。用Protobuffer来定义自己的消息格式。 推送我们场景上的那些消息。比如, 我们各种上线,下线,离开,进入场景,副本里战斗,伤害计算,玩家移动等,都是需要推送。所以需要一个强大的Broadcast 来推这些东西。告知客户端,告知其ta客户端。
4. 场景,场景中是否有基类, 是否可以拓展,多开等等。
场景服务器中,算是压力最大的一个服务器。所有注册过的玩家,最终都会集中在这里。场景的可拓展,就成了关键一步。Base场景就成为了基场景,其它的场景继承此场景,就可以拓展出多个场景副本来,供玩家在主城与副本直接来回穿梭; 多开的意思,就是在场景服务器人数装满的情况下,再开一个场景服务器出来,继续装。 能一直开下去吗?能,开到服务器进程拉满,塞满负荷。这个时候你就该考虑是不是要多加一台机器出来了。 一般,像我们这种,国内的这帮人的水平,一个正常的MMO服务器 大概装3000人是没任何问题的。那么,首先做的就是把场景服务器实现出来。再考虑其它的。我把场景基类的代码结构 贴出来。
class Sence
{
//主场景 这里大概要写个一千多行 毕竟这里是主要场景。
//各种行为上的初始化
// 场景的创建,玩家,怪物 等等。
//各种状态的 初始与回收等等。
}
class SenceManager
{
//管理场景 场景的管理 初始化会在这里实现。
}
class Base_Sence_Logic
{
//场景逻辑上的一个基类,用于拓展场景中
}
class Sence_Normorl:public Base_Sence_Logic
{
//其它场景副本
}
class Sence_Team:public Base_Sence_Logic
{
// //其它场景副本
}
class Sence_Object
{
//大多数框架中都会有一个 SenceObject 用来当玩家或者怪物 多数的实现会继承此类
}
我们把这些大的框架模块解决完之后,剩下的就是些添砖加瓦的事情了。比如一些场景中的战斗,技能,buff ,伤害计算,碰撞检测什么的,应该都不是很难了就。
目测代码量,大致在1w行左右。
写到这里,其实不过就是一堆代码么。游戏的具体设计,还是要根据策划,游戏类型,玩法来决定。场景服并不是一成不变,重要的是基础要有。