MMORPG寻路基于Unity的原生Navmesh数据实现服务端寻路

背景

在MMORPG中,寻路一般分为客户端寻路与服务器寻路,两种寻路方式的侧重点是不一样的,客户端寻路由于独享计算与内存资源,一般使用更高的精度与更长的时间,以达到更高质量的寻路。而服务器寻路由于要服务多个实体,则更侧重于,在保证寻路质量的前提下,使用更小的计算资源,更快速的输出寻路数据。

在通常情况下,玩家主角的寻路一般使用客户端寻路,客户端自行计算路径并开始移动,并将移动的过程点发送至服务器,服务器只进行校验,校验通过后则同步移动信息给其他玩家,校验失败则强制拉回原点。这样的方式可以最大程度的减小玩家的移动响应时间,同时节省服务器计算资源,玩家的移动体验较为优秀。

而NPC和怪物的寻路则一般使用服务器寻路,服务器直接进行路径的计算,并在移动心跳中逐步发送给客户端,服务器的计算可以保证多个玩家间看到的NPC的位置与移动的同步,同时也避免了修改NPC寻路路径的外挂的出现。

大部分情况下,客户端寻路一般使用游戏引擎内置的寻路系统。在常用的游戏引擎Unity或者Unreal中,均为Navmesh寻路系统,游戏引擎根据游戏场景模型预处理计算出Navmesh多边形数据,存储于资源(Asset)文件中,在游戏中加载进内存,进行寻路。

NavMesh(导航网格)数据

在Unity引擎中,NavMesh(导航网格)是用于AI路径查找的一个重要组件。NavMesh数据通常包含以下内容:

1. 网格顶点(Vertices)

NavMesh由多个三角形组成,这些三角形的顶点定义了导航网格的形状和大小。顶点数据包含每个顶点的三维坐标(x, y, z)。

2. 网格多边形(Polygons)

多边形是由顶点连接而成的三角形或其他形状。每个多边形包含顶点索引,定义了多边形的形状。

3. 边界(Edges)

边界定义了多边形之间的连接关系。边界数据包含每个边的起点和终点顶点索引,以及相邻多边形的索引。

4. 区域(Areas)

区域用于定义不同类型的导航区域,例如可行走区域、不可行走区域、跳跃区域等。每个区域都有一个唯一的标识符(ID)和类型。

5. 高度信息(Height Information)

高度信息用于处理三维空间中的导航问题,例如楼梯、斜坡和平台。高度信息包含每个顶点的高度值。

6. 连接(Links)

连接用于定义不同NavMesh之间的连接关系,例如跨越不同楼层的连接点。连接数据包含连接点的坐标和连接的多边形索引。

7. 标志(Flags)

标志用于定义多边形的属性,例如是否可行走、是否是跳跃区域等。标志数据包含每个多边形的属性标志。

8. 成本(Cost)

成本用于定义在不同区域行走的代价,例如在泥地上行走的代价比在平坦地面上行走的代价高。成本数据包含每个区域的行走代价。

9. 障碍物(Obstacles)

障碍物用于定义动态障碍物,例如移动的物体或临时障碍物。障碍物数据包含障碍物的形状、位置和大小。

NavMesh数据服务器解析

在Unity中,NavMesh(导航网格)通常用于AI路径寻路。NavMesh数据通常在客户端生成并使用,但在某些情况下,您可能希望在服务器端解析、加载和使用这些数据,例如在服务器端进行路径计算或验证。

要在服务器端解析和使用Unity生成的NavMesh数据,您需要执行以下步骤:

1. 导出NavMesh数据

首先,您需要在Unity客户端导出NavMesh数据。Unity提供了NavMeshData类,可以用来获取和保存NavMesh数据。

using UnityEngine;
using UnityEngine.AI;
using System.IO;

public class NavMeshExporter : MonoBehaviour
{
    public NavMeshSurface navMeshSurface;

    void Start()
    {
        ExportNavMesh();
    }

    void ExportNavMesh()
    {
        NavMeshData navMeshData = navMeshSurface.navMeshData;
        byte[] navMeshBytes = NavMeshDataToBytes(navMeshData);
        File.WriteAllBytes(Path.Combine(Application.persistentDataPath, "NavMeshData.bytes"), navMeshBytes);
        Debug.Log("NavMesh data exported.");
    }

    byte[] NavMeshDataToBytes(NavMeshData navMeshData)
    {
        // 序列化NavMeshData为字节数组
        // 这里需要实现具体的序列化逻辑
        // 例如使用BinaryFormatter或其他序列化方法
        // 这里只是一个示例,具体实现需要根据实际需求编写
        return new byte[0];
    }
}

2. 传输NavMesh数据到服务器

将导出的NavMesh数据文件传输到服务器。可以使用HTTP、FTP、WebSocket等方式进行传输。

3. 服务器端解析NavMesh数据

在服务器端,您需要解析并加载NavMesh数据。由于服务器端可能不运行Unity引擎,您需要使用一个能够解析和使用NavMesh数据的库或工具。

选项1:使用Unity服务器端

如果服务器端运行Unity引擎,您可以直接加载和使用NavMesh数据。

using UnityEngine;
using UnityEngine.AI;
using System.IO;

public class NavMeshLoader : MonoBehaviour
{
    void Start()
    {
        LoadNavMesh();
    }

    void LoadNavMesh()
    {
        string path = Path.Combine(Application.persistentDataPath, "NavMeshData.bytes");
        byte[] navMeshBytes = File.ReadAllBytes(path);
        NavMeshData navMeshData = BytesToNavMeshData(navMeshBytes);
        NavMesh.AddNavMeshData(navMeshData);
        Debug.Log("NavMesh data loaded.");
    }

    NavMeshData BytesToNavMeshData(byte[] navMeshBytes)
    {
        // 反序列化字节数组为NavMeshData
        // 这里需要实现具体的反序列化逻辑
        // 例如使用BinaryFormatter或其他反序列化方法
        // 这里只是一个示例,具体实现需要根据实际需求编写
        return new NavMeshData();
    }
}
选项2:使用第三方库

如果服务器端不运行Unity引擎,您可以使用第三方库来解析和使用NavMesh数据。例如,Recast & Detour是一个开源的导航网格生成和路径寻路库,可以在服务器端使用。

继续上面的示例,以下是如何使用Recast & Detour库在服务器端进行路径查询的完整示例代码:

4. 使用NavMesh数据进行路径计算(

#include "DetourNavMesh.h"
#include "DetourNavMeshQuery.h"

// 加载NavMesh数据
dtNavMesh* navMesh = ...; // 从文件或内存加载NavMesh数据
dtNavMeshQuery* navQuery = dtAllocNavMeshQuery();
navQuery->init(navMesh, 2048);

// 设置起点和终点
float startPos[3] = { ... }; // 起点坐标
float endPos[3] = { ... };   // 终点坐标

// 查询路径
dtQueryFilter filter;
dtPolyRef startRef, endRef;
float extents[3] = {2.0f, 4.0f, 2.0f}; // 搜索范围

// 找到起点和终点的最近多边形
navQuery->findNearestPoly(startPos, extents, &filter, &startRef, 0);
navQuery->findNearestPoly(endPos, extents, &filter, &endRef, 0);

dtPolyRef path[256];
int pathCount;
navQuery->findPath(startRef, endRef, startPos, endPos, &filter, path, &pathCount, 256);

if (pathCount > 0)
{
    // 找到路径
    float straightPath[256 * 3];
    unsigned char straightPathFlags[256];
    dtPolyRef straightPathPolys[256];
    int straightPathCount;
    
    navQuery->findStraightPath(startPos, endPos, path, pathCount,
                               straightPath, straightPathFlags,
                               straightPathPolys, &straightPathCount, 256);

    // 处理路径点
    for (int i = 0; i < straightPathCount; ++i)
    {
        float* pos = &straightPath[i * 3];
        printf("Path point %d: (%f, %f, %f)\n", i, pos[0], pos[1], pos[2]);
    }
}
else
{
    // 没有找到路径
    printf("No path found.\n");
}

5. 服务器端集成

将上述路径查询逻辑集成到服务器端应用中。以下是一个简单的示例,展示了如何在服务器端处理路径查询请求:

#include <iostream>
#include "DetourNavMesh.h"
#include "DetourNavMeshQuery.h"

// 假设我们有一个函数来加载NavMesh数据
dtNavMesh* LoadNavMesh(const char* path);

int main()
{
    // 加载NavMesh数据
    dtNavMesh* navMesh = LoadNavMesh("path/to/NavMeshData.bin");
    if (!navMesh)
    {
        std::cerr << "Failed to load NavMesh data." << std::endl;
        return -1;
    }

    dtNavMeshQuery* navQuery = dtAllocNavMeshQuery();
    navQuery->init(navMesh, 2048);

    // 示例路径查询
    float startPos[3] = {0.0f, 0.0f, 0.0f}; // 起点坐标
    float endPos[3] = {10.0f, 0.0f, 10.0f}; // 终点坐标

    dtQueryFilter filter;
    dtPolyRef startRef, endRef;
    float extents[3] = {2.0f, 4.0f, 2.0f}; // 搜索范围

    navQuery->findNearestPoly(startPos, extents, &filter, &startRef, 0);
    navQuery->findNearestPoly(endPos, extents, &filter, &endRef, 0);

    dtPolyRef path[256];
    int pathCount;
    navQuery->findPath(startRef, endRef, startPos, endPos, &filter, path, &pathCount, 256);

    if (pathCount > 0)
    {
        float straightPath[256 * 3];
        unsigned char straightPathFlags[256];
        dtPolyRef straightPathPolys[256];
        int straightPathCount;

        navQuery->findStraightPath(startPos, endPos, path, pathCount,
                                   straightPath, straightPathFlags,
                                   straightPathPolys, &straightPathCount, 256);

        for (int i = 0; i < straightPathCount; ++i)
        {
            float* pos = &straightPath[i * 3];
            std::cout << "Path point " << i << ": (" << pos[0] << ", " << pos[1] << ", " << pos[2] << ")" << std::endl;
        }
    }
    else
    {
        std::cout << "No path found." << std::endl;
    }

    // 清理
    dtFreeNavMeshQuery(navQuery);
    dtFreeNavMesh(navMesh);

    return 0;
}

dtNavMesh* LoadNavMesh(const char* path)
{
    // 读取NavMesh数据文件
    FILE* fp = fopen(path, "rb");
    if (!fp)
    {
        std::cerr << "Failed to open file: " << path << std::endl;
        return nullptr;
    }

    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long fileSize = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    // 读取文件内容
    unsigned char* data = new unsigned char[fileSize];
    fread(data, fileSize, 1, fp);
    fclose(fp);

    // 创建NavMesh
    dtNavMesh* navMesh = dtAllocNavMesh();
    if (!navMesh)
    {
        std::cerr << "Failed to allocate NavMesh." << std::endl;
        delete[] data;
        return nullptr;
    }

    // 初始化NavMesh
    dtStatus status = navMesh->init(data, fileSize, DT_TILE_FREE_DATA);
    if (dtStatusFailed(status))
    {
        std::cerr << "Failed to initialize NavMesh." << std::endl;
        dtFreeNavMesh(navMesh);
        delete[] data;
        return nullptr;
    }

    delete[] data;
    return navMesh;
}

6. 服务器端路径查询服务

为了使路径查询服务更加实用,您可以将其封装为一个网络服务,例如使用HTTP或WebSocket来处理客户端请求。以下是一个简单的示例,展示了如何使用C++和Boost.Asio库创建一个HTTP服务器来处理路径查询请求:

#include <iostream>
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include "DetourNavMesh.h"
#include "DetourNavMeshQuery.h"

using tcp = boost::asio::ip::tcp;
namespace http = boost::beast::http;

#include <iostream>
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#include "DetourNavMesh.h"
#include "DetourNavMeshQuery.h"

using tcp = boost::asio::ip::tcp;
namespace http = boost::beast::http;

dtNavMesh* LoadNavMesh(const char* path);

class PathfindingServer
{
public:
    PathfindingServer(boost::asio::io_context& ioc, tcp::endpoint endpoint, dtNavMesh* navMesh)
        : acceptor_(ioc, endpoint), navMesh_(navMesh)
    {
        do_accept();
    }

private:
    void do_accept()
    {
        acceptor_.async_accept(
            [this](boost::system::error_code ec, tcp::socket socket)
            {
                if (!ec)
                {
                    std::make_shared<Session>(std::move(socket), navMesh_)->start();
                }
                do_accept();
            });
    }

    tcp::acceptor acceptor_;
    dtNavMesh* navMesh_;
};

class Session : public std::enable_shared_from_this<Session>
{
public:
    Session(tcp::socket socket, dtNavMesh* navMesh)
        : socket_(std::move(socket)), navMesh_(navMesh)
    {
        navQuery_ = dtAllocNavMeshQuery();
        navQuery_->init(navMesh_, 2048);
    }

    void start()
    {
        do_read();
    }

private:
    void do_read()
    {
        auto self(shared_from_this());
        http::async_read(socket_, buffer_, request_,
            [this, self](boost::system::error_code ec, std::size_t bytes_transferred)
            {
                boost::ignore_unused(bytes_transferred);
                if (!ec)
                {
                    handle_request();
                }
            });
    }

    void handle_request()
    {
        auto const response = process_pathfinding_request();
        auto self(shared_from_this());
        http::async_write(socket_, response,
            [this, self](boost::system::error_code ec, std::size_t)
            {
                socket_.shutdown(tcp::socket::shutdown_send, ec);
            });
    }

    http::response<http::string_body> process_pathfinding_request()
    {
        // 解析请求中的起点和终点
        float startPos[3] = {0.0f, 0.0f, 0.0f};
        float endPos[3] = {10.0f, 0.0f, 10.0f};

        // 示例:从请求中解析起点和终点
        // 这里假设请求是一个简单的JSON格式
        // {"start": [0.0, 0.0, 0.0], "end": [10.0, 0.0, 10.0]}
        // 实际实现中需要解析请求体并提取坐标

        dtQueryFilter filter;
        dtPolyRef startRef, endRef;
        float extents[3] = {2.0f, 4.0f, 2.0f};

        navQuery_->findNearestPoly(startPos, extents, &filter, &startRef, 0);
        navQuery_->findNearestPoly(endPos, extents, &filter, &endRef, 0);

        dtPolyRef path[256];
        int pathCount;
        navQuery_->findPath(startRef, endRef, startPos, endPos, &filter, path, &pathCount, 256);

        std::string response_body;
        if (pathCount > 0)
        {
            float straightPath[256 * 3];
            unsigned char straightPathFlags[256];
            dtPolyRef straightPathPolys[256];
            int straightPathCount;

            navQuery_->findStraightPath(startPos, endPos, path, pathCount,
                                        straightPath, straightPathFlags,
                                        straightPathPolys, &straightPathCount, 256);

            response_body = "[";
            for (int i = 0; i < straightPathCount; ++i)
            {
                float* pos = &straightPath[i * 3];
                response_body += "[" + std::to_string(pos[0]) + "," + std::to_string(pos[1]) + "," + std::to_string(pos[2]) + "]";
                if (i < straightPathCount - 1)
                {
                    response_body += ",";
                }
            }
            response_body += "]";
        }
        else
        {
            response_body = "[]";
        }
        http::response<http::string_body> res{http::status::ok, request_.version()};
        res.set(http::field::server, "PathfindingServer");
        res.set(http::field::content_type, "application/json");
        res.keep_alive(request_.keep_alive());
        res.body() = response_body;
        res.prepare_payload();
        return res;
    }

    tcp::socket socket_;
    boost::beast::flat_buffer buffer_;
    http::request<http::string_body> request_;
    dtNavMesh* navMesh_;
    dtNavMeshQuery* navQuery_;
};

int main(int argc, char* argv[])
{
    try
    {
        if (argc != 3)
        {
            std::cerr << "Usage: PathfindingServer <address> <port>\n";
            return EXIT_FAILURE;
        }

        auto const address = boost::asio::ip::make_address(argv[1]);
        auto const port = static_cast<unsigned short>(std::atoi(argv[2]));

        boost::asio::io_context ioc{1};

        dtNavMesh* navMesh = LoadNavMesh("path/to/NavMeshData.bin");
        if (!navMesh)
        {
            std::cerr << "Failed to load NavMesh data." << std::endl;
            return EXIT_FAILURE;
        }

        PathfindingServer server(ioc, tcp::endpoint{address, port}, navMesh);

        ioc.run();
    }
    catch (const std::exception& e)
    {
        std::cerr << "Error: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }

    return EXIT_SUCCESS;
}

dtNavMesh* LoadNavMesh(const char* path)
{
    // 读取NavMesh数据文件
    FILE* fp = fopen(path, "rb");
    if (!fp)
    {
        std::cerr << "Failed to open file: " << path << std::endl;
        return nullptr;
    }

    // 获取文件大小
    fseek(fp, 0, SEEK_END);
    long fileSize = ftell(fp);
    fseek(fp, 0, SEEK_SET);

    // 读取文件内容
    unsigned char* data = new unsigned char[fileSize];
    fread(data, fileSize, 1, fp);
    fclose(fp);

    // 创建NavMesh
    dtNavMesh* navMesh = dtAllocNavMesh();
    if (!navMesh)
    {
        std::cerr << "Failed to allocate NavMesh." << std::endl;
        delete[] data;
        return nullptr;
    }

    // 初始化NavMesh
    dtStatus status = navMesh->init(data, fileSize, DT_TILE_FREE_DATA);
    if (dtStatusFailed(status))
    {
        std::cerr << "Failed to initialize NavMesh." << std::endl;
        dtFreeNavMesh(navMesh);
        delete[] data;
        return nullptr;
    }

    delete[] data;
    return navMesh;
}

7. 运行服务器

编译并运行服务器:

g++ -o PathfindingServer PathfindingServer.cpp -lboost_system -lboost_beast -ldetour -lrecast
./PathfindingServer 0.0.0.0 8080

8. 测试路径查询

使用HTTP客户端(如curl或Postman)发送路径查询请求:

curl -X POST http://localhost:8080/pathfinding -d '{"start": [0.0, 0.0, 0.0], "end": [10.0, 0.0, 10.0]}'

服务器将返回路径点的JSON数组:

[
    [0.0, 0.0, 0.0],
    [5.0, 0.0, 5.0],
    [10.0, 0.0, 10.0]
]

9. 结论

通过以上步骤,您可以在服务器端使用Recast & Detour库进行路径查询,并通过HTTP接口提供路径查询服务。这个示例展示了如何加载NavMesh数据、处理路径查询请求并返回路径点。您可以根据实际需求扩展和优化此示例,例如添加更多的错误处理、支持更多的请求格式等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值