3D网页小实验-基于Babylon.js与recast.js实现RTS式单位控制

一、运行效果

1、建立一幅具有地形起伏和不同地貌纹理的地图:

地图中间为凹陷的河道,两角为突出的高地,高地和低地之间以斜坡通道相连。

水下为沙土材质,沙土材质网格贴合地形,河流材质网格则保持水平。

2、在地图上随机放置土黄色小方块表示可控单位

默认控制为自由相机——鼠标左键拖拽改变视角,上下左右键进行移动;按v键切换为RTS式控制,视角锁定为45度俯视,按wasd键水平移动相机,鼠标滚轮调整相机缩放。

3、左键拖拽鼠标产生选框:

 

松开鼠标后,被选中的单位显示为白色

4、右键单击地图,选中单位开始向目标地点寻路

白色虚线为单位的预计路径,可以看到单位贴合地面运动,经由坡道跨越河流,而非直线飞跃。(在长距离导航时发现部分单位的预计路径没有显示,因时间有限尚未仔细调试)

可以先后为多组单位指定不同的目的地,单位在相遇时会自动绕开对方继续前进

5、鼠标左键单击也可以选中单位

 

以上代码可从//github.com/ljzc002/ControlRTS 下载,项目使用传统html引用css、js形式构建,建议读者具有Babylon.js基础知识以及少许ES6知识。

项目结构如下:

 

 

 

二、实现地图编辑

createmap2.html是地图编辑程序的入口文件,推荐阅读//www.cnblogs.com/ljzc002/p/11105496.html了解在WebGL中建立地形的一些方法,本项目用到了其中的一些思路,这里只介绍不同的地方,重复的部分则不再赘述。地图编辑与RTS控制没有直接关系,对地图编辑不感兴趣的读者可以直接跳到下一章节。

1、入口html中生成地形的代码:

 1 var ground1=new FrameGround();//定义在FrameGround2.js中的“地面类”,负责管理地面的纹理坐标和顶点位置
 2         var obj_p={
 3             name:"ground1",
 4             segs_x:segs_x,
 5             segs_z:segs_z,
 6             size_per_x:size_per_x,
 7             size_per_z:size_per_z,
 8             mat:"mat_grass",
 9         };
10         ground1.init(obj_p);
11         //ground1.TransVertexGradientlyByDistance(new BABYLON.Vector3(0,0,-50),30,[[0,14,15],[15,4,5],[30,0,1]]);
12         obj_ground["ground1"]=ground1;
13 
14         cri();//这是写在command.js文件中的一些全局方法的简写,比如“cri”是全局方法command.RefreshisInArea的简写,
//用来在程序运行时引入额外的代码,这里默认引入的是additionalscript.js,其中包含判断范围的代码。
15 ct2(isInArea1,3);//把在isInArea1范围内的顶点的高度设为3 16 ct2(isInArea2,-3); 17 18 ct3(15,15,-Math.PI/4,6,3,3,0);//在指定位置,按指定水平角度、长度、宽度、高度建立斜坡 19 ct3(70,20,-Math.PI/4,6,3,0,-3); 20 ct3(45,45,-Math.PI/4,6,3,0,-3); 21 ct3(20,70,-Math.PI/4,6,3,0,-3); 22 ct3(85,85,-Math.PI/4,6,3,0,3); 23 ct3(80,30,-Math.PI/4,6,3,-3,0); 24 ct3(55,55,-Math.PI/4,6,3,-3,0); 25 ct3(30,80,-Math.PI/4,6,3,-3,0) 26 27 ground1.MakeLandtype1(function(vec){ 28 if(vec.y<-2) 29 { 30 return true; 31 } 32 },ground1.obj_mat.mat_sand,"ground_sand");//将指定范围内的地面纹理设为“ground_sand” 33      //尝试使用Babylon.js水面反射材质 34 var water = new BABYLON.WaterMaterial("water", scene, new BABYLON.Vector2(1024, 1024)); 35 water.backFaceCulling = true; 36 water.bumpTexture = new BABYLON.Texture("../../ASSETS/IMAGE/LANDTYPE/waterbump.png", scene); 37 water.windForce = -5; 38 water.waveHeight = 0.1; 39 water.bumpHeight = 0.1; 40 water.waveLength = 0.05; 41 water.colorBlendFactor = 0.2; 42 water.addToRenderList(skybox); 43 water.addToRenderList(ground1.ground_base); 44 water.addToRenderList(obj_ground.ground_sand.ground_base); 45 46 ground1.MakeLandtype1(function(vec){ 47 if(vec.y<-0) 48 { 49 return true; 50 } 51 }, 52 ground1.obj_mat.mat_shallowwater//改用普通水面纹理 53 //water,发现水面反射材质存在bug 54 ,"ground_water",true,-2);

此处初始化了frameground对象,并通过“ct2”等“地图编辑方法”设置了地形和地面纹理,值得注意的是这里的地图编辑方法方法既可以写在代码中运行,也可以在程序运行时写在浏览器的控制台中运行,甚至可以使用cri方法随时引入新的地图编辑方法。

