上一节讲到的客户端使用Unity 自带的 NavMesh 来做寻路 3D寻路系统NavMesh-客户端篇。然而,怪物的刷新、移动,和AI是由服务器负责的,怪物的寻路是由服务器控制的,或者像SLG,大地图寻路在玩家离线的情况下要继续寻路,这必须要服务器来主导寻路。
那么,这怎么去实现呢?
我们服务器必须要有一份导航网格的寻路数据。
用ExportSceneToObj 工具导出场景,这个好像不依赖于烘焙出来的导航网格。

//TODO 贴一下源码:
第一份源码是不依赖于导航网格的生成工具:

using System.IO;
using System.Text;
using UnityEditor;
using UnityEngine;
using UnityEngine.SceneManagement;public class ExportScene : EditorWindow
{private const string CUT_LB_OBJ_PATH = "export/bound_lb";private const string CUT_RT_OBJ_PATH = "export/bound_rt";private static float autoCutMinX = 1000;private static float autoCutMaxX = 0;private static float autoCutMinY = 1000;private static float autoCutMaxY = 0;private static float cutMinX = 0;private static float cutMaxX = 0;private static float cutMinY = 0;private static float cutMaxY = 0;private static long startTime = 0;private static int totalCount = 0;private static int count = 0;private static int counter = 0;private static int progressUpdateInterval = 10000;[MenuItem("ExportScene/ExportSceneToObj")][MenuItem("GameObject/ExportScene/ExportSceneToObj")]public static void Export(){ExportSceneToObj(false);}[MenuItem("ExportScene/ExportSceneToObj(AutoCut)")][MenuItem("GameObject/ExportScene/ExportSceneToObj(AutoCut)")]public static void ExportAutoCut(){ExportSceneToObj(true);}[MenuItem("ExportScene/ExportSelectedObj")][MenuItem("GameObject/ExportScene/ExportSelectedObj", priority = 44)]public static void ExportObj(){GameObject selectObj = Selection.activeGameObject;if (selectObj == null){Debug.LogWarning("Select a GameObject");return;}string path = GetSavePath(false, selectObj);if (string.IsNullOrEmpty(path)) return;Terrain terrain = selectObj.GetComponent<Terrain>();MeshFilter[] mfs = selectObj.GetComponentsInChildren<MeshFilter>();SkinnedMeshRenderer[] smrs = selectObj.GetComponentsInChildren<SkinnedMeshRenderer>();Debug.Log(mfs.Length + "," + smrs.Length);ExportSceneToObj(path, terrain, mfs, smrs, false, false);}public static void ExportSceneToObj(bool autoCut){string path = GetSavePath(autoCut, null);if (string.IsNullOrEmpty(path)) return;Terrain terrain = UnityEngine.Object.FindObjectOfType<Terrain>();MeshFilter[] mfs = UnityEngine.Object.FindObjectsOfType<MeshFilter>();SkinnedMeshRenderer[] smrs = UnityEngine.Object.FindObjectsOfType<SkinnedMeshRenderer>();ExportSceneToObj(path, terrain, mfs, smrs, autoCut, true);}public static void ExportSceneToObj(string path, Terrain terrain, MeshFilter[] mfs,SkinnedMeshRenderer[] smrs, bool autoCut, bool needCheckRect){int vertexOffset = 0;string title = "export GameObject to .obj ...";StreamWriter writer = new StreamWriter(path);startTime = GetMsTime();UpdateCutRect(autoCut);counter = count = 0;progressUpdateInterval = 5;totalCount = (mfs.Length + smrs.Length) / progressUpdateInterval;foreach (var mf in mfs){UpdateProgress(title);if (mf.GetComponent<Renderer>() != null &&(!needCheckRect || (needCheckRect && IsInCutRect(mf.gameObject)))){ExportMeshToObj(mf.gameObject, mf.sharedMesh, ref writer, ref vertexOffset);}}foreach (var smr in smrs){UpdateProgress(title);if (!needCheckRect || (needCheckRect && IsInCutRect(smr.gameObject))){ExportMeshToObj(smr.gameObject, smr.sharedMesh, ref writer, ref vertexOffset);}}if (terrain){ExportTerrianToObj(terrain.terrainData, terrain.GetPosition(),ref writer, ref vertexOffset, autoCut);}writer.Close();EditorUtility.ClearProgressBar();long endTime = GetMsTime();float time = (float)(endTime - startTime) / 1000;Debug.Log("Export SUCCESS:" + path);Debug.Log("Export Time:" + time + "s");OpenDir(path);}private static void OpenDir(string path){DirectoryInfo dir = Directory.GetParent(path);int index = path.LastIndexOf("/");OpenCmd("explorer.exe", dir.FullName);}private static void OpenCmd(string cmd, string args){System.Diagnostics.Process.Start(cmd, args);}private static string GetSavePath(bool autoCut, GameObject selectObject){string dataPath = Application.dataPath;string dir = dataPath.Substring(0, dataPath.LastIndexOf("/"));string sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;string defaultName = "";if (selectObject == null){defaultName = (autoCut ? sceneName + "(autoCut)" : sceneName);}else{defaultName = (autoCut ? selectObject.name + "(autoCut)" : selectObject.name);}return EditorUtility.SaveFilePanel("Export .obj file", dir, defaultName, "obj");}private static long GetMsTime(){return System.DateTime.Now.Ticks / 10000;//return (System.DateTime.Now.ToUniversalTime().Ticks - 621355968000000000) / 10000;}private static void UpdateCutRect(bool autoCut){cutMinX = cutMaxX = cutMinY = cutMaxY = 0;if (!autoCut){Vector3 lbPos = GetObjPos(CUT_LB_OBJ_PATH);Vector3 rtPos = GetObjPos(CUT_RT_OBJ_PATH);cutMinX = lbPos.x;cutMaxX = rtPos.x;cutMinY = lbPos.z;cutMaxY = rtPos.z;}}private static void UpdateAutoCutRect(Vector3 v){if (v.x < autoCutMinX) autoCutMinX = v.x;if (v.x > autoCutMaxX) autoCutMaxX = v.x;if (v.z < autoCutMinY) autoCutMinY = v.z;if (v.z > autoCutMaxY) autoCutMaxY = v.z;}private static bool IsInCutRect(GameObject obj){if (cutMinX == 0 && cutMaxX == 0 && cutMinY == 0 && cutMaxY == 0) return true;Vector3 pos = obj.transform.position;if (pos.x >= cutMinX && pos.x <= cutMaxX && pos.z >= cutMinY && pos.z <= cutMaxY)return true;elsereturn false;}private static void ExportMeshToObj(GameObject obj, Mesh mesh, ref StreamWriter writer, ref int vertexOffset){Quaternion r = obj.transform.localRotation;StringBuilder sb = new StringBuilder();foreach (Vector3 vertice in mesh.vertices){Vector3 v = obj.transform.TransformPoint(vertice);UpdateAutoCutRect(v);sb.AppendFormat("v {0} {1} {2}\n", -v.x, v.y, v.z);}foreach (Vector3 nn in mesh.normals){Vector3 v = r * nn;sb.AppendFormat("vn {0} {1} {2}\n", -v.x, -v.y, v.z);}foreach (Vector3 v in mesh.uv){sb.AppendFormat("vt {0} {1}\n", v.x, v.y);}for (int i = 0; i < mesh.subMeshCount; i++){int[] triangles = mesh.GetTriangles(i);for (int j = 0; j < triangles.Length; j += 3){sb.AppendFormat("f {1} {0} {2}\n",triangles[j] + 1 + vertexOffset,triangles[j + 1] + 1 + vertexOffset,triangles[j + 2] + 1 + vertexOffset);}}vertexOffset += mesh.vertices.Length;writer.Write(sb.ToString());}private static void ExportTerrianToObj(TerrainData terrain, Vector3 terrainPos,ref StreamWriter writer, ref int vertexOffset, bool autoCut){int tw = terrain.heightmapResolution;int th = terrain.heightmapResolution;Vector3 meshScale = terrain.size;meshScale = new Vector3(meshScale.x / (tw - 1), meshScale.y, meshScale.z / (th - 1));Vector2 uvScale = new Vector2(1.0f / (tw - 1), 1.0f / (th - 1));Vector2 terrainBoundLB, terrainBoundRT;if (autoCut){terrainBoundLB = GetTerrainBoundPos(new Vector3(autoCutMinX, 0, autoCutMinY), terrain, terrainPos);terrainBoundRT = GetTerrainBoundPos(new Vector3(autoCutMaxX, 0, autoCutMaxY), terrain, terrainPos);}else{terrainBoundLB = GetTerrainBoundPos(CUT_LB_OBJ_PATH, terrain, terrainPos);terrainBoundRT = GetTerrainBoundPos(CUT_RT_OBJ_PATH, terrain, terrainPos);}int bw = (int)(terrainBoundRT.x - terrainBoundLB.x);int bh = (int)(terrainBoundRT.y - terrainBoundLB.y);int w = bh != 0 && bh < th ? bh : th;int h = bw != 0 && bw < tw ? bw : tw;int startX = (int)terrainBoundLB.y;int startY = (int)terrainBoundLB.x;if (startX < 0) startX = 0;if (startY < 0) startY = 0;Debug.Log(string.Format("Terrian:tw={0},th={1},sw={2},sh={3},startX={4},startY={5}",tw, th, bw, bh, startX, startY));float[,] tData = terrain.GetHeights(0, 0, tw, th);Vector3[] tVertices = new Vector3[w * h];Vector2[] tUV = new Vector2[w * h];int[] tPolys = new int[(w - 1) * (h - 1) * 6];for (int y = 0; y < h; y++){for (int x = 0; x < w; x++){Vector3 pos = new Vector3(-(startY + y), tData[startX + x, startY + y], (startX + x));tVertices[y * w + x] = Vector3.Scale(meshScale, pos) + terrainPos;tUV[y * w + x] = Vector2.Scale(new Vector2(x, y), uvScale);}}int index = 0;for (int y = 0; y < h - 1; y++){for (int x = 0; x < w - 1; x++){tPolys[index++] = (y * w) + x;tPolys[index++] = ((y + 1) * w) + x;tPolys[index++] = (y * w) + x + 1;tPolys[index++] = ((y + 1) * w) + x;tPolys[index++] = ((y + 1) * w) + x + 1;tPolys[index++] = (y * w) + x + 1;}}count = counter = 0;progressUpdateInterval = 10000;totalCount = (tVertices.Length + tUV.Length + tPolys.Length / 3) / progressUpdateInterval;string title = "export Terrain to .obj ...";for (int i = 0; i < tVertices.Length; i++){UpdateProgress(title);StringBuilder sb = new StringBuilder(22);sb.AppendFormat("v {0} {1} {2}\n", tVertices[i].x, tVertices[i].y, tVertices[i].z);writer.Write(sb.ToString());}for (int i = 0; i < tUV.Length; i++){UpdateProgress(title);StringBuilder sb = new StringBuilder(20);sb.AppendFormat("vt {0} {1}\n", tUV[i].x, tUV[i].y);writer.Write(sb.ToString());}for (int i = 0; i < tPolys.Length; i += 3){UpdateProgress(title);int x = tPolys[i] + 1 + vertexOffset; ;int y = tPolys[i + 1] + 1 + vertexOffset;int z = tPolys[i + 2] + 1 + vertexOffset;StringBuilder sb = new StringBuilder(30);sb.AppendFormat("f {0} {1} {2}\n", x, y, z);writer.Write(sb.ToString());}vertexOffset += tVertices.Length;}private static Vector2 GetTerrainBoundPos(string path, TerrainData terrain, Vector3 terrainPos){var go = GameObject.Find(path);if (go){Vector3 pos = go.transform.position;return GetTerrainBoundPos(pos, terrain, terrainPos);}return Vector2.zero;}private static Vector2 GetTerrainBoundPos(Vector3 worldPos, TerrainData terrain, Vector3 terrainPos){Vector3 tpos = worldPos - terrainPos;return new Vector2((int)(tpos.x / terrain.size.x * terrain.heightmapResolution),(int)(tpos.z / terrain.size.z * terrain.heightmapResolution));}private static Vector3 GetObjPos(string path){var go = GameObject.Find(path);if (go){return go.transform.position;}return Vector3.zero;}private static void UpdateProgress(string title){if (counter++ == progressUpdateInterval){counter = 0;float process = Mathf.InverseLerp(0, totalCount, ++count);long currTime = GetMsTime();float sec = ((float)(currTime - startTime)) / 1000;string text = string.Format("{0}/{1}({2:f2} sec.)", count, totalCount, sec);EditorUtility.DisplayProgressBar(title, text, process);}}
}

第二份是要基于导航网格的,但生成的效果不大理想,所以没有采用:

/************************************************* 文件名:ExportNavMesh.cs* 描述:导出NavMesh数据给服务器使用* ************************************************/using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using UnityEngine.AI;
using UnityEngine.SceneManagement;public class ExportNavMesh
{[MenuItem("NavMesh/Export")]static void Export(){Debug.Log("ExportNavMesh");NavMeshTriangulation tmpNavMeshTriangulation = NavMesh.CalculateTriangulation();//新建文件string tmpPath = Application.dataPath + "/" + UnityEngine.SceneManagement.SceneManager.GetActiveScene().name + ".obj";StreamWriter tmpStreamWriter = new StreamWriter(tmpPath);//顶点for (int i = 0; i < tmpNavMeshTriangulation.vertices.Length; i++){tmpStreamWriter.WriteLine("v  " + tmpNavMeshTriangulation.vertices[i].x + " " +tmpNavMeshTriangulation.vertices[i].y + " " +tmpNavMeshTriangulation.vertices[i].z);}tmpStreamWriter.WriteLine("g pPlane1");//索引for (int i = 0; i < tmpNavMeshTriangulation.indices.Length;){tmpStreamWriter.WriteLine("f " + (tmpNavMeshTriangulation.indices[i] + 1) + " " +(tmpNavMeshTriangulation.indices[i + 1] + 1) + " " +(tmpNavMeshTriangulation.indices[i + 2] + 1));i = i + 3;}tmpStreamWriter.Flush();tmpStreamWriter.Close();Debug.Log("ExportNavMesh Success");}
}

选中场景,选择菜单,ExportScene–>ExportSelectedObj,会生成一个Main Terrain.obj文件,大概20几M.
然后用recast 工具打开:
选择solomesh,

调整Agent下Radius参数,与当前项目中 最大寻路单位的半径 保持一致,点击 Build ,等待一段时间后,中间渲染图形会显示生成后的寻路网格,

蓝色地块就是可通点,可以尝试寻路一下。原始的recast是没有开始点和结束点的坐标的,那如何能显示出来呢?

void NavMeshTesterTool::handleRenderOverlay(double* proj, double* model, int* view)
{GLdouble x, y, z;char buf[64];// Draw start and end point labelsif (m_sposSet && gluProject((GLdouble)m_spos[0], (GLdouble)m_spos[1], (GLdouble)m_spos[2],model, proj, view, &x, &y, &z)){if (m_showCoord){snprintf(buf, sizeof(buf), "Start (%.1f, %.1f, %.1f)", m_spos[0], m_spos[1], m_spos[2]);imguiDrawText((int)x, (int)(y - 25), IMGUI_ALIGN_CENTER, buf, imguiRGBA(0, 0, 0, 220));}elseimguiDrawText((int)x, (int)(y - 25), IMGUI_ALIGN_CENTER, "Start", imguiRGBA(0, 0, 0, 220));}if (m_toolMode == TOOLMODE_RAYCAST && m_hitResult && m_showCoord &&gluProject((GLdouble)m_hitPos[0], (GLdouble)m_hitPos[1], (GLdouble)m_hitPos[2],model, proj, view, &x, &y, &z)){snprintf(buf, sizeof(buf), "HitPos (%.1f, %.1f, %.1f)", m_hitPos[0], m_hitPos[1], m_hitPos[2]);imguiDrawText((int)x, (int)(y - 25), IMGUI_ALIGN_CENTER, buf, imguiRGBA(0, 0, 0, 220));}if (m_eposSet && gluProject((GLdouble)m_epos[0], (GLdouble)m_epos[1], (GLdouble)m_epos[2],model, proj, view, &x, &y, &z)){if (m_showCoord){float totalCost = 0.0f;for (int i = 0; i + 1 < m_nstraightPath; i++)totalCost += dtVdist(&m_straightPath[i * 3], &m_straightPath[(i + 1) * 3]);snprintf(buf, sizeof(buf), "End (%.1f, %.1f, %.1f), Cost %.1f", m_epos[0], m_epos[1], m_epos[2], totalCost);imguiDrawText((int)x, (int)(y - 25), IMGUI_ALIGN_CENTER, buf, imguiRGBA(0, 0, 0, 220));}elseimguiDrawText((int)x, (int)(y - 25), IMGUI_ALIGN_CENTER, "End", imguiRGBA(0, 0, 0, 220));}
}

那么,如何能显示出关键点point list?,首先,路径搜索的模式要改成TOOLMODE_PATHFIND_STRAIGHT模式,代码需要增加如下的打印,
在NavMeshTesterTool.cpp中增加,

void NavMeshTesterTool::recalc(){....
....if (m_toolMode == TOOLMODE_PATHFIND_STRAIGHT) {m_sample->getContext()->log(RC_LOG_PROGRESS, "total point size=%d", m_nstraightPath);for (int i = 0; i < m_nstraightPath; ++i){m_sample->getContext()->log(RC_LOG_PROGRESS, "(%.1f, %.1f, %.1f)", m_straightPath[i * 3], m_straightPath[i * 3 + 1], m_straightPath[i * 3 + 2]);}}
}

在Sample.h中增加

public:Sample();virtual ~Sample();void setContext(BuildContext* ctx) { m_ctx = ctx; }BuildContext* getContext() {return m_ctx;}

寻路结果为:

这里要提一下,这里导出的坐标是以左手坐标系,也就是(x,y,z),这和unity一致,实际上客服端发给服务器的是x,z 当做坐标点,这点要特别注意, 左手坐标系是什么样的?如图所示

点击 save ,会在当前目录下生成【导出文件】 solo_navmesh.bin,只有156KB,就是服务器导航需要的bin文件。
引入第三方库:

基于recast4j封装的Java版本3D游戏寻路组件

<dependency><groupId>com.github.silencesu</groupId><artifactId>Easy3dNav</artifactId><version>1.1.0</version>
</dependency>
       Easy3dNav   javaNav = new Easy3dNav();javaNav.setPrintMeshInfo(false);javaNav.setUseU3dData(false);javaNav .init(id, "../../../../game_data/server/navmesh/solo_navmesh.bin");List<float[]> array = javaNav.find(new float[]{-107.4f, 0f, 148.5f}, new float[]{-51.7f, 0.2f, 150f}, new float[]{1.f, 1.f, 1.f});

执行结果为:

point 5 array=[[-107.4, 0.3209684, 148.5], [-64.09999, 1.2, 137.40001], [-56.299988, 0.6, 139.8], [-54.5, 0.6, 142.5], [-51.7, 0.4, 150.0]]

与工具生成的一致。
以上是JAVA版本的服务器寻路,如何用JNI制作更高性能的寻路包呢,下一篇文章中会做说明。

如果用工具寻路出来的数据和程序寻路出来的数据一致,说明navmesh寻路成功。
接下去就是动态寻路,优化寻路结果。

相关参考:

ExportSceneToObj
Recast & Detour
将Unity场景(包含物件和地形)导出到.obj文件
Easy3dNav

3D寻路系统NavMesh-服务端篇相关推荐

  1. 互联网酒店预订系统的服务端架构图-4

    互联网酒店预订系统的服务端架构图 1.技术架构说明 2.访问层 3.接入层 4.服务层 5.数据层 说明:儒猿技术窝基于RocketMQ的互联网酒店预订系统项目实战学习笔记 1.技术架构说明 2.访问 ...

  2. 2022-5-1-jjk网络验证系统开源--服务端

    2022-5-1-jjk网络验证系统开源 服务端代码 服务端相关代码 http协议 通讯 .版本 2.程序集 通讯.子程序 初始化服务端, 逻辑型全_通信句柄 = 全_Http服务端.创建 (假) . ...

  3. 【Unity】游戏寻路系统—NavMesh入门(个人翻译)

    以下内容笔者个人翻译自:http://www.theknightsofunity.com/ 游戏中的许多角色经常需要绕过关卡种中的障碍物.正是因为这是一个游戏中非常常见的情形,Unity提供了内置的寻 ...

  4. 物联网视频监控服务(三)-监控服务端 篇

    概述 此篇文章主要描述 监控服务端(video_server) 开发部分; 功能点 接收视频上传功能; 利用opencv动态检测 视频帧是否变化,标记变更部分及显示当前时间; 根据被监测环境是否变化( ...

  5. oracle11g服务端配置实例,Win7系统Oracle11g服务端和客户端连接数据库配置

    A连接本地数据库 1)服务端配置 创建一个监听程序 a)开始-Oracle-OraDb11g_home1-配置和移植工具-Net Manager. b)选中"监听程序",点击&qu ...

  6. tigase mysql_即时通讯之服务端篇Tigase.

    摘要 xmpp是一个应用层协议,主要做网页版即时通讯,是基于RFC3920和RFC3921实现,服务端常见的框架有openfire,tigase,prosody等,这三个都亲自踩过坑,唯有tigase ...

  7. 后台服务器和安卓系统,在线答题系统(服务端,客户端包括web和android)、( 一)...

    最近在做练习,练习的内容就是做一个在线答题系统,主要内容包括后台开发,前台展示,前台又包括了web端和android端,现在将写的系统分享出来. 先放上后台的成果图 科目管理.png 题目和选项管理. ...

  8. unix课设购物系统基于服务端和客户端

    前言 伴随着Internet的蓬勃发展,网络购物中心作为电子商务的一种形式正以其高效.低成本的优势,逐步成为新兴的经营模式和理念,人们已经不再满足用途信息的浏览和发布,而是渴望着能够充分享受网络所带来 ...

  9. 传奇服务器充值系统,传奇服务端会员系统COM引擎传奇增加会员系统的方法

    会员系统主要用来对一些加盟玩家的特殊控制.可以用IP地址,登录帐号,角色名,之类的为标志控制. 相关脚本命令 设置会员类型:SETMEMBERTYPE 设置会员等级:SETMEMBERLEVEL 检查 ...

