主頁 >  其他 > 3D網頁小實驗-基于Babylon.js與recast.js實作RTS式單位控制

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

2021-06-09 17:50:34 其他

一、運行效果

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、處理地面紋理扭曲問題:

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

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

這是因為構成斜坡與平臺的三角形,紋理坐標尺寸相同但面積不同,用術語說就是“不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         onm ouseClick(evt);//
 15     }, false);
 16     canvas.addEventListener("dblclick", function(evt) {//是否要用到滑鼠雙擊??
 17         onm ouseDblClick(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 onm ouseDblClick(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 onm ouseClick(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 onm ouseMove(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 onm ouseDown(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 onm ouseUp(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 onm ouseWheel(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計算,

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/285785.html

標籤:其他

上一篇:沒有安裝vs通過Rider編譯Dll

下一篇:Unity Editor自定義選單排序(MenuItem Order)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 網閘典型架構簡述

    網閘架構一般分為兩種:三主機的三系統架構網閘和雙主機的2+1架構網閘。 三主機架構分別為內端機、外端機和仲裁機。三機無論從軟體和硬體上均各自獨立。首先從硬體上來看,三機都用各自獨立的主板、記憶體及存盤設備。從軟體上來看,三機有各自獨立的作業系統。這樣能達到完全的三機獨立。對于“2+1”系統,“2”分為 ......

    uj5u.com 2020-09-10 02:00:44 more
  • 如何從xshell上傳檔案到centos linux虛擬機里

    如何從xshell上傳檔案到centos linux虛擬機里及:虛擬機CentOs下執行 yum -y install lrzsz命令,出現錯誤:鏡像無法找到軟體包 前言 一、安裝lrzsz步驟 二、上傳檔案 三、遇到的問題及解決方案 總結 前言 提示:其實很簡單,往虛擬機上安裝一個上傳檔案的工具 ......

    uj5u.com 2020-09-10 02:00:47 more
  • 一、SQLMAP入門

    一、SQLMAP入門 1、判斷是否存在注入 sqlmap.py -u 網址/id=1 id=1不可缺少。當注入點后面的引數大于兩個時。需要加雙引號, sqlmap.py -u "網址/id=1&uid=1" 2、判斷文本中的請求是否存在注入 從文本中加載http請求,SQLMAP可以從一個文本檔案中 ......

    uj5u.com 2020-09-10 02:00:50 more
  • Metasploit 簡單使用教程

    metasploit 簡單使用教程 浩先生, 2020-08-28 16:18:25 分類專欄: kail 網路安全 linux 文章標簽: linux資訊安全 編輯 著作權 metasploit 使用教程 前言 一、Metasploit是什么? 二、準備作業 三、具體步驟 前言 Msfconsole ......

    uj5u.com 2020-09-10 02:00:53 more
  • 游戲逆向之驅動層與用戶層通訊

    驅動層代碼: #pragma once #include <ntifs.h> #define add_code CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS) /* 更多游戲逆向視頻www.yxfzedu.com ......

    uj5u.com 2020-09-10 02:00:56 more
  • 北斗電力時鐘(北斗授時服務器)讓網路資料更精準

    北斗電力時鐘(北斗授時服務器)讓網路資料更精準 北斗電力時鐘(北斗授時服務器)讓網路資料更精準 京準電子科技官微——ahjzsz 近幾年,資訊技術的得了快速發展,互聯網在逐漸普及,其在人們生活和生產中都得到了廣泛應用,并且取得了不錯的應用效果。計算機網路資訊在電力系統中的應用,一方面使電力系統的運行 ......

    uj5u.com 2020-09-10 02:01:03 more
  • 【CTF】CTFHub 技能樹 彩蛋 writeup

    ?碎碎念 CTFHub:https://www.ctfhub.com/ 筆者入門CTF時時剛開始刷的是bugku的舊平臺,后來才有了CTFHub。 感覺不論是網頁UI設計,還是題目質量,賽事跟蹤,工具軟體都做得很不錯。 而且因為獨到的金幣制度的確讓人有一種想去刷題賺金幣的感覺。 個人還是非常喜歡這個 ......

    uj5u.com 2020-09-10 02:04:05 more
  • 02windows基礎操作

    我學到了一下幾點 Windows系統目錄結構與滲透的作用 常見Windows的服務詳解 Windows埠詳解 常用的Windows注冊表詳解 hacker DOS命令詳解(net user / type /md /rd/ dir /cd /net use copy、批處理 等) 利用dos命令制作 ......

    uj5u.com 2020-09-10 02:04:18 more
  • 03.Linux基礎操作

    我學到了以下幾點 01Linux系統介紹02系統安裝,密碼啊破解03Linux常用命令04LAMP 01LINUX windows: win03 8 12 16 19 配置不繁瑣 Linux:redhat,centos(紅帽社區版),Ubuntu server,suse unix:金融機構,證券,銀 ......

    uj5u.com 2020-09-10 02:04:30 more
  • 05HTML

    01HTML介紹 02頭部標簽講解03基礎標簽講解04表單標簽講解 HTML前段語言 js1.了解代碼2.根據代碼 懂得挖掘漏洞 (POST注入/XSS漏洞上傳)3.黑帽seo 白帽seo 客戶網站被黑帽植入劫持代碼如何處理4.熟悉html表單 <html><head><title>TDK標題,描述 ......

    uj5u.com 2020-09-10 02:04:36 more
最新发布
  • 2023年最新微信小程式抓包教程

    01 開門見山 隔一個月發一篇文章,不過分。 首先回顧一下《微信系結手機號資料庫被脫庫事件》,我也是第一時間得知了這個訊息,然后跟蹤了整件事情的經過。下面是這起事件的相關截圖以及近日流出的一萬條資料樣本: 個人認為這件事也沒什么,還不如關注一下之前45億快遞資料查詢渠道疑似在近日復活的訊息。 訊息是 ......

    uj5u.com 2023-04-20 08:48:24 more
  • web3 產品介紹:metamask 錢包 使用最多的瀏覽器插件錢包

    Metamask錢包是一種基于區塊鏈技術的數字貨幣錢包,它允許用戶在安全、便捷的環境下管理自己的加密資產。Metamask錢包是以太坊生態系統中最流行的錢包之一,它具有易于使用、安全性高和功能強大等優點。 本文將詳細介紹Metamask錢包的功能和使用方法。 一、 Metamask錢包的功能 數字資 ......

    uj5u.com 2023-04-20 08:47:46 more
  • vulnhub_Earth

    前言 靶機地址->>>vulnhub_Earth 攻擊機ip:192.168.20.121 靶機ip:192.168.20.122 參考文章 https://www.cnblogs.com/Jing-X/archive/2022/04/03/16097695.html https://www.cnb ......

    uj5u.com 2023-04-20 07:46:20 more
  • 從4k到42k,軟體測驗工程師的漲薪史,給我看哭了

    清明節一過,盲猜大家已經無心上班,在數著日子準備過五一,但一想到銀行卡里的余額……瞬間心情就不美麗了。最近,2023年高校畢業生就業調查顯示,本科畢業月平均起薪為5825元。調查一出,便有很多同學表示自己又被平均了。看著這一資料,不免讓人想到前不久中國青年報的一項調查:近六成大學生認為畢業10年內會 ......

    uj5u.com 2023-04-20 07:44:00 more
  • 最新版本 Stable Diffusion 開源 AI 繪畫工具之中文自動提詞篇

    🎈 標簽生成器 由于輸入正向提示詞 prompt 和反向提示詞 negative prompt 都是使用英文,所以對學習母語的我們非常不友好 使用網址:https://tinygeeker.github.io/p/ai-prompt-generator 這個網址是為了讓大家在使用 AI 繪畫的時候 ......

    uj5u.com 2023-04-20 07:43:36 more
  • 漫談前端自動化測驗演進之路及測驗工具分析

    隨著前端技術的不斷發展和應用程式的日益復雜,前端自動化測驗也在不斷演進。隨著 Web 應用程式變得越來越復雜,自動化測驗的需求也越來越高。如今,自動化測驗已經成為 Web 應用程式開發程序中不可或缺的一部分,它們可以幫助開發人員更快地發現和修復錯誤,提高應用程式的性能和可靠性。 ......

    uj5u.com 2023-04-20 07:43:16 more
  • CANN開發實踐:4個DVPP記憶體問題的典型案例解讀

    摘要:由于DVPP媒體資料處理功能對存放輸入、輸出資料的記憶體有更高的要求(例如,記憶體首地址128位元組對齊),因此需呼叫專用的記憶體申請介面,那么本期就分享幾個關于DVPP記憶體問題的典型案例,并給出原因分析及解決方法。 本文分享自華為云社區《FAQ_DVPP記憶體問題案例》,作者:昇騰CANN。 DVPP ......

    uj5u.com 2023-04-20 07:43:03 more
  • msf學習

    msf學習 以kali自帶的msf為例 一、msf核心模塊與功能 msf模塊都放在/usr/share/metasploit-framework/modules目錄下 1、auxiliary 輔助模塊,輔助滲透(埠掃描、登錄密碼爆破、漏洞驗證等) 2、encoders 編碼器模塊,主要包含各種編碼 ......

    uj5u.com 2023-04-20 07:42:59 more
  • Halcon軟體安裝與界面簡介

    1. 下載Halcon17版本到到本地 2. 雙擊安裝包后 3. 步驟如下 1.2 Halcon軟體安裝 界面分為四大塊 1. Halcon的五個助手 1) 影像采集助手:與相機連接,設定相機引數,采集影像 2) 標定助手:九點標定或是其它的標定,生成標定檔案及內參外參,可以將像素單位轉換為長度單位 ......

    uj5u.com 2023-04-20 07:42:17 more
  • 在MacOS下使用Unity3D開發游戲

    第一次發博客,先發一下我的游戲開發環境吧。 去年2月份買了一臺MacBookPro2021 M1pro(以下簡稱mbp),這一年來一直在用mbp開發游戲。我大致分享一下我的開發工具以及使用體驗。 1、Unity 官網鏈接: https://unity.cn/releases 我一般使用的Apple ......

    uj5u.com 2023-04-20 07:40:19 more