“WaterMaterial”是Babylon.js内置的一种水面反射方法,可以用来生成水面倒影和波浪效果,在平面地形上效果较好,但在高低起伏的地形上存在bug,具体描述见此://forum.babylonjs.com/t/questions-about-the-watermaterial/10380/9,所以选用普通水面纹理。

2、处理地面纹理扭曲问题:

在前面提到的博客文章中,斜坡地块出现纹理扭曲:

可见斜坡上的草比平台上的草更大

这是因为构成斜坡与平台的三角形,纹理坐标尺寸相同但面积不同,用术语说就是“不flat”。Babylon.js官方给出的解决方案是在完成地形变化后,经过“扩充顶点”和“计算UV”两步,统一将纹理转变为“flat的”;或者只保留Babylon.js的顶点位置计算功能,用自己计算的uv坐标代替Babylon.js自动生成的。而我的决定是参考Babylon.js的“MeshBuilder.CreateRibbon”方法自己编写“FrameGround.myCreateRibbon2”方法解决此问题,FrameGround.myCreateRibbon2方法在FrameGround2.js文件中。官方解决方案见此://forum.babylonjs.com/t/which-way-should-i-choose-to-make-a-custom-mesh-from-ribbon/10793

3、地形设置完毕后,执行FrameGround.ExportObjGround方法,将地图导出为模型文件“ObjGround20210427.babylon”。

三、建立场景与导航网格

Babylon.js使用Recast寻路引擎的wasm版本进行群组寻路,可以在这里查看官方文档//doc.babylonjs.com/extensions/crowdNavigation,在这里查看中英对照版本//www.cnblogs.com/ljzc002/p/14831648.html(从word复制到博客园时丢失了代码颜色,稍后会在github上传word版本)

个人理解“导航网格”就是把组成场景地形的多个网格的“可到达部分”合并成一个网格,然后计算单位与导航网格的位置关系以确定单位如何移动到目标位置。

TestSlopNav3.html是导航程序的入口文件,这里还是只介绍前面博客未提到的部分

1、程序入口

 1  function webGLStart()
 2     {
 3         initScene();//初始化相机、光照,注意相机初始化中包括拖拽画框的准备工作
 4         initArena();//初始化天空盒、导入的地图模型要使用的材质
 5         //obj_ground={};
 6         InitMouse();//初始化鼠标键盘控制
 7         window.addEventListener("resize", function () {//处理窗口尺寸变化
 8             if (engine) {
 9                 engine.resize();
10                 var width=canvas.width;
11                 var height=canvas.height;
12                 var fov=camera0.fov;//以弧度表示的相机视野角《-这个计算并不准确!!-》尝试改用巨型蒙版方法
13                 camera0.pos_kuangbase=new BABYLON.Vector3(-camera0.dis*Math.tan(fov)
14                     , camera0.dis*Math.tan(fov)*height/width, camera0.dis);
15             }
16         },false);
17      //导入刚才编辑的地图
18         FrameGround.ImportObjGround("../../ASSETS/SCENE/","ObjGround20210427.babylon",webGLStart2,obj_ground,false);
19 
20 
21     }

最初计划通过读取相机的fov属性(相机的水平视角的一半的弧度制表示,Babylon.js默认初始值为0.8)计算选框的位置,但实践中发现Babylon.js在屏幕比例发生变化时将自动修改相机的视角大小,而这一自动修改并不改变相机的fov属性!所以放弃此方法,可在TestSlopNav2.html查看使用fov计算选框位置的代码。

2、initScene方法如下:

 1 function initScene()
 2     {
 3         navigationPlugin = new BABYLON.RecastJSPlugin();
 4         var camera = new BABYLON.FreeCamera("camera1", new BABYLON.Vector3(0, 14, -14), scene);
 5         //camera.setTarget(BABYLON.Vector3.Zero());
 6         camera.rotation.x=Math.PI/4;//需要45度斜向下视角
 7         camera.attachControl(canvas, true);//一开始是默认的自由相机
 8         MyGame.camera0=camera;
 9         camera0=camera;
10         camera.move=0;//沿轴向的移动距离
11         camera.length0=19.8;//14*Math.pow(2,0.5);//相机在(0, 14, -14)位置45度角向下俯视,则相机到海平面的距离为19.8
12         camera.dis=3//相机到框选框的距离
13         camera.path_line_kuang=[new BABYLON.Vector3(0, 14, -14),new BABYLON.Vector3(0, -14, -14)];//线框的路径
14         camera.line_kuang= BABYLON.MeshBuilder.CreateDashedLines("line_kuang"//线框对象
15             , {points: camera.path_line_kuang, updatable: true}, scene);//第一次建立不应有instance!!否则不显示
16         camera.line_kuang.renderingGroupId=3;
17         camera.line_kuang.parent=camera;
18         camera.line_kuang.isVisible=true;//每次通过instance建立虚线都会继承它?
19         camera.mesh_kuang=new BABYLON.Mesh("mesh_kuang");
20 
21         camera0.mesh_kuang0 = new BABYLON.MeshBuilder.CreatePlane("mesh_kuang0"//一个与地面等大的不可见网格,用来接收鼠标事件
22             , {width: 100, height: 100, sideOrientation: BABYLON.Mesh.DOUBLESIDE}, scene);
23         camera0.mesh_kuang0.parent = camera0;
24         camera0.mesh_kuang0.renderiGroupId = 0;//不可见,但要可pick
25         camera0.mesh_kuang0.position.z=3
26         camera0.mesh_kuang0.rotation.x = Math.PI;
27 
28         var light0 = new BABYLON.HemisphericLight("light0", new BABYLON.Vector3(0, 1, 0), scene);
29         light0.diffuse = new BABYLON.Color3(1,1,1);//这道“颜色”是从上向下的,底部收到100%,侧方收到50%,顶部没有
30         light0.specular = new BABYLON.Color3(0,0,0);
31         light0.groundColor = new BABYLON.Color3(1,1,1);//这个与第一道正相反
32         //var light = new BABYLON.HemisphericLight("light1", new BABYLON.Vector3(0, 1, 0), scene);
33         //light.intensity = 0.7;
34     }