最新文章

  1. 《深入java虚拟机》读书笔记类加载
  2. 用ul和li实现表格table效果 (转)
  3. JQuery 自动触发 a 标签的 click事件
  4. php 5.5.38 ldap安装,centos5.5系统下面,lnmp环境下面php加载ldap扩展
  5. Spring Security使用数据库管理资源整理
  6. 2021年最值得推荐的报表工具,无代码轻松实现可视化开发
  7. random是python标准库吗_python标准库介绍——27 random 模块详解
  8. codeforces 496 E. Distributing Parts(贪心+set二分)
  9. scratch制作彩虹猫病毒模拟器
  10. java基本数据类型
  11. html饼图显示百分比,Excel饼图中既显示百分比又显示数量的方法
  12. reg51 reg52区别
  13. 金融行业市场策划案例(共12份)
  14. IP协议详解之头部结构
  15. 云服务器什么配置才够用?
  16. Cocos Creator 随笔(1)-- 世界坐标,相对坐标转换问题
  17. 【学校实验】编码实现一个地址簿类(AddressBook),通过getAllInfo()输出,实现另一个类(TestAddressBook)该类存有主方法
  18. 【欣赏】logo设计原则 + 一组设计独特的logo欣赏
  19. kafka 命令重新启动_命令行基础知识:关闭和重新启动
  20. Java比较两个对象是否相同并获取值不同的属性

热门文章

  1. nuxt.js 打包上线
  2. Android Framework内部启动流程
  3. Pipeline aggregations管道聚合-Sibling-1
  4. ctfshow学习记录-misc入门(图片篇-文件结构45-49)
  5. 阿里面试官为什么面试狂问 Redis,把我问到哑口无言……
  6. 深度学习 蜜蜂数据集下载(Hymenoptera)
  7. jest测试vuex
  8. 数据结构树-->B树
  9. 平台千万级订单推送实现
  10. 2016年上半年总结