跳转至

03-游戏服务器设计与客户端基础架构搭建

1 服务器设计

上一节我们分析了网络同步需求,要想实现不同玩家之间的世界状态同步,我们只需要同步不同玩家当前打字的进度即可。
所以我们现在便可以来到服务端代码中,引入头文件并编写这样的变量定义。

全局互斥锁在先前已经讨论过了。

#include <mutex>
#include <string>
#include <fstream>

std::mutex g_mutex;     // 全局互斥锁
std::string str_text;   // 文本内容

int progress_1 = -1;
int progress_2 = -1;

为了避免同时访问导致的数据竞争,我们需要使用锁来同步线程数据.
对于游戏中玩家需要输入的文本内容,在金山打字通的生死时速中,数据文本可以直接北玩家修改,所以时有作弊发生.所以在我们的《哈基米大冒险》中,数据文件存放在服务端侧。
对于两位玩家的进度表示,我们使用progress_1和progress_2两个整形变量进行存储。默认状态的初始值为-1,当玩家加入后,这个值被修改为0.而在游戏对局进行的过程中,这个值会随着不同客户端提交的游戏进度增长为合适的值。
那么来到main函数部分,由于游戏中的文本内容被存放到了外部单独的文件中
所以我们先读取文件,然后存储在str_text中,
int main(int argc, char** argv)
{
    std::ifstream file("text.txt");

    if(!file.good()) {
        MessageBox(nullptr, _T("无法打开文本文件text.txt"), _T("启动失败"), MB_OK | MB_ICONERROR);
        return -1;
    }

    std::stringstream str_stream;
    str_stream << file.rdbuf();
    str_text = str_stream.str();

    file.close();

    return 0;
}

下面便是定义服务器对象,并开始设计通信的借口了。
游戏中玩家存在三个状态:未登录,已登录和游戏进行。
那么我们自然需要提供一个login登录接口,来让玩家告知服务器,我已经成功加入游戏了。login登录接口不需要读取请求体中额外的数据,但我们需要返回给客户端一个玩家ID。来告诉客户端你是玩家1还是玩家2.
所以在程序中,我们检测一个特殊的情况,如果玩家1已经登录且玩家2也已经登录到游戏中了,那么后续加入的玩家自然就是多余的了。那么我们对这些多余的客户端返回一个特殊值-1.
如果正常登录,那么我们向客户端返回对应的ID。然后初始化对应玩家的进度值。

httplib::Server server;
server.Post("/login", [&](const httplib::Request& req, httplib::Response& res)
    {
        std::lock_guard<std::mutex> lock(g_mutex);
        if (progress_1 >= 0 && progress_2 >= 0) {
            res.set_content("-1", "text/plain");
            return;
        }
        res.set_content(progress_1 >= 0 ? "2" : "1", "text/plain");
        (progress_1 >= 0) ? (progress_2 = 0) : (progress_1 = 0);
    });

既然我们的打字内容存放在服务端,那么自然也需要一个接口用来传输游戏文本内容。
我们只需要向客户端返回先前读取的文本文件内容即可。

server.Post("/query_text", [&](const httplib::Request& req, httplib::Response& res)
    {
        res.set_content(str_text, "text/plain");
    });

下面便是游戏进行的过程中两个极为关键的通信接口。update_1和update_2两个路由的处理逻辑几乎完全相同。
我们以玩家1的更新代码为例,客户端向服务端发送的请求体中,包含的是玩家1当前打字数字符串。我们将它转化为整型存储在

server.Post("/update_1", [&](const httplib::Request& req, httplib::Response& res)
    {
        std::lock_guard<std::mutex> lock(g_mutex);
        progress_1 = std::stoi(req.body);
        res.set_content(std::to_string(progress_2), "text/plain");
    });

server.Post("/update_2", [&](const httplib::Request& req, httplib::Response& res)
    {
        std::lock_guard<std::mutex> lock(g_mutex);
        progress_2 = std::stoi(req.body);
        res.set_content(std::to_string(progress_1), "text/plain");
    });

