背景
在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数据、处理路径查询请求并返回路径点。您可以根据实际需求扩展和优化此示例,例如添加更多的错误处理、支持更多的请求格式等。
2231

被折叠的 条评论
为什么被折叠?



