3D網頁小實驗-基於Babylon.js與recast.js實現RTS式單位控制

ljzc002發表於2021-06-01

一、執行效果

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

地圖中間為凹陷的河道,兩角為突出的高地,高地和低地之間以斜坡通道相連。

水下為沙土材質,沙土材質網格貼合地形,河流材質網格則保持水平。

2、在地圖上隨機放置土黃色小方塊表示可控單位

預設控制為自由相機——滑鼠左鍵拖拽改變視角,上下左右鍵進行移動;按v鍵切換為RTS式控制,視角鎖定為45度俯視,按wasd鍵水平移動相機,滑鼠滾輪調整相機縮放。

3、左鍵拖拽滑鼠產生選框:

 

鬆開滑鼠後,被選中的單位顯示為白色

4、右鍵單擊地圖,選中單位開始向目標地點尋路

白色虛線為單位的預計路徑,可以看到單位貼合地面運動,經由坡道跨越河流,而非直線飛躍。(在長距離導航時發現部分單位的預計路徑沒有顯示,因時間有限尚未仔細除錯)

可以先後為多組單位指定不同的目的地,單位在相遇時會自動繞開對方繼續前進

5、滑鼠左鍵單擊也可以選中單位

 

以上程式碼可從https://github.com/ljzc002/ControlRTS 下載,專案使用傳統html引用css、js形式構建,建議讀者具有Babylon.js基礎知識以及少許ES6知識。

專案結構如下:

 

 

 

二、實現地圖編輯

createmap2.html是地圖編輯程式的入口檔案,推薦閱讀https://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,具體描述見此:https://forum.babylonjs.com/t/questions-about-the-watermaterial/10380/9,所以選用普通水面紋理。

2、處理地面紋理扭曲問題:

在前面提到的部落格文章中,斜坡地塊出現紋理扭曲:

3D網頁小實驗-基於Babylon.js與recast.js實現RTS式單位控制

可見斜坡上的草比平臺上的草更大

這是因為構成斜坡與平臺的三角形,紋理座標尺寸相同但面積不同,用術語說就是“不flat”。Babylon.js官方給出的解決方案是在完成地形變化後,經過“擴充頂點”和“計算UV”兩步,統一將紋理轉變為“flat的”;或者只保留Babylon.js的頂點位置計算功能,用自己計算的uv座標代替Babylon.js自動生成的。而我的決定是參考Babylon.js的“MeshBuilder.CreateRibbon”方法自己編寫“FrameGround.myCreateRibbon2”方法解決此問題,FrameGround.myCreateRibbon2方法在FrameGround2.js檔案中。官方解決方案見此:https://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版本進行群組尋路,可以在這裡檢視官方文件https://doc.babylonjs.com/extensions/crowdNavigation,在這裡檢視中英對照版本https://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計算。

相關文章