上一节讲到的客户端使用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; else return 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 labels if (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)); } else imguiDrawText((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)); } else imguiDrawText((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寻路成功。
接下去就是动态寻路,优化寻路结果。
相关参考: