[C++] 联网单人副本闯关游戏--服务器部分

[C++] 联网单人副本闯关游戏–服务器部分

基本游戏逻辑:http://t.csdn.cn/nZ6xa

在基本游戏逻辑的基础上增添了联网和副本选择功能

实现了玩家的登录注册、场景信息下发、场景信息同步和校验、玩家断线重连、副本得分结算与排行榜逻辑

使用注册的形式将编译时硬编码转为运行时注册优化CMD分发,使用运行时类型判断简化同步信息处理逻辑

业务流程

在这里插入图片描述

业务逻辑

基础业务逻辑

玩家登录后,副本探索的基础的业务逻辑包括:

  • 玩家选择副本,发送场景加载请求,服务器回复场景信息
  • 玩家开始游戏时,发送开始游戏请求,服务器开始计时
  • 游戏过程中,根据同步策略发送同步请求,服务器进行数据同步和模糊校验
  • 玩家到达终点或倒计时结束后,发送结算请求,服务器校验玩家用时并根据用时和杀敌数计算分数返回

实现时,每个客户端回对应一个场景数据结构,存储客户端的PlayerID,场景ID,开始时间,击杀数,场景物体集合等内容

struct SceneData {
	string playerID = "";
	string playerName = "";
	int SceneID = -1;
	bool isStart = false;
	time_t startTime = 0;
	time_t disconnectTime = 0;
	int killCount = 0;
	map<int, SceneObject*> objMap;
};

在这里插入图片描述

同步逻辑

同步信息有四种,服务器对这四种同步信息进行校验和处理并返回结果,当校验失败时,客户端将根据返回值将场景拉回到正确状态

位置同步

需要位置同步的物体包括角色、幽灵,两者在移动时发送位置同步请求

在这里插入图片描述

幽灵信息同步

由于幽灵具有巡逻、追逐主角、死亡三种状态,因此在幽灵状态改变时需要同步幽灵状态以便后续断线重连需要

当幽灵状态改变不合法(如死亡状态下改变巡逻点),服务器返回当前维护的状态让客户端设置幽灵到正确状态

在这里插入图片描述

玩家操作同步

当玩家进行操作时(与开宝箱、开关灯、扔捡枪)会发送玩家操作同步请求,服务器首先会校验玩家操作是否合法(如与被互动物体距离是否过远),校验成功后根据玩家操作改变objMap被操作对象的状态,客户端只有在收到校验成功回复后才完成操作
在这里插入图片描述

伤害同步

最后一种同步是伤害同步,包括子弹对幽灵造成伤害、怪物对玩家造成伤害等,根据攻击力扣除被伤害物体血量,若血量归零还会进行相应逻辑处理

在这里插入图片描述

断线重连逻辑

当游戏进行过程中玩家断线,服务器会将客户端对应的SceneData以PlayerID为键加入到一个Map中,当玩家登录时检查玩家ID是否在Map中,若在Map中则在登录返回包中设置结果码,让客户端决定是否需要重连

若客户端需要重连则发送重连请求,服务器返回关卡Json文件和需要更新的物体信息,客户端还原后继续进行游戏

此外,服务器还会定时扫描断线Map,将超时的数据释放掉

在这里插入图片描述

服务器实现

服务器内以SceneObject类保存校验、同步和断线重连所需的场景信息

在这里插入图片描述

SceneObject类

SceneObject

SceneObject是所有场景物体的基类,拥有位置信息和_sceneObjectID用于与客户端通信时定位场景物体

class SceneObject
{
public:
	SceneObject(int ID, Vector3 pos) :_sceneObjectID(ID), _pos(pos) {}
	Vector3 getPos() { return _pos; }
	// 获取重连所需信息
	virtual Message* GetReconnectInfo() { return nullptr; }
	// 虚拟化的拷贝构造函数
	virtual SceneObject* Clone() = 0;
protected:
	int _sceneObjectID;	// 场景物体的标识,用于于客户端通信
	Vector3 _pos;	// 位置信息
};
Gargoyle,SlowDown,Lurker,Light

这四种场景物体不需要处理同步请求且断线重连时不需要更新信息,因此直接继承SceneObject且没有重写GetReconnectInfo方法

class Gargoyle : public SceneObject
{
public:
	Gargoyle(int ID, Vector3 pos, int ATK) :SceneObject(ID,pos), _ATK(ATK) {}
	virtual SceneObject* Clone() override { return new Gargoyle(*this); }
private:
	int _ATK;
};
Chest

宝箱需要在断线重连时更新信息但不需要处理同步请求,因此直接继承SceneObject并且重写GetReconnectInfo方法

class Chest : public SceneObject
{
public:
	Chest(int ID, Vector3 pos) :SceneObject(ID, pos) {}
	virtual ChestInfo* GetReconnectInfo();
	virtual SceneObject* Clone() override { return new Chest(*this); }

	bool isOpened = false;
};

Movable

需要进行位置同步的场景物体继承Movable,定义了PositionSyncHanle方法用于处理位置同步请求

class Movable : public SceneObject
{
public:
	Movable(int ID, Vector3 pos) :SceneObject(ID, pos) {}
	virtual PositionSyncRsp PositionSyncHandle(const PositionSyncReq* req, double tolerance = 5);
protected:
	bool DistanceCheck(Vector3 other, double tolerance = 5);
	bool _needCheck = true;
};
Weapon

Weapon需要同步位置且没有血量信息因此继承Movable类,并且断线重连时更新信息因此重写GetReconnectInfo方法

class Weapon : public Movable
{
public:
	Weapon(int ID, Vector3 pos = Vector3()) :Movable(ID, pos) { _needCheck = false; }
	virtual WeaponInfo* GetReconnectInfo();
	virtual SceneObject* Clone() override { return new Weapon(*this); }
private:
};
Character

除进行位置同步还要进行伤害同步(拥有血量)的物体继承Character,拥有DamageHandle方法用于处理血量同步请求

class Character : public Movable
{
public:
	Character(int HP, int ID, Vector3 pos) :_HP(HP), Movable(ID, pos) {}
	virtual DamageSyncRsp DamageHandle(const DamageSyncReq* req) = 0;
	bool isDead() { return _HP <= 0; }
protected:
	int _HP;
};
Protagonist

主角还需OperationSyncHandle处理玩家操作同步请求

class Protagonist : public Character
{
public:
	Protagonist(int ID, Vector3 pos, int HP) :Character(HP, ID, pos) {}
	virtual DamageSyncRsp DamageHandle(const DamageSyncReq* req) override;
	OperationSyncRsp OperationSyncHandle(const OperationSyncReq* req, SceneObject* other);
	virtual ProtagonistInfo* GetReconnectInfo() override;
	virtual SceneObject* Clone() override { return new Protagonist(*this); }
private:
	bool _withWeapon = false;
};
Spirit

幽灵还需SpiritSyncHandle处理幽灵状态同步请求,此外还有一些私有数据成员用于完成复活等逻辑

class Spirit : public Character
{
public:
	Spirit(int ID, Vector3 pos, int HP, int ATK) :Character(HP, ID, pos), _ATK(ATK), _defaultHP(HP), _defaultPos(pos) {}
	virtual DamageSyncRsp DamageHandle(const DamageSyncReq* req) override;
	SpiritSyncRsp SpiritSyncHandle(const SpiritSyncReq* req);
	virtual SpiritInfo* GetReconnectInfo();
	virtual SceneObject* Clone() override { return new Spirit(*this); }
private:
	int _ATK;
	int _state = SpiritState::PATROL;
	int _wayPointIndex = 0;
	time_t _dieTime = 0;	// 记录死亡时间
	int _defaultHP;
	Vector3 _defaultPos;
};
使用运行时类型判断优化同步逻辑

由于同步信息中仅有同步的ID而无法确定类型,并且场景物体对象是以基类指针形式存储的map<int, SceneObject*> objMap;因此使用运行时类型判断来优化代码逻辑,以位置同步为例:

需要进行位置同步的场景物体都继承自Movable,因此只需将基类指针转化为Movable指针并调用PositionSyncHandle即可处理位置同步,其他同步类似

bool Scene::SceneSync(uv_tcp_t* client, const SceneSyncReq* req)
{
	SceneData* sceneData = _sceneDataMap[client];
	SceneSyncRsp rsp;
	// 处理位置同步
	for (auto i : req->positionsync()) {
		Movable* move = dynamic_cast<Movable*>(sceneData->objMap[i.id()]);
		if (move) {
			auto add = rsp.add_positionrsp();
			add->CopyFrom(move->PositionSyncHandle(&i));
		}
	}
	// 处理幽灵状态同步
	for (auto i : req->spiritsync()) {
		Spirit* spirit = dynamic_cast<Spirit*>(sceneData->objMap[i.id()]);
		if (spirit) {
			auto add = rsp.add_spiritrsp();
			add->CopyFrom(spirit->SpiritSyncHandle(&i));
		}
	}
    
    ...
}

场景逻辑处理类–Scene

静态场景数据

静态场景数据以Json文件形式存储与服务器,服务器启动时读取Json文件并以SceneID为键存储于map<int, string> _sceneJsonMap;