这里建立了一个足够大的“蒙板网格”用来接收鼠标拖拽事件

3、鼠标键盘控制方法稍后介绍

4、载入模型后执行webGLStart2方法,建立导航网格(此处代码参考官方文档)

  1 function webGLStart2()
  2     {
  3         arr_ground=[obj_ground["ground1"].ground_base];
  4         var navmeshParameters = {//导航网格的初始化参数
  5             cs: 0.2,
  6             ch: 0.2,
  7             walkableSlopeAngle: 90,
  8             walkableHeight: 1.0,
  9             walkableClimb: 1,
 10             walkableRadius: 1,
 11             maxEdgeLen: 12.,
 12             maxSimplificationError: 1.3,
 13             minRegionArea: 8,
 14             mergeRegionArea: 20,
 15             maxVertsPerPoly: 6,
 16             detailSampleDist: 6,
 17             detailSampleMaxError: 1,
 18         };
 19         navigationPlugin.createNavMesh(arr_ground, navmeshParameters);//建立导航网格
 20         // var navmeshdebug = navigationPlugin.createDebugNavMesh(scene);//这段代码可以把导航网格显示出来
 21         // navmeshdebug.position = new BABYLON.Vector3(0, 0.01, 0);
 22         // navmeshdebug.renderingGroupId=3;
 23         // navmeshdebug.myname="navmeshdebug";
 24         // var matdebug = new BABYLON.StandardMaterial('matdebug', scene);
 25         // matdebug.diffuseColor = new BABYLON.Color3(0.1, 0.2, 1);
 26         // matdebug.alpha = 0.2;
 27         // navmeshdebug.material = matdebug;
 28 
 29 // crowd
 30         var crowd = navigationPlugin.createCrowd(40, 0.1, scene);//建立一个群组,群组容纳单位的上限为40
 31         var i;
 32         var agentParams = {//单位初始化参数
 33             radius: 0.1,
 34             height: 0.2,
 35             maxAcceleration: 4.0,
 36             maxSpeed: 1.0,
 37             collisionQueryRange: 0.5,
 38             pathOptimizationRange: 0.0,
 39             separationWeight: 1.0};
 40 
 41         for (i = 0; i <20; i++) {//在河道右侧建立20个单位
 42             var width = 0.20;
 43             var id="a_"+i;
 44             var agentCube = BABYLON.MeshBuilder.CreateBox("cube_"+id, { size: width, height: width*2 }, scene);
 45             //var targetCube = BABYLON.MeshBuilder.CreateBox("cube", { size: 0.1, height: 0.1 }, scene);
 46             agentCube.renderingGroupId=3;
 47             //targetCube.renderingGroupId=3;
 48             //var matAgent = new BABYLON.StandardMaterial('mat2_'+id, scene);
 49             //var variation = Math.random();
 50             //matAgent.diffuseColor = new BABYLON.Color3(0.4 + variation * 0.6, 0.3, 1.0 - variation * 0.3);
 51             //targetCube.material = matAgent;
 52             agentCube.material = MyGame.materials.mat_sand;
 53             var randomPos = navigationPlugin.getRandomPointAround(new BABYLON.Vector3(20.0, 0.2, 0), 0.5);
 54             var transform = new BABYLON.TransformNode();
 55             //agentCube.parent = transform;
 56             var agentIndex = crowd.addAgent(randomPos, agentParams, transform);
 57             //transform.pathPoints=[transform.position];
 58             var state={//单位的状态
 59                 feeling:"free",
 60                 wanting:"waiting",
 61                 doing:"standing",
 62                 being:"none",
 63             }
 64             var unit={idx:agentIndex, trf:transform, mesh:agentCube, target:new BABYLON.Vector3(20.0, 2.1, 0)
 65                 ,data:{state:state,id:id}};
 66             agentCube.unit=unit
 67             arr_unit.push(unit);//保存所有单位的数组
 68         }
 69         for (i = 0; i <20; i++) {
 70             var width = 0.20;
 71             var id="b_"+i;
 72             var agentCube = BABYLON.MeshBuilder.CreateBox("cube_"+id, { size: width, height: width*2 }, scene);
 73             //var targetCube = BABYLON.MeshBuilder.CreateBox("cube", { size: 0.1, height: 0.1 }, scene);
 74             agentCube.renderingGroupId=3;
 75             //targetCube.renderingGroupId=3;
 76             //var matAgent = new BABYLON.StandardMaterial('mat2_'+id, scene);
 77             //var variation = Math.random();
 78             //matAgent.diffuseColor = new BABYLON.Color3(0.4 + variation * 0.6, 0.3, 1.0 - variation * 0.3);
 79             //targetCube.material = matAgent;
 80             agentCube.material = MyGame.materials.mat_sand;
 81             var randomPos = navigationPlugin.getRandomPointAround(new BABYLON.Vector3(-20.0, 0.2, 0), 0.5);
 82             var transform = new BABYLON.TransformNode();
 83             //agentCube.parent = transform;
 84             var agentIndex = crowd.addAgent(randomPos, agentParams, transform);
 85             //transform.pathPoints=[transform.position];
 86             var state={
 87                 feeling:"free",
 88                 wanting:"waiting",
 89                 doing:"standing",
 90                 being:"none",
 91             }
 92             var unit={idx:agentIndex, trf:transform, mesh:agentCube, target:new BABYLON.Vector3(20.0, 2.1, 0)
 93                 ,data:{state:state,id:id}};
 94             agentCube.unit=unit;
 95             arr_unit.push(unit);
 96         }
 97         var startingPoint;
 98         var currentMesh;
 99         var pathLine;
100 
101         

5、监听鼠标右键单击:

 1 var startingPoint;
 2         var currentMesh;
 3         var pathLine;
 4 
 5         document.oncontextmenu = function(evt){//右键单击事件
 6             //点击右键后要执行的代码
 7             onContextMenu(evt);
 8             return false;//阻止浏览器的默认弹窗行为
 9         }
10         function onContextMenu(evt)
11         {
12             var pickInfo = scene.pick(scene.pointerX, scene.pointerY,  (mesh)=>(mesh.id!="mesh_kuang0"), false, MyGame.camera0);
13             if(pickInfo.hit)//正常来讲,右键单击会点到我们之前建立的蒙板网格(mesh_kuang0),但因上一行代码中的过滤参数设置,跳过了对蒙板的检测
14             {
15                 var mesh = pickInfo.pickedMesh;
16                 //if(mesh.myname=="navmeshdebug")//这是限制只能点击导航网格
17                 var startingPoint=pickInfo.pickedPoint;//点击的坐标作为目的地
18                 var agents = crowd.getAgents();
19                 var len=arr_selected.length;//对于被选中的每个单位(显示为白色的单位)
20                 var i;
21                 for (i=0;i<len;i++) {//分别指挥被框选中的每个单位
22                     var unit=arr_selected[i];
23                     var agent=agents[unit.idx];
24                     unit.data.state.doing="walking";//修改单位的状态
25                     crowd.agentGoto(agent, navigationPlugin.getClosestPoint(startingPoint));//让每个单位开始向目的地移动
26                     //用agentTeleport方法结束寻路?
27                     var pathPoints=navigationPlugin.computePath(crowd.getAgentPosition(agent), navigationPlugin.getClosestPoint(startingPoint));
28                     unit.lastPoint=pathPoints[0];//保留上一个节点,以对比确定是否要减少路径线的节点数量
29                     pathPoints.unshift(unit.trf.position);//将路径的第一个点,设为运动物体本身
30                     unit.pathPoints=pathPoints;//保存预计路线
31             //根据预计路线绘制虚线
32                     unit.pathLine = BABYLON.MeshBuilder.CreateDashedLines("ribbon_"+unit.idx, {points: unit.pathPoints, updatable: true, instance: unit.pathLine}, scene);
33                     unit.pathLine.renderingGroupId=3;
34                 }
35                 //var pathPoints = navigationPlugin.computePath(crowd.getAgentPosition(agents[0]), navigationPlugin.getClosestPoint(startingPoint));
36                 //pathLine = BABYLON.MeshBuilder.CreateDashedLines("ribbon", {points: pathPoints, updatable: true, instance: pathLine}, scene);
37             }
38 
39         }

6、在每一帧渲染前对表示单位的网格和虚线进行调整

 1 scene.onBeforeRenderObservable.add(()=> {
 2             var len = arr_unit.length;
 3             //var flag_rest=false;//每个运动单位都要有专属的运动结束标志!!!!
 4             for(let i = 0;i<len;i++)//对于场景中的每个单位
 5             {
 6                 var ag = arr_unit[i];//单位,注意单位和“表示单位的网格”是两个概念
 7                 ag.mesh.position = crowd.getAgentPosition(ag.idx);//移动表示单位的网格的位置
 8                 if(ag.data.state.doing=="walking")//如果单位正在走路
 9                 {
10 
11                     let vel = crowd.getAgentVelocity(ag.idx);//当前移动速度
12                     crowd.getAgentNextTargetPathToRef(ag.idx, ag.target);//实时计算下一个将要前往的节点,保存为ag.target
13                     if (vel.length() > 0.2)//开始运动时有一个速度很低的加速阶段?
14                     {
15 
16                         vel.normalize();
17                         var desiredRotation = Math.atan2(vel.x, vel.z);//速度的方向,使网格朝向这一方向
18                         ag.mesh.rotation.y = ag.mesh.rotation.y + (desiredRotation - ag.mesh.rotation.y) * 0.05;
19                         var pos=ag.target;//实时计算的网格正前往的位置
20                         var posl=ag.lastPoint;//上一次计算保存的,网格当前正直线前往的位置(虚线上的下一个顶点)
21                         ag.pathPoints[0]=ag.mesh.position;
22                         //console.log(ag.pathPoints[0],pos);//更新虚线,注意使用了instance属性!
23                         ag.pathLine = BABYLON.MeshBuilder.CreateDashedLines("ribbon_"+ag.idx
24                             , {points: ag.pathPoints, updatable: true, instance: ag.pathLine}, scene);
25                         if(pos&&posl)
26                         {
27                             if(pos.x!=posl.x||pos.y!=posl.y||pos.z!=posl.z)//如果下一导航点发生变化
28                             {
29                                 //console.log(pos,posl);
30                                 ag.pathPoints.splice(1,1);//虚线的顶点减少一个
31                                 ag.lastPoint=ag.pathPoints[1];//更换下一目标点
32                                 //ag.target.position=ag.lastPoint;
33                                 //console.log(ag.pathPoints.length);
34                                 // ag.pathLine = BABYLON.MeshBuilder.CreateDashedLines("ribbon_"+ag.idx
35                                 //     , {points: ag.pathPoints, updatable: true, instance: ag.pathLine}, scene);
36 
37                             }
38                         }
39                         else
40                         {
41                             //console.log(ag);
42 
43                         }
44                     }
45                     else {//如果在一个时间单位(1s?)内的移动距离小于它自身的尺寸
46                         //ag.target=ag.mesh.position;
47                         if (vel.length() <0.01&&ag.pathPoints.length==2)//速度很慢,并且当前的虚线只剩下两个顶点
48                         {
49                             crowd.agentTeleport(ag.idx, ag.mesh.position);
50                             //如果速度太慢,则把单位传送到当前所处的位置,以停止寻路(文档中没有手动停止寻路的方法)-》遇到堵车怎么办?《-目前未遇到
51                             ag.data.state.doing=="standing"//切换单位状态
52                             console.log("单位"+ag.mesh.id+"停止导航")
53                         }
54 
55                     }
56                 }
57 
58             }
59         });

如此我们完成了导航的准备工作

四、RTS式键盘鼠标控制

ControlRTS3.js文件内容如下:

  1 //用于RTS控制的相机-》用大遮罩多层pick代替计算框选位置
  2 var node_temp;
  3 function InitMouse()//初始化事件监听
  4 {
  5     canvas.addEventListener("blur",function(evt){//监听失去焦点
  6         releaseKeyStateOut();
  7     })
  8     canvas.addEventListener("focus",function(evt){//改为监听获得焦点,因为调试失去焦点时事件的先后顺序不好说
  9         releaseKeyStateIn();
 10     })
 11 
 12     //scene.onPointerPick=onMouseClick;//如果不attachControl onPointerPick不会被触发,并且onPointerPick必须pick到mesh上才会被触发
 13     canvas.addEventListener("click", function(evt) {//这个监听也会在点击GUI按钮时触发!!
 14         onMouseClick(evt);//
 15     }, false);
 16     canvas.addEventListener("dblclick", function(evt) {//是否要用到鼠标双击??
 17         onMouseDblClick(evt);//
 18     }, false);
 19     scene.onPointerMove=onMouseMove;
 20     scene.onPointerDown=onMouseDown;
 21     scene.onPointerUp=onMouseUp;
 22     //scene.onKeyDown=onKeyDown;
 23     //scene.onKeyUp=onKeyUp;
 24     window.addEventListener("keydown", onKeyDown, false);//按键按下
 25     window.addEventListener("keyup", onKeyUp, false);//按键抬起
 26     window.onmousewheel=onMouseWheel;//鼠标滚轮滚动
 27     node_temp=new BABYLON.TransformNode("node_temp",scene);//用来提取相机的姿态矩阵(不包括位置的姿态)
 28     node_temp.rotation=camera0.rotation;
 29 
 30     pso_stack=camera0.position.clone();//用来在切换控制方式时保存相机位置
 31 }
 32 function onMouseDblClick(evt)//这段没用
 33 {
 34     var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, camera0);
 35     if(pickInfo.hit)
 36     {
 37         var mesh = pickInfo.pickedMesh;
 38         if(mesh.name.split("_")[0]=="mp4")//重放视频
 39         {
 40             if(obj_videos[mesh.name])
 41             {
 42                 var videoTexture=obj_videos[mesh.name];
 43 
 44                     videoTexture.video.currentTime =0;
 45 
 46             }
 47         }
 48     }
 49 }
 50 function onMouseClick(evt)//鼠标单击
 51 {
 52     if(flag_view=="locked") {
 53         ThrowSomeBall();//没用
 54     }
 55     if(flag_view=="rts"&&evt.button!=2) {//选择了单个单位《-目前是rts控制状态,并且不是右键单击
 56         evt.preventDefault();
 57         var pickInfo = scene.pick(scene.pointerX, scene.pointerY, (mesh)=>(mesh.id.substr(0,5)=="cube_")
 58             , false, camera0);
 59         if(pickInfo.hit)
 60         {
 61             var mesh = pickInfo.pickedMesh;
 62             resetSelected();
 63             mesh.material=MyGame.materials.mat_frame;//改变被选中的单位的显示
 64             arr_selected.push(mesh.unit);//将被选中的单位放到“被选中数组”中
 65         }else
 66         {
 67             resetSelected();
 68         }
 69     }
 70 }
 71 var lastPointerX,lastPointerY;
 72 var flag_view="free"
 73 var obj_keystate=[];
 74 var pso_stack;
 75 var flag_moved=false;//在拖拽模式下有没有移动,如果没移动则等同于click
 76 var point0,point;//拖拽时点下的第一个点与当前移动到的点
 77 function onMouseMove(evt)//鼠标移动响应
 78 {
 79 
 80     if(flag_view=="rts")
 81     {
 82         evt.preventDefault();
 83         if(camera0.line_kuang.isVisible)
 84         {
 85             flag_moved=true;
 86             drawKuang();//画框
 87         }
 88     }
 89     lastPointerX=scene.pointerX;
 90     lastPointerY=scene.pointerY;
 91 }
 92 function drawKuang(){
 93     var m_cam=camera0.getWorldMatrix();
 94     if(!point0)
 95     {//第一次按下鼠标时在蒙板网格上点到的点
 96         var pickInfo0 = scene.pick(downPointerX, downPointerY, (mesh)=>(mesh.id=="mesh_kuang0")
 97             , false, camera0);
 98         if(pickInfo0.hit)
 99         {
100             point0 = pickInfo0.pickedPoint;
101             point0=BABYLON.Vector3.TransformCoordinates(point0,m_cam.clone().invert());//转为相机的局部坐标系中的坐标
102         }
103     }
104     if(point0)
105     {
106         var pickInfo = scene.pick(scene.pointerX, scene.pointerY, (mesh)=>(mesh.id=="mesh_kuang0")
107             , false, camera0);
108         if(pickInfo.hit)
109         {//当前鼠标在蒙板网格上点到的点,根据这两个点绘制一个线框
110             point = pickInfo.pickedPoint ;
111             point=BABYLON.Vector3.TransformCoordinates(point,m_cam.clone().invert());
112             camera0.path_line_kuang=[point0,new BABYLON.Vector3(point.x, point0.y, 3)
113                 ,point,new BABYLON.Vector3(point0.x, point.y, 3),point0];//封口
114             camera0.line_kuang= BABYLON.MeshBuilder.CreateDashedLines("line_kuang"
115                 , {points: camera0.path_line_kuang, updatable: true, instance: camera0.line_kuang}, scene);
116         }
117     }
118 }
119 var downPointerX,downPointerY;
120 function onMouseDown(evt)//鼠标按下响应
121 {
122     if(flag_view=="rts"&&evt.button!=2) {
123         evt.preventDefault();
124         //单选单位的情况放在click中
125         //显示框选框(四条线段围成的矩形)
126         downPointerX=scene.pointerX;
127         downPointerY=scene.pointerY;
128         camera0.line_kuang.isVisible=true;//将线框设为可见
129         drawKuang();
130     }
131 }
132 function onMouseUp(evt)//鼠标抬起响应
133 {
134     if(flag_view=="rts"&&evt.button!=2) {
135         evt.preventDefault();
136         if(camera0.line_kuang.isVisible)
137         {
138             camera0.line_kuang.isVisible=false;//令线框不可见
139             if(flag_moved)
140             {
141                 flag_moved = false;
142           //依靠point0和point,在之前画线框的位置建立一个不可见的平面网格,把它叫做“框网格”
143                 var pos = new BABYLON.Vector3((point0.x + point.x) / 2, (point0.y + point.y) / 2, 3);
144                 var width2 = Math.abs(point0.x - point.x);
145                 var height2 = Math.abs(point0.y - point.y);
146                 if (camera0.mesh_kuang) {
147                     camera0.mesh_kuang.dispose();
148                 }
149                 camera0.mesh_kuang = new BABYLON.MeshBuilder.CreatePlane("mesh_kuang"
150                     , {width: width2, height: height2, sideOrientation: BABYLON.Mesh.DOUBLESIDE}, scene);
151                 camera0.mesh_kuang.parent = camera0;
152                 camera0.mesh_kuang.renderingGroupId = 0;//测试时可见,实际使用时不可见
153                 camera0.mesh_kuang.position = pos;
154                 camera0.mesh_kuang.rotation.x = Math.PI;
155                 //camera0.mesh_kuang.material=MyGame.materials.mat_sand;
156 
157                 //发射射线
158                 resetSelected();//清空当前选中的单位
159                 requestAnimFrame(function(){//这里要延迟到下一帧发射射线,否则框网格还没绘制,射线射不到它
160                     arr_unit.forEach((obj, i) => {//从相机到每个可控单位发射射线
161                         var ray = BABYLON.Ray.CreateNewFromTo(camera0.position, obj.mesh.position);
162                         //console.log(i);
163                         //var pickInfo = scene.pickWithRay(ray, (mesh)=>(mesh.id=="mesh_kuang0"));
164                         var pickInfo = ray.intersectsMesh(camera0.mesh_kuang);//难道是因为网格尚未渲染,所以找不到?
165                         if (pickInfo.hit)//如果相机与物体的连线穿过了框网格,则这个物体应该被选中!
166                         {
167                             obj.mesh.material = MyGame.materials.mat_frame;
168                             arr_selected.push(obj);
169                         }
170                         //ray.dispose();//射线没有这个方法?
171                     })
172                     camera0.mesh_kuang.dispose();//用完后释放掉框网格
173                     camera0.mesh_kuang = null;
174                 })
175 
176             }
177         }
178         point0=null;
179         point=null;
180 
181     }
182 }
183 function onKeyDown(event)//按下按键
184 {
185     if(flag_view=="rts") {
186         event.preventDefault();
187         var key = event.key;
188         obj_keystate[key] = 1;//修改按键状态,
189         if(obj_keystate["Shift"]==1)//注意,按下Shift+w时,event.key的值为W!
190         {
191             obj_keystate[key.toLowerCase()] = 1;
192         }
193     }
194     else {
195         var key = event.key;
196         if(key=='f')
197         {
198             if(DoAni)
199             {
200                 DoAni();
201             }
202         }
203     }
204 }
205 function onKeyUp(event)//键盘按键抬起
206 {
207     var key = event.key;
208     if(key=="v"||key=="Escape")
209     {
210         event.preventDefault();
211         if(flag_view=="rts")//切换为rts控制
212         {
213             flag_view="free";
214             camera0.attachControl(canvas, true);
215             pso_stack=camera0.positions;
216 
217         }
218         else if(flag_view=="free")//切换为自由控制
219         {
220             flag_view="rts";
221             camera0.position= pso_stack;
222             resetCameraRotation(camera0);
223             camera0.detachControl()
224         }
225     }
226     if(flag_view=="rts") {
227         event.preventDefault();
228 
229         obj_keystate[key] = 0;
230         //因为shift+w=W,所以为了避免结束高速运动后,物体仍普速运动
231         obj_keystate[key.toLowerCase()] = 0;
232     }
233 }
234 function onMouseWheel(event){//鼠标滚轮转动响应
235     var delta =event.wheelDelta/120;
236     if(flag_view=="rts")
237     {
238         camera0.move+=delta;
239         if(camera0.move>16.8)//防止相机过于向下
240         {
241             delta=delta-(camera0.move-16.8);//沿着相机指向的方向移动相机
242             camera0.move=16.8;
243         }
244         //camera0.movePOV(0,0,delta);//轴向移动相机?<-mesh有这一方法,但camera没有!!《-所以自己写一个
245         movePOV(node_temp,camera0,new BABYLON.Vector3(0,0,delta));//camera0只能取姿态,不能取位置!!!!
246     }
247 }
248 function movePOV(node,node2,vector3)//将局部坐标系的移动转为全局坐标系的移动,参数:含有姿态矩阵的变换节点、要变换位置的对象、在物体局部坐标系中的移动
249 {
250     var m_view=node.getWorldMatrix();
251     v_delta=BABYLON.Vector3.TransformCoordinates(vector3,m_view);
252     var pos_temp=node2.position.add(v_delta);
253     node2.position=pos_temp;
254 }
255 function resetSelected(){
256     arr_selected.forEach((obj,i)=>{
257         //如果单位选中前后有外观变化,则在这里切换
258         obj.mesh.material=MyGame.materials.mat_sand;
259     });
260     arr_selected=[];
261 }
262 function resetCameraRotation(camera)//重置相机位置
263 {
264     //camera.movePOV(0,0,-camera0.move||0);//轴向移动相机?<-不需要,把转为自由相机前的位置入栈即可
265     //camera.move=0;
266     camera.rotation.x=Math.PI/4;
267     camera.rotation.y=0;
268     camera.rotation.z=0;
269 }
270 function releaseKeyStateIn(evt)
271 {
272     for(var key in obj_keystate)
273     {
274         obj_keystate[key]=0;
275     }
276     lastPointerX=scene.pointerX;
277     lastPointerY=scene.pointerY;
278 
279 }
280 function releaseKeyStateOut(evt)
281 {
282     for(var key in obj_keystate)
283     {
284         obj_keystate[key]=0;
285     }
286     // scene.onPointerMove=null;
287     // scene.onPointerDown=null;
288     // scene.onPointerUp=null;
289     // scene.onKeyDown=null;
290     // scene.onKeyUp=null;
291 }
292 
293 var pos_last;
294 var delta;
295 var v_delta;
296 function MyBeforeRender()
297 {
298     pos_last=camera0.position.clone();
299     scene.registerBeforeRender(
300         function(){
301             //Think();
302 
303         }
304     )
305     scene.registerAfterRender(
306         function() {
307             if(flag_view=="rts")
308             {//rts状态下,相机的位置变化
309                 var flag_speed=2;
310                 //var m_view=camera0.getViewMatrix();
311                 //var m_view=camera0.getProjectionMatrix();
312                 //var m_view=node_temp.getWorldMatrix();
313                 //只检测其运行方向?-》相对论问题!《-先假设直接外围环境不移动
314                 if(obj_keystate["Shift"]==1)//Shift+w的event.key不是Shift和w,而是W!!!!
315                 {
316                     flag_speed=10;
317                 }
318                 delta=engine.getDeltaTime();
319                 //console.log(delta);
320                 flag_speed=flag_speed*engine.getDeltaTime()/10;
321                 var r_cameramove=(camera0.length0-camera0.move)/camera0.length0//相机移动造成的速度变化
322                 if(r_cameramove<0.1)
323                 {
324                     r_cameramove=0.1;
325                 }
326                 if(r_cameramove>5)
327                 {
328                     r_cameramove=5;
329                 }
330                 flag_speed=flag_speed*r_cameramove;
331                 var v_temp=new BABYLON.Vector3(0,0,0);
332                 if(obj_keystate["w"]==1)
333                 {
334                     v_temp.z+=0.1*flag_speed;
335 
336                 }
337                 if(obj_keystate["s"]==1)
338                 {
339                     v_temp.z-=0.1*flag_speed;
340                 }
341                 if(obj_keystate["d"]==1)
342                 {
343                     v_temp.x+=0.1*flag_speed;
344                 }
345                 if(obj_keystate["a"]==1)
346                 {
347                     v_temp.x-=0.1*flag_speed;
348                 }
349                 // if(obj_keystate[" "]==1)
350                 // {
351                 //     v_temp.y+=0.05*flag_speed;
352                 // }
353                 // if(obj_keystate["c"]==1)
354                 // {
355                 //     v_temp.y-=0.05*flag_speed;
356                 // }
357 
358                 //camera0.position=camera0.position.add(BABYLON.Vector3.TransformCoordinates(v_temp,camera0.getWorldMatrix()).subtract(camera0.position));
359                 //engine.getDeltaTime()
360                 //v_delta=BABYLON.Vector3.TransformCoordinates(v_temp,m_view);
361                 var pos_temp=camera0.position.add(v_temp);
362                 camera0.position=pos_temp;
363                 // if(camera0.line_kuang.isVisible)
364                 // {
365                 //     camera0.line_kuang= BABYLON.MeshBuilder.CreateDashedLines("line_kuang"
366                 //         , {points: camera0.path_line_kuang, updatable: true, instance: camera0.line_kuang}, scene);
367                 // }
368             }
369             pos_last=camera0.position.clone();
370         }
371     )
372     engine.runRenderLoop(function () {
373         engine.hideLoadingUI();
374         if (divFps) {
375             divFps.innerHTML = engine.getFps().toFixed() + " fps";
376         }
377         scene.render();
378     });
379 }
380 function sort_compare(a,b)
381 {
382     return a.distance-b.distance;
383 }
384 var requestAnimFrame = (function() {//下一帧,复制自谷歌公司开源代码
385     return window.requestAnimationFrame ||
386         window.webkitRequestAnimationFrame ||
387         window.mozRequestAnimationFrame ||
388         window.oRequestAnimationFrame ||
389         window.msRequestAnimationFrame ||
390         function(/* function FrameRequestCallback */ callback, /* DOMElement Element */ element) {
391             window.setTimeout(callback, 1000/60);
392         };
393 })();

 如此就完成了一个基本的rts控制效果。

五、下一步

让光标移动到不同对象上时显示不同的动画效果,加入ai线程为每个单位添加ai计算。