这篇文章将讨论如何在一个自定义的地面网格上进行简单的2D寻路,以及确定路径后如何使用基于物理引擎的运动方式使物体沿路径到达目标地点。读者需要预先对WebGL和Babylonjs知识有一些了解,可以参考我录制的WebGL入门视频教程和翻译的官方入门文档,当然也可以用自己喜欢的其他方式来学习。
文章主要分成如下几部分:
1、自定义地面网格与寻路矩阵
2、生成Babylon格式3D模型
3、使用pathfinding库进行2D寻路
4、基于cannon.js物理引擎使物体沿路径移动
场景可以通过http://ljzc002.github.io/FPS3/index.html访问,完整代码可以在https://github.com/ljzc002/ljzc002.github.io查看。
场景如下图:
使用WASD控制自由相机位置,移动鼠标控制视角,右键点击地面会在地面上放置一个“目标方块”,然后标有“农民”标志的小球会向目标方块移动。
场景中使用了2DA*寻路算法,如下图所示:
当目标方块位于障碍的另一边时,农民会尽量寻找最短的路径绕开障碍物前往目标方块。
1、自定义地面网格与寻路矩阵:
a、在Babylon.js渲染引擎中自定义地面网格
1 var vdata_ground=new BABYLON.VertexData.CreateGround({198,height:198,subdivisionsX:99,subdivisionsY:99});//分成了99段,一条边上100个顶点 2 var arr_vposition=vdata_ground.positions; 3 var map=[]; 4 var len=arr_vposition.length/3;//和挨个遍历比起来,似乎有目的的去找更好 5 for(var i=20;i<81;i++)//对于这个范围内的行 6 { 7 for(var j=20;j<23;j++)//对于这个范围内的列 8 { 9 arr_vposition[(j+100*i)*3+1]=1; 10 } 11 } 12 for(var i=20;i<81;i++)//对于这个范围内的行 13 { 14 for(var j=40;j<43;j++)//对于这个范围内的列 15 { 16 arr_vposition[(j+100*i)*3+1]=2; 17 } 18 } 19 for(var i=20;i<81;i++)//对于这个范围内的行 20 { 21 for(var j=60;j<63;j++)//对于这个范围内的列 22 { 23 arr_vposition[(j+100*i)*3+1]=4; 24 } 25 } 26 BABYLON.VertexData._ComputeSides(0, arr_vposition, vdata_ground.indices, vdata_ground.normals 27 , vdata_ground.uvs); 28 var mesh_ground=new BABYLON.Mesh("mesh_ground",scene); 29 mesh_ground.renderingGroupId=2; 30 vdata_ground.applyToMesh(mesh_ground, true);
第一行建立了一个Ground类型的Babylonjs“顶点数据”对象,这个对象包含了建立地面网格所需的顶点位置、法线、纹理坐标、顶点索引数据(建议读者亲自用调试模式看一下这个对象的结构),构造函数中的两个198表示地面的长宽是198,两个99表示每一条边被分为99段(由100个顶点组成,每两个顶点之间的距离为2),至于为什么设置为99段后文会有说明,这时的地面网格如果渲染出来将是一个平面。
要让地面变得凹凸不平有两种思路:在高度变化的点的比例较大时,可以尝试对每个顶点进行遍历,然后按照某种规则改变顶点的高度;在比例较小时建议直接在缓存数组中找到这些顶点进行改变,显然后者速度更快。
第26行根据顶点数据对每个面的正反进行计算,在对网格的顶点信息进行修改后一般都要执行这一条语句,而另一条经常在它之前执行的语句是:“
BABYLON.VertexData.ComputeNormals(positions, indices, normals);
”,它的作用是在顶点数据变化后重新计算法线方向。这里不执行这条语句的原因是Babylonjs中的地面网格是一种“简化”的网格
如图所示:
同样表示两个方块,简化的方式使用六个顶点,顶点之间的片元数据由顶点数据插值而成,因为左边的方块和右边的方块共用了两个顶点,所以这两个方块的法线方向和纹理坐标必定是连续的。而用非简化的方式表示这两个方块,则需要使用八个顶点,缺点是增加了对性能的消耗,优点是法线方向和纹理坐标不必连续,可以进行截然不同的变化。
因为采用了简化的方式,地面网格的同一个顶点处于多个不同的平面中,使用ComputeNormals计算地面网格的顶点的法线方向也就失去了意义,事实上Babylonjs把地面网格顶点的法线方向都默认为竖直向上。
后面的代码建立了一个空的网格,将网格的渲染组设为2,将顶点数据交给这个空网格对象。
生成的网格如下图所示:
b、建立2D寻路矩阵
在寻路场景中使用的pathfinding库需要用一个矩阵(二维数组)来定义障碍物的位置,其中零元素表示这个地块可以通行,不为零的元素表示无法通行,下面是建立这个数组的方法:
1 mesh_ground.mydata={}; 2 mesh_ground.mydata.walkabilityMatrix=MakewalkabilityMatrix(arr_vposition,99,99,2); 3 mesh_ground.mydata.len_x=99; 4 mesh_ground.mydata.len_y=99; 5 mesh_ground.mydata.len_s=2; 6 7 。 8 。 9 。 10 11 //对每个正方形区块的倾斜程度进行计算,得出是否可以通行 12 //顶点数据,寻路空间宽度,寻路空间高度,每个方格区域的边长 13 function MakewalkabilityMatrix(arr,len_x,len_y,len_s) 14 { 15 var arr_Matrix=numeric.rep([len_y,len_x],0); 16 var len_s2=len_s*0.707;//求得平均点到其中一个边线点的水平距离0.7071067811865476 17 //var len_s2a=len_s2; 18 //var len_s2b=len_s2; 19 //var len_s2c=len_s2; 20 for(var i=0;i<len_y;i++)//对于每一行寻路单元格 21 { 22 for(var j=0;j<len_x;j++)//对于这一行里的每一个单元格 23 { 24 var int1=j+i*(len_x+1); 25 var int2=j+i*(len_x+1)+1; 26 var int3=j+(i+1)*(len_x+1); 27 var int4=j+(i+1)*(len_x+1)+1; 28 var y1=arr[int1*3+1]; 29 var y2=arr[int2*3+1]; 30 var y3=arr[int3*3+1]; 31 var y4=arr[int4*3+1]; 32 var ya=(y1+y2+y3+y4)/4; 33 var yb=Math.max(Math.abs(y1-ya),Math.abs(y2-ya),Math.abs(y3-ya),Math.abs(y4-ya)); 34 arr_Matrix[i][j]=parseInt((yb)/0.707);//高度超过了水平距离几倍就认为是几倍的障碍物 35 } 36 } 37 return arr_Matrix; 38 }
这一段代码的思路是:将地面网格垂直方向的正投影作为寻路单元格,取每一个寻路单元格的四个顶点,算出这四个顶点的平均高度与每个顶点高度的差的最大值与寻路单元格中心点到顶点的水平距离的比,将这个比值作为“障碍程度”(简单来说就是顶点高度变化的越剧烈,障碍就越难跨越)。然后为网格添加一个mydata属性(JavaScript语言的优势),把和寻路矩阵有关的信息放到这个属性里。
这里用到了numeric数学库,可以在http://www.numericjs.com/查看文档。
2、生成babylon格式的3D模型:
网格生成完毕后需要拿到其他程序中使用,这里我选择把它保存为babylon格式的3D模型,babylon是一种json字符串模型文件,其优点是结构简单功能全面。
Babylon.js的官方网站上有完整的格式说明和例子:http://doc.babylonjs.com/generals/file_format_map_(.babylon),遗憾的是例子里的一行少了一个逗号所以会导致导入出错,不知道现在改正了没有。
以下是生成对应json的代码:
1 /** 2 * Created by Administrator on 2017/7/14. 3 */ 4 function Export_mesh(arr_mesh,PngName)//用Babylon格式导出模型 5 { 6 //场景对象 7 var obj_scene= 8 { 9 'autoClear': true, 10 'clearColor': [0,0,0], 11 'ambientColor': [0,0,0], 12 'gravity': [0,-9.81,0], 13 'cameras': [{ 14 'name': 'Camera', 15 'id': 'Camera', 16 'position': [7.4811,5.3437,-6.5076], 17 'target': [-0.3174,0.8953,0.3125], 18 'fov': 0.8576, 19 'minZ': 0.1, 20 'maxZ': 100, 21 'speed': 1, 22 'inertia': 0.9, 23 'checkCollisions': false, 24 'applyGravity': false, 25 'ellipsoid': [0.2,0.9,0.2] 26 }], 27 'activeCamera': 'Camera', 28 'lights': [{ 29 'name': 'Sun', 30 'id': 'Sun', 31 'type': 1, 32 'position': [0.926,7.3608,14.1829], 33 'direction': [-0.347,-0.4916,-0.7987], 34 'intensity': 1, 35 'diffuse': [1,1,1], 36 'specular': [1,1,1] 37 }], 38 'materials':[{ 39 'name': 'mball', 40 'id': 'mball', 41 'ambient': [1,1,1], 42 'diffuse': [1,1,1], 43 'specular': [1,1,1], 44 'specularPower': 50, 45 'emissive': [0,0,0], 46 'alpha': 1, 47 'backFaceCulling': true, 48 'diffuseTexture': { 49 'name': PngName?PngName:'snow2.jpg', 50 'level': 1, 51 'hasAlpha': 1, 52 'coordinatesMode': 0, 53 'uOffset': 0, 54 'vOffset': 0, 55 'uScale': 1, 56 'vScale': 1, 57 'uAng': 0, 58 'vAng': 0, 59 'wAng': 0, 60 'wrapU': true, 61 'wrapV': true, 62 'coordinatesIndex': 0 63 } 64 }], 65 'geometries': {}, 66 'meshes': [], 67 'multiMaterials': [], 68 'shadowGenerators': [], 69 'skeletons': [], 70 'sounds': [], 71 'mydata':{'walkabilityMatrix':[]} 72 }; 73 //所有模型组件的父物体 74 var obj_allbase= 75 { 76 'name': 'allbase', 77 'id': 'allbase', 78 'materialId': 'mball', 79 'position': [0,0,0], 80 'rotation': [0,0,0], 81 'scaling': [1,1,1], 82 'isVisible': true, 83 'isEnabled': true, 84 'checkCollisions': false, 85 'billboardMode': 0, 86 'receiveShadows': true, 87 'positions': [], 88 'normals': [], 89 'uvs': [], 90 'indices': [], 91 'subMeshes': [{ 92 'materialIndex': 0, 93 'verticesStart': 0, 94 'verticesCount': 0, 95 'indexStart': 0, 96 'indexCount': 0 97 }] 98 }; 99 obj_scene.meshes.push(obj_allbase); 100 var len=arr_mesh.length; 101 var all_x=0; 102 var all_y=0; 103 var all_z=0; 104 for(var i=0;i<len;i++) 105 { 106 var obj_child={}; 107 if(arr_mesh[i].geometry._vertexBuffers!=null) 108 { 109 var child=arr_mesh[i]; 110 if(!child.mydata) 111 { 112 child.mydata={} 113 } 114 var vb=child.geometry._vertexBuffers; 115 all_x+=child.position.x; 116 all_y+=child.position.y; 117 all_z+=child.position.z; 118 obj_child= 119 { 120 'name': child.name, 121 'id': child.id, 122 'parentID': 'allbase', 123 'materialId': 'mball', 124 'position': [child.position.x,child.position.y,child.position.z], 125 'rotation': [child.rotation.x,child.rotation.y,child.rotation.z], 126 'scaling': [child.scaling.x,child.scaling.y,child.scaling.z], 127 'isVisible': true, 128 'isEnabled': true, 129 'checkCollisions': false, 130 'billboardMode': 0, 131 'receiveShadows': true, 132 'positions': vb.position._buffer._data, 133 'normals': vb.normal._buffer._data, 134 'uvs': vb.uv._buffer._data, 135 'indices': child.geometry._indices, 136 'subMeshes': [{ 137 'materialIndex': 0, 138 'verticesStart': 0, 139 'verticesCount': vb.position._buffer._data.length, 140 'indexStart': 0, 141 'indexCount': child.geometry._indices.length 142 }], 143 'mydata':child.mydata 144 }; 145 obj_scene.meshes.push(obj_child); 146 } 147 } 148 //不能让模型的主体过于偏离模型的中心 149 all_x=all_x/len; 150 all_y=all_y/len; 151 all_z=all_z/len; 152 for(var i=1;i<len+1;i++) 153 { 154 obj_scene.meshes[i].position[0]-=all_x; 155 obj_scene.meshes[i].position[1]-=all_y; 156 obj_scene.meshes[i].position[2]-=all_z; 157 } 158 var str_data=JSON.stringify(obj_scene); 159 DownloadText(MakeDateStr()+"testscene",str_data,".babylon"); 160 }
可以看出,一个babylon文件可以包含多个网格对象,除了网格对象之外这个模型文件还可以存储场景、光照、相机、动画、骨骼等信息,这些功能可以选择性使用。方法的最后使用DownloadText方法将json文本导出,DownloadText是我参考网络资料编写的字符下载方法,如果不使用DownloadText,直接在Chrome浏览器的调试模式下的命令行里输入“console.log(str_data)”也能得到json字符串。
DownloadText内容如下:
1 /** 2 * Created by Administrator on 2015/3/2. 3 */ 4 /** 5 * 将指定字符写入指定名称的文本文件中,并可以选择本地保存目录,兼容IE11和谷歌浏览器 6 */ 7 function DownloadText(filename,content,filetype) 8 { 9 if(filetype==null) 10 { 11 filetype=".txt"; 12 } 13 if(document.createElement("a").download!=null)//谷歌和火狐 14 { 15 var aLink = document.createElement('a'); 16 var datatype="data:text/plain;charset=UTF-8,"; 17 if(filetype==".xml") 18 { 19 datatype="data:text/xml;charset=UTF-8,"; 20 } 21 if(filetype==".babylon") 22 {//浏览器还没有支持babylon的mime类型!! 23 datatype="data:text/plain;charset=UTF-8,"; 24 } 25 if(filetype==".png"||filetype==".jpeg") 26 { 27 datatype=""; 28 } 29 if(content.length<1000000) 30 { 31 aLink.href = datatype+content;//dataurl格式的字符串" 32 } 33 else 34 {//对于过大的文件普通dataURL不支持,所以使用“二进制流大对象” 35 aLink.href=URL.createObjectURL(new Blob([content],{type:"text/plain"})); 36 } 37 aLink.download = filename; 38 aLink.innerHTML=filename; 39 //aLink.setAttribute("onclick",""); 40 aLink.onclick=function() 41 { 42 document.getElementById("div_choose").style.display="none"; 43 //delete_div('div_choose'); 44 delete_div('div_mask'); 45 } 46 //aLink.style.display="none"; 47 //document.body.appendChild(aLink); 48 /*var evt = document.createEvent("HTMLEvents");//建立一个事件 49 evt.initEvent("click", false, false);//这是一个单击事件 50 evt.eventType = 'message'; 51 aLink.dispatchEvent(evt);//触发事件*/ 52 //chrome认为点击超链接下载文件是超链接标签的“默认属性”,谷歌认为默认属性不可以用脚本来触发,所以从M53版本开始dispatchEvent无法触发超链接下载 53 //window.open(datatype+content, "_blank"); 54 //document.write(datatype+content); 55 delete_div('div_choose'); 56 delete_div('div_mask'); 57 var evt=evt||window.event; 58 cancelPropagation(evt); 59 var obj=evt.currentTarget?evt.currentTarget:evt.srcElement; 60 61 Open_div("", "div_choose", 240, 180, 400, 80, "", "",1,401);//打开一个带遮罩的弹出框 62 var div_choose=$("#div_choose")[0]; 63 div_choose.style.border="1px solid"; 64 div_choose.innerHTML="<span>谷歌浏览器专用文件生成完毕,请点击下面的文件名下载文件。</span><br>" 65 div_choose.appendChild(aLink); 66 drag(div_choose);//让弹出框可以被拖拽 67 aLink.onmousedown=function() 68 { 69 var evt=evt||window.event; 70 cancelPropagation(evt); 71 } 72 } 73 else//IE 74 { 75 var Folder=BrowseFolder(); 76 if(Folder=="false") 77 { 78 alert("保存失败!"); 79 } 80 else 81 { 82 var fso, tf; 83 fso = new ActiveXObject("Scripting.FileSystemObject");//创建文件系统对象 84 tf = fso.CreateTextFile(Folder + filename+filetype, true,true);//创建一个文件 85 tf.write(content); 86 tf.Close(); 87 alert("保存完毕!"); 88 } 89 } 90 } 91 function BrowseFolder() 92 {//使用ActiveX控件 93 try 94 { 95 var Message = "请选择保存文件夹"; //选择框提示信息 96 var Shell = new ActiveXObject( "Shell.Application" ); 97 var Folder = Shell.BrowseForFolder(0,Message,0x0040,0x11);//起始目录为:我的电脑 98 //var Folder = Shell.BrowseForFolder(0,Message,0); //起始目录为:桌面//选择桌面会报错!! 99 100 if(Folder != null) 101 { 102 Folder = Folder.items(); // 返回 FolderItems 对象 103 Folder = Folder.item(); // 返回 Folderitem 对象 104 Folder = Folder.Path; // 返回路径 105 if(Folder.charAt(Folder.length-1) != "\") 106 { 107 Folder = Folder + "\"; 108 } 109 //document.all.savePath.value=Folder; 110 return Folder; 111 } 112 } 113 catch(e) 114 { 115 return "false"; 116 alert(e.message); 117 } 118 }
接下来,我们要在另一个程序中使用上面生成的模型文件,使用Babylonjs的资源管理器加载网格:
1 this.loader = new BABYLON.AssetsManager(this.scene);//资源管理器 2 3 // 资源数组 4 this.assets = {}; 5 //为资源管理器分配一个任务 6 var meshTask = this.loader.addMeshTask("gun", "", "./assets/", "gun.babylon"); 7 meshTask.onSuccess = function(task) {//这个任务完成 8 _this._initMesh(task); 9 };
//第一个参数表示task的name,第二个参数表示加载模型文件中的哪个网格,为空则用数组形式加载全部,第三个参数表示路径,第四个参数是文件名 10 var meshTask2 = this.loader.addMeshTask("mesh_ground", "", "./assets/arena/", "2017810_14_12_59testscene.babylon"); 11 meshTask2.onSuccess = function(task) { 12 _this._initMesh(task); 13 }; 14 15 this.loader.onFinish = function (tasks)//所有任务完成 16 { 17 。。。 18 } 19 20 。 21 。 22 。 23 24 _initMesh : function(task) 25 { 26 this.assets[task.name] = task.loadedMeshes; 27 for (var i=0; i<task.loadedMeshes.length; i++ ){ 28 var mesh = task.loadedMeshes[i]; 29 mesh.isVisible = false; 30 //预先把所有资源加载下来,但不显示,当需要时再把它显示在需要的位置,或者在需要的位置,建立一个资源的实例(克隆) 31 } 32 } 33
这时,会发生一个小问题:Babylonjs并不支持我们夹带在mesh中的mydata属性。解决方法是在babylon.30.all.max.js的21272行附近修改:
1 if (parsedMesh.metadata !== undefined) { 2 mesh.metadata = parsedMesh.metadata; 3 } 4 if (parsedMesh.mydata !== undefined) { 5 mesh.mydata = parsedMesh.mydata; 6 }
仿照metadata的写法加上对mydata的支持,当然,也可以考虑把mydata夹带到其他被mesh所支持的属性里。
3、使用pathfinding库进行2D寻路
pathfindingjs是一个开源2D寻路库,可以在https://github.com/qiao/PathFinding.js下载完整代码和文档,可以在http://qiao.github.io/PathFinding.js/visual/在线试验各种寻路方式
pathfinding的基本用法如下:
1 var finder = new PF.AStarFinder({//“寻路器” 2 diagonalMovement: 3 3 }); 4 5 。 6 this.grid=new PF.Grid(this.len_x,this.len_y,this.walkabilityMatrix);//生成寻路网格 7 。
8 9 function FindWaytogo(pickResult)//pickResult是Babylonjs中定义的“鼠标选取结果”对象 10 { 11 var faceId=pickResult.faceId;//点击了网格中的第几个面 12 var pickedMesh=pickResult.pickedMesh;//被点击的网格 13 var px=MyGame.player.mesh.position.x;//被控对象在场景中的水平位置 14 var py=MyGame.player.mesh.position.z; 15 16 var len_x=MyGame.arena.len_x;//寻路网格的格数和每格的长度 17 var len_y=MyGame.arena.len_y; 18 var len_s=MyGame.arena.len_s; 19 if(px>-len_x*len_s/2&&px<len_x*len_s/2&&py>-len_y*len_s/2&&py<len_y*len_s/2&&MyGame.arena.grid)//如果使用了pathfinder的障碍矩阵 20 { 21 var arr_matrix=MyGame.arena.walkabilityMatrix;//寻路矩阵 22 var count=parseInt(faceId/2);//第几个方格 23 //接下来要把网格的面转换为寻路方格的坐标,后面还要把寻路方格的坐标转换为scene中的位置 24 //面数转换为方格坐标 25 var count_y=parseInt(count/len_x); 26 var count_x=count%len_x; 27 //场景坐标转化为方格坐标 28 var count_x0=parseInt(px/len_s+len_x/2); 29 var count_y0=parseInt(-py/len_s+len_y/2); 30 31 //寻路,返回一个由方格坐标组成的数组 32 var path = finder.findPath(count_x0,count_y0 , count_x,count_y, MyGame.arena.grid.clone());//这些是寻路网格坐标 33 var len=path.length; 34 for(var i=0;i<len;i++) 35 {//把方格坐标转化为场景坐标 36 var obj=path[i]; 37 obj[0]=(obj[0]-len_x/2)*len_s; 38 obj[1]=(-obj[1]+len_y/2)*len_s; 39 } 40 41 path.push([pickResult.pickedPoint.x,pickResult.pickedPoint.z]); 42 MyGame.player.path_goto=path;//在使用时在生成高度 43 MyGame.player.positiontogo=[pickResult.pickedPoint.x,pickResult.pickedPoint.z]; 44 path.shift();//把第一个出发节点去掉 45 console.log("生成路径,起点:["+px+","+py+"],终点:["+pickResult.pickedPoint.x+","+pickResult.pickedPoint.z+"]"); 46 } 47 }
这样,我们就把场景中的位置对应成了寻路网格中的位置,然后使用pathfinding生成了2D路径。需要注意的是pathfinding中的grid对象只能使用一次,再次寻路时需要重新生成grid或者使用grid的克隆对象。
4、基于cannon.js物理引擎沿路径移动
接下来需要让被控物体沿着指定的路径运动,为了能让物体在凹凸不平的地形中运动时保持紧贴地面,我在这里使用了cannonjs物理引擎(关于物理引擎的用法可以参考上一篇文章)。经过试验,这个版本的cannonjs的单个物理仿真器最多支持对10000个顶点的物理仿真,所以前文没法把地面网格分成更多段。
我们在这个场景中监听“右键点击地面”的事件,代码如下:
1 canvas.addEventListener("click", function(evt) { 2 var width = engine.getRenderWidth(); 3 var height = engine.getRenderHeight(); 4 var pickInfo = scene.pick(width/2, height/2, null, false, _this.camera);//点击信息 5 if(evt.button==2)//右键单击 6 { 7 cancelEvent(evt);//阻止默认响应 8 if(pickInfo.hit&&pickInfo.pickedMesh.name=="mesh_ground")//点击到了地面上 9 { 10 MyGame.player.mesh.physicsImpostor.setMass(70);//给被控物体赋予质量,这样它才可以下落 11 FindWaytogo(pickInfo);//在玩家到点击目的地之间找到一条路径 12 var mesh_togo=BABYLON.Mesh.CreateBox("box", 1, scene);//目标方块 13 mesh_togo.position = pickInfo.pickedPoint.clone();//pickResult.pickedPoint 14 mesh_togo.renderingGroupId=2; 15 MyGame.player.mesh_togo=mesh_togo; 16 } 17 } 18 19 20 21 }, false);
然后在每次渲染之前执行以下运动方法:
1 scene.registerBeforeRender(function() { 2 if(MyGame.flag_startr==1)//如果开始渲染了 3 { 4 if(MyGame.flag_view=="first"||MyGame.flag_view=="third") 5 { 6 physics20170725(MyGame.player); 7 } 8 if(MyGame.flag_view=="free") 9 { 10 pathgoto20170808(MyGame.player); 11 } 12 } 13 });
1 function pathgoto20170808(obj)//obj是player 2 { 3 if(true) 4 //if(obj.standonTheGround==1)//站在地面上时考虑将质量设为0? 5 { 6 if(obj.path_goto!="sleep"&&obj.path_goto!="lose") 7 { 8 var len_x=MyGame.arena.len_x; 9 var len_y=MyGame.arena.len_y; 10 var len_s=MyGame.arena.len_s; 11 var vl_now=obj.mesh.physicsImpostor.getLinearVelocity(); 12 13 if(obj.path_goto.length>0) 14 { 15 var px=obj.mesh.position.x;//全是场景坐标!! 16 var py=obj.mesh.position.z; 17 var count_x0=px; 18 var count_y0=py; 19 var count_x=obj.path_goto[0][0]; 20 var count_y=obj.path_goto[0][1]; 21 var len=obj.path_goto.length; 22 var count_x2=obj.path_goto[len-1][0]; 23 var count_y2=obj.path_goto[len-1][1]; 24 var y_obj=obj.mesh.position.y; 25 if((Math.pow(count_x0-count_x2,2)+Math.pow(count_y0-count_y2,2))<0.25*len_s*len_s) 26 {//在移动过程中因未知原因跳到距终点0.5以内距离的地方,直接寻找最终点 27 console.log("在最终格内"); 28 if((Math.pow(count_x0-count_x2,2)+Math.pow(count_y0-count_y2,2))<0.01*len_s*len_s)//到达0.1距离以内的地方,认为到达最终目标,直接定位 29 { 30 31 obj.mesh.position.x=count_x; 32 obj.mesh.position.z=count_y; 33 obj.path_goto="sleep"; 34 console.log("到达最终目标:["+obj.mesh.position.x+","+obj.mesh.position.y+","+obj.mesh.position.z+"]"); 35 obj.mesh.physicsImpostor.setMass(0);//质量设为零将不会下落 36 obj.mesh_togo.dispose(); 37 obj.mesh.physicsImpostor.setAngularVelocity(new BABYLON.Vector3(0,0,0));//停下 38 obj.mesh.physicsImpostor.setLinearVelocity(new BABYLON.Vector3(0,0,0)); 39 40 } 41 else{ 42 var v_temp=new BABYLON.Vector3(count_x2,0,count_y2).subtract(new BABYLON.Vector3(obj.mesh.position.x,0,obj.mesh.position.z)).normalize().scaleInPlace(obj.vm.forward); 43 v_temp.y=vl_now.y<=0?vl_now.y:0;//这个单位应该脚踏实地的平稳运动 44 obj.mesh.physicsImpostor.setLinearVelocity(v_temp); 45 //obj.mesh.physicsImpostor.setAngularVelocity(new BABYLON.Vector3(0,0,0));//停下 46 if(obj.path_goto.length>1) 47 { 48 obj.path_goto=[obj.path_goto[len-1]];//只剩一个最终目标 49 } 50 } 51 } 52 else if((Math.pow(count_x0-count_x,2)+Math.pow(count_y0-count_y,2))>4*len_s*len_s) 53 {//在移动过程中因未知原因跳到距下个目标格2以外距离的地方,需要重新寻路,这种计算可能耗时较大,不能每帧执行!! 54 obj.path_goto="lose"; 55 //obj.mesh.physicsImpostor.setMass(0);//不掉落 56 obj.mesh.physicsImpostor.setLinearVelocity(new BABYLON.Vector3(0,0,0)); 57 obj.mesh.physicsImpostor.setAngularVelocity(new BABYLON.Vector3(0,0,0));//停下 58 return false; 59 } 60 else if(obj.path_goto.length>1&&(Math.pow(count_x0-count_x,2)+Math.pow(count_y0-count_y,2))<0.25*len_s*len_s) 61 {//距离下一寻路格足够近,切换下一寻路格 62 63 obj.path_goto.shift(); 64 count_x=obj.path_goto[0][0]; 65 count_y=obj.path_goto[0][1]; 66 console.log("切换下一个寻路单元格:["+count_x+","+count_y+"]"); 67 var v_temp=new BABYLON.Vector3(count_x,0,count_y).subtract(new BABYLON.Vector3(obj.mesh.position.x,0,obj.mesh.position.z)).normalize().scaleInPlace(obj.vm.forward); 68 v_temp.y=vl_now.y<=0?vl_now.y:0; 69 obj.mesh.physicsImpostor.setLinearVelocity(v_temp); 70 //obj.mesh.physicsImpostor.setAngularVelocity(new BABYLON.Vector3(0,0,0));//停下 71 } 72 else//正常向目标寻路格移动 73 { 74 console.log("普通寻路"); 75 var v_temp=new BABYLON.Vector3(count_x,0,count_y).subtract(new BABYLON.Vector3(obj.mesh.position.x,0,obj.mesh.position.z)).normalize().scaleInPlace(obj.vm.forward); 76 v_temp.y=vl_now.y<=0?vl_now.y:0; 77 obj.mesh.physicsImpostor.setLinearVelocity(v_temp); 78 //obj.mesh.physicsImpostor.setAngularVelocity(new BABYLON.Vector3(0,0,0));//停下 79 } 80 81 } 82 } 83 else 84 { 85 //obj.mesh.physicsImpostor.setMass(0);//不掉落 86 } 87 } 88 89 90 91 }
这里分几种可能发生的运动情况(最常见的几种)分别设置被控物体的线速度,使得物体平稳的沿着路径运动,当物体到达目标时将进入sleep状态,当物体偏离路径时将进入lose状态,程序每秒钟检查一下物体是否lose,如果lose则重新寻路(没有测试过):
1 _this.currentframet=new Date().getTime(); 2 _this.DeltaTime=_this.currentframet-_this.lastframet;//取得两帧之间的时间 3 _this.lastframet=_this.currentframet; 4 _this.nohurry+=_this.DeltaTime; 5 if(MyGame&&_this.nohurry>1000)//每一秒进行一次导航修正 6 { 7 _this.nohurry=0; 8 if(_this.player.path_goto=="lose")//发现迷失了路途 9 { 10 console.log("发现迷路,重新规划路径"); 11 var len_x=MyGame.arena.len_x; 12 var len_y=MyGame.arena.len_y; 13 var len_s=MyGame.arena.len_s; 14 //场景坐标转化为方格坐标 15 var count_x0=parseInt(_this.player.mesh.position.x/len_s+len_x/2); 16 var count_y0=parseInt(-_this.player.mesh.position.z/len_s+len_y/2); 17 var count_x=parseInt(_this.player.positiontogo[0]/len_s+len_x/2); 18 var count_y=parseInt(-_this.player.positiontogo[1]/len_s+len_y/2); 19 var path = finder.findPath(count_x0,count_y0 , count_x,count_y, MyGame.arena.grid.clone());//这些是寻路网格坐标 20 var len=path.length; 21 for(var i=0;i<len;i++) 22 {//把方格坐标转化为场景坐标 23 var obj=path[i]; 24 obj[0]=(obj[0]-len_x/2)*len_s; 25 obj[1]=(-obj[1]+len_y/2)*len_s; 26 } 27 path.push(MyGame.player.positiontogo); 28 path.shift();//把第一个出发节点去掉 29 MyGame.player.path_goto=path;//在使用时在生成高度 30 console.log("生成路径,起点:["+_this.player.mesh.position.x+","+_this.player.mesh.position.z+"]" + 31 ",终点:["+_this.player.positiontogo[0]+","+_this.player.positiontogo[1]+"]"); 32 } 33 }
这样,我们就成功的完成了在WebGL场景中寻路的目标,接下来可以尝试修改pathfinding使之能根据不同地形进行加权寻路,以及控制多个单位进行寻路行为。