此外加载文件时服务器还会解析Json文件并实例化SceneObject存储于map<int, map<int, SceneObject*>> _defaultObjMap;用于后续逻辑处理

相关类成员:

// 存储场景Json文件
map<int, string> _sceneJsonMap;
// 存储不同场景的初始SceneObject
map<int, map<int, SceneObject*>> _defaultObjMap;

// 解析json文件,并写入map
int _parseSceneCfg(int sceneID, const char* const monitor);
// 从json文件中加载场景信息到map
bool _load_scece(int sceneID, const char* filePath);
玩家场景数据

每个玩家以Client为键保存一个SceneData*结构体,结构体内存储了所有玩家逻辑所需的数据,包括玩家ID、姓名等,还有map<int, SceneObject*> objMap;用于保存每个玩家拥有的动态的场景物体信息

struct SceneData {
	string playerID = "";
	string playerName = "";
	int SceneID = -1;
	bool isStart = false;
	time_t startTime = 0;
	time_t disconnectTime = 0;
	int killCount = 0;
	map<int, SceneObject*> objMap;

	void reset() {
		isStart = false;
		startTime = 0;
		disconnectTime = 0;
		killCount = 0;
	}
};

// 每个客户端存储一个SceneData数据结构
map<uv_tcp_t*, SceneData*> _sceneDataMap;
其他数据
// 存储不同场景的排行榜, 从小到大排列
map<int, Rank> _rankMap;
// 待重连的玩家Map(以PlayerID)为键
map<string, SceneData*> _disconnectMap;
使用注册的形式优化CMD分发

当使用Switch Case的形式分发CMD时,每增添一种CMD就要修改消息层代码,既增加了大量重复代码也不便于维护

switch (pack->cmd) {
        case CLIENT_LOGIN_REQ:
        {
            PlayerLoginReq req;
            req.ParseFromArray(pack->data, pack->len);
            g_playerMgr.player_login(client, &req);
            break;
        }
        case CLIENT_CREATE_REQ:
        {
            PlayerCreateReq req;
            req.ParseFromArray(pack->data, pack->len);
            g_playerMgr.player_create(client, &req);
            break;
        }
        
        ...
}

因此使用注册的形式将编译时判断延后到运行时判断,CMDHandle负责处理CMD分发

CMDHandle
class CMDhandle
{
public:
	using handle_t = void (*)(uv_tcp_t* client, Packet* pack);
	static CMDhandle* getInstance();
	void RegisterHandle(CLIENT_CMD cmd, handle_t handle);
	void HandlePack(uv_tcp_t* client, Packet* pack);
private:
	CMDhandle() {};
	static CMDhandle* instance;
	map<CLIENT_CMD, handle_t> _CMDhandleMap;	
};

CMDHandle作为一个单例提供了注册Handle和处理CMD两个主要方法,当分发CMD时只需调用HandlePack而无需Switch Case判断:

/*switch (pack->cmd) {
        case CLIENT_LOGIN_REQ:
        {
            PlayerLoginReq req;
            req.ParseFromArray(pack->data, pack->len);
            g_playerMgr.player_login(client, &req);
            break;
        }
        case CLIENT_CREATE_REQ:
        {
            PlayerCreateReq req;
            req.ParseFromArray(pack->data, pack->len);
            g_playerMgr.player_create(client, &req);
            break;
        }
        
        ...
}*/
// 改进CMD分发
CMDhandle::getInstance()->HandlePack(client, pack);

// 内部直接根据CMD回调对应处理函数
void CMDhandle::HandlePack(uv_tcp_t* client, Packet* pack)
{
	auto it = _CMDhandleMap.find((CLIENT_CMD)pack->cmd);
	if (it != _CMDhandleMap.end()) {
		it->second(client, pack);
	}
}

当有业务逻辑需要处理CMD时只需在初始化时注册即可

	CMDhandle::getInstance()->RegisterHandle(CLIENT_SCENE_LOAD_REQ, &SceneJsonLoadHandle);
	CMDhandle::getInstance()->RegisterHandle(CLIENT_SCENE_START_REQ, &SceneStartHandle);
	CMDhandle::getInstance()->RegisterHandle(CLIENT_SCENE_SETTLE_REQ, &SceneSettleHandle);
	CMDhandle::getInstance()->RegisterHandle(CLIENT_SCENE_RANK_REQ, &SceneRankHandle);
	CMDhandle::getInstance()->RegisterHandle(CLIENT_SCENE_SYNC_REQ, &SceneSyncHandle);
	CMDhandle::getInstance()->RegisterHandle(CLIENT_SCENE_RECONNECT_REQ, &ReconnectHandle);
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值