最后不要忘记调用调用服务器的listen方法,让它监听25565端口。
server.listen("localhost", 25565);

我们让服务器监听的是localhost,而不是0.0.0.0这个主机地址。
那么localhost,0.0.0.0,以及127.0.0.1这几个IP地址有什么区别呢?

  • 127.0.0.1: 固定代表本机自己,只在本机内部通信,别人的电脑 / 手机绝对访问不到
  • localhost:是个名字,不是 IP。系统默认解析成 127.0.0.1。日常开发里基本等价于 127.0.0.1
  • 0.0.0.0:不代表某一个 IP。用在服务启动绑定时:绑定 0.0.0.0 → 允许任何网卡、任何 IP访问。绑定 127.0.0.1 → 只能本机访问

到此为止,我们哈基米大冒险服务端的程序就全部完成了。

2 客户端的底层类封装

我们首先在client目录下引入之前项目中编写的Vector2头文件。游戏所需的底层框架类代码和先前项目中的几乎完全一样。
需要注意的是我们在vector2中添加关于除法的运算符重载

Vector2 operator/(float val) const
{
    return Vector2(x / val, y / val);
}

void operator/=(float val)
{
    x /= val, y /= val;
}

然后是Atlas图集类头文件中对应的内容。
之后,引入camera.h,删除镜头抖动相关的代码,并增添成员变量size和方法look_at().
方法look_at()方便让摄像机跟随玩家移动。
#include "vector2.h"

class Camera
{
public:
    Camera() : position(0.0f, 0.0f) { } 
    ~Camera() = default;

    void set_size(const Vector2 size) 
    {
        this->size = size;
    }

    const Vector2& get_size() const
    {
        return size;
    }

    const Vector2& get_position() const
    {
        return position;
    }

    void reset()
    {
        position.x = 0;
        position.y = 0;
    }

    void look_at(const Vector2& target)
    {
        position = target - size / 2.0f;
    }

private:
    Vector2 position;               //摄像机位置
    Vector2 size;
};

随后,引入util.h
#include <graphics.h>
#include <string>

struct Rect
{
    int x, y;   // 左上角点坐标
    int w, h;   // 宽度和高度
};

inline void putimage_ex(IMAGE* img, const Rect* rect_dst, const Rect* rect_src = nullptr)
{
    // 当无需裁切图片时,无需传入rect_src
    static BLENDFUNCTION blend_func = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
    AlphaBlend(GetImageHDC(GetWorkingImage()), rect_dst->x, rect_dst->y, rect_dst->w, rect_dst->h,
               GetImageHDC(img), rect_src ? rect_src->x : 0, rect_src ? rect_src->y : 0,
               rect_src ? rect_src->w : img->getwidth(), rect_src ? rect_src->h : img->getheight(), blend_func);
}

inline void load_audio(LPCSTR path, LPCSTR id)
{
    static TCHAR str_cmd[512];
    _stprintf_s(str_cmd, _T("open %s alias %s"), path, id);
    mciSendString(str_cmd, NULL, 0, NULL);
}

inline void play_audio(LPCTSTR id, bool is_loop = false)
{
    static TCHAR str_cmd[512];
    _stprintf_s(str_cmd, _T("play %s %s from 0"), id, is_loop ? _T("repeat") : _T(""));
    mciSendString(str_cmd, NULL, 0, NULL);
}

inline void stop_audio(LPCTSTR id)
{
    static TCHAR str_cmd[512];
    _stprintf_s(str_cmd, _T("stop %s"), id);
    mciSendString(str_cmd, NULL, 0, NULL);
}

inline int range_random(int min_num, int max_num)   // 在[min_num, max_num]范围内返回一个随机整数
{
    return min_num + rand() % (max_num - min_num + 1);
}

并且引入timer.h定时器和animaiton.h动画类

评论

如果你已登录 GitHub,就可以直接在这里评论。