简易场景服务
unity/c++ mmo大型多人在线游戏开发系列教程,本节代码在此。
本节我们来让之前的tcp服务器真正往游戏服务器靠一点,而游戏服务器在mmo中很重要的一个功能就是提供场景服务。
目标
什么是场景服务呢?人话来说就是提供一个场景,让多个玩家能同时参与其中,互相看到。
这也就是我们本节的小目标,后续玩家在场景中的各种交互,例如战斗,都是建立在此基础之上。
服务
之前的echo服务,我们的代码中并没有体现服务的概念,对消息的解析和处理都直接写在了TcpConnection
中。但TcpConnection
应该是作为底层组件的存在,不应该涉及具体的服务逻辑。不然当要同时实现echo服务和场景服务时, TcpConnection::handle_data
里就要写if/else了。
所以这里先抽象出一个服务的概念:
class Service {
public:
virtual ~Service() {}
virtual void start(const std::string& listen_address, int listen_port) {
_server = new TcpServer(listen_address, listen_port, this);
_server->start(EVENT_BASE);
}
virtual void stop() {
_server->stop();
_server = nullptr;
}
virtual void on_new_connection(TcpConnection* conn) {}
virtual void on_lost_connection(TcpConnection* conn) {
// 默认直接把连接对象删掉
delete conn;
}
virtual void handle_msg(TcpConnection* conn, const std::string& msg) = 0;
private:
TcpServer* _server = nullptr;
};
上层逻辑不再直接创建TcpServer
,而是创建相应的Service
,例如对于echo服务来说,可以实现一个EchoService
:
class EchoService : public Service {
public:
virtual void handle_msg(TcpConnection* conn, const std::string& msg) override {
conn->send_msg(msg.c_str(), msg.size());
}
};
// 上层直接创建service
// EchoService echo_service;
// echo_service.start(HOST, PORT);
同时,现在TcpConnection
不再由TcpServer
管理,TcpServer
仅负责生成,然后交由Service
管理。
场景服务
现在,我们实现一个场景服务类,逻辑很简单,启动时创建一个场景,并处理玩家进入和退出的请求。
但说起来,我们的代码里连玩家是什么都还没定义,定义一下:
class Player {
public:
// conn的生命周期由外部管理
Player(TcpConnection* conn, const std::string& name) : _conn(conn), _name(name) {}
~Player() {}
inline TcpConnection* get_conn() { return _conn; }
inline const std::string& get_name() const { return _name; }
void send_msg(const char* msg_bytes, size_t n);
inline void set_position(float x, float y, float z) {
_x = x;
_y = y;
_z = z;
}
inline void get_position(float& x, float& y, float& z) const {
x = _x;
y = _y;
z = _z;
}
private:
TcpConnection* _conn;
std::string _name;
float _x;
float _y;
float _z;
};
目前Player
只有简单的名字和位置信息。
同样,我们也需要定义何谓场景:
class Space {
public:
Space(size_t w, size_t h) : _width(w), _height(h) {}
~Space();
void join(Player* player);
void leave(Player* player);
bool has_player(Player* player);
private:
size_t _width;
size_t _height;
std::vector<Player*> _players;
};
最后,终于可以定义场景服务了:
class SpaceService : public Service {
public:
~SpaceService() {}
virtual void start(const std::string& listen_address, int listen_port) override;
virtual void stop() override;
virtual void on_lost_connection(TcpConnection* conn) override;
virtual void handle_msg(TcpConnection* conn, const std::string& msg) override;
// 请求以username登录
void login(TcpConnection* conn, const std::string& username);
// 请求进入场景(此时客户端场景已加载完成)
void join(TcpConnection* conn);
// 请求离开场景
void leave(TcpConnection* conn);
Player* find_player(TcpConnection* conn);
private:
Space* _space = nullptr;
std::map<TcpConnection*, Player*> _conn_2_player;
std::set<std::string> _exists_names;
};
在SpaceService::handle_msg
中,我们解析客户端发上来的请求,然后进行相应的处理:
void SpaceService::handle_msg(TcpConnection* conn, const std::string& msg)
{
if (msg.starts_with("login#")) {
std::string username = msg.substr(5);
login(conn, username);
}
else if (msg.starts_with("join")) {
join(conn);
}
else if (msg.starts_with("leave")) {
leave(conn);
}
}
本节我们的目标就仅仅是让多个玩家可以进入场景,然后互相看见而已。
整个流程很简单,如下图:
客户端先登录,然后加载默认场景,再向服务端发起进入场景的请求。一切顺利的话,服务端会为其创建Player
对象,并加入到Space
的随机位置中,同时向其下发当前场景中的所有玩家信息。当然,也会向其他所有玩家发送有新玩家进入的消息。两个消息其实是一样的,都是players_enter_sight
:
void Space::join(Player* player)
{
if (has_player(player))
return;
// 随机一个出生点
float x = get_random() * _width;
float y = 3.f;
float z = get_random() * _height;
player->set_position(x, y, z);
std::ostringstream buffer;
// 告知玩家加入场景成功,并附带初始坐标
buffer << "join_successed#" << player->get_name() << ":" << x << ":" << y << ":" << z;
std::string join_successed_reply = buffer.str();
player->send_msg(join_successed_reply.c_str(), join_successed_reply.size());
buffer.str("");
buffer.clear();
buffer << "players_enter_sight#" << player->get_name() << ":" << x << ":" << y << ":" << z;
std::string enter_other_sight = buffer.str();
buffer.str("");
buffer.clear();
std::string players_enter_sight{"players_enter_sight#"};
bool first = true;
for (Player* other : _players) {
// 告知场景内的其他玩家,有新玩家进入了场景
other->send_msg(enter_other_sight.c_str(), enter_other_sight.size());
float px, py, pz;
other->get_position(px, py, pz);
std::ostringstream buffer;
buffer << other->get_name() << ":" << px << ":" << py << ":" << pz;
if (!first)
players_enter_sight += "|";
else
first = false;
players_enter_sight += buffer.str();
}
// 把场景内所有已存在的玩家信息发送给新玩家
player->send_msg(players_enter_sight.c_str(), players_enter_sight.size());
_players.push_back(player);
}
客户端收到players_enter_sight
时,就可以以相应的信息去创建其他玩家的显示了。目前我们就先以一个默认的Sphere
来显示就好:
public class NetworkManager : MonoBehaviour
{
private void HandleNetworkMsg(string msg)
{
// ...
else if (msg.StartsWith("players_enter_sight#"))
{
int pos = msg.IndexOf('#');
string tmp = msg.Substring(pos+1);
if (tmp.Length == 0)
return;
string[] playerStrings = tmp.Split('|');
foreach (string playerString in playerStrings)
{
string[] arguments = playerString.Split(':');
Vector3 position = new Vector3(float.Parse(arguments[1]), float.Parse(arguments[2]), float.Parse(arguments[3]));
Debug.Log($"player enter sight, name: {arguments[0]}, position: {position}");
GameObject otherPlayer = GameObject.CreatePrimitive(PrimitiveType.Sphere);
otherPlayer.transform.position = position;
_players.Add(arguments[0], otherPlayer);
}
}
}
}
当客户端退出时,服务端会发送players_leave_sight
给场景内的其他玩家,他们就可以删除相应的显示了。
小结
现在我们有了简单的场景服务,不过仅仅处理了玩家的进出与显示,连最简单的移动同步都还没做,所以本地是看不到其他玩家的移动的。本来下一节应该就是写移动同步了,但是,可以看到我们消息处理部分的代码纯靠解析字符串,这样非常低效,扩展性也很差。因此下一节我们先来整一个好用一点的消息处理框架吧。