一、實驗目的:
1、在上一篇的“RTS式單位控制”的基礎上新增邏輯執行緒,為每個單位實現ai計算;
2、用精靈動畫為單位的行為顯示對應的動作效果。
二、執行效果:
1、場景中的單位分為紅藍兩方,單位在發現敵對單位後向敵人移動:
2、進入攻擊範圍後對敵對單位發起攻擊:
注意,單位在“移動”、“攻擊”、“受傷”、“死亡”時分別播放不同的動畫。
3、切換為RTS式控制後,可以選擇單位併發布“移動攻擊”命令:
有一些單位已經與敵人接觸,優先執行攻擊動作。
三、程式結構:
1、工程目錄:
外層ASSETS目錄儲存了場景的地面貼圖、天空盒、地形資源,內層ASSETS目錄下是單位的精靈動畫資源。(實際的github倉庫裡還有上篇文章的程式碼)
2、執行緒結構:
主執行緒中的TESTRTS.html是程式入口,負責初始化WebGL場景和與邏輯執行緒通訊,babylon50.min.js是Babylon.js引擎庫,newland.js是一個Web3D工具庫,One.js是單位渲染程式碼,VTools.js是向量計算程式碼,ControlRTS3.js是rts控制程式碼,FrameGround2.js是地形生成程式碼,recast.js是群組導航庫。主執行緒負責dom管理、WebGL場景的渲染、單位的群組尋路、單位的動畫計算。
邏輯執行緒中的worker.js負責初始化邏輯環境、維持邏輯迴圈和與主執行緒通訊,OneThink.js負責單位ai計算。邏輯執行緒和主執行緒間使用postMessage進行通訊。
四、主執行緒初始化:
在之前的文章中介紹過的內容不再贅述,這裡只討論新增的部分,可在https://github.com/ljzc002/ControlRTS下載程式碼,國內訪問github的一種方法見附錄一。
1、生成單位的精靈動畫圖片:
場景中使用的精靈動畫圖片如下:
這是一張透明背景的PNG圖片,圖中每個128*128的畫素小塊對應精靈動畫的一幀,這種圖片一般由美工使用專業工具繪製,但這裡為演示方便用程式碼生成:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>建立用於精靈動畫的方塊圖列</title> 6 </head> 7 <body> 8 <div id="div_allbase" ><!--style="border:1px solid #888888;width: 1282px;height: 386px"--> 9 <canvas id="renderCanvas" width="1280" height="384" style="width: 1280px;height: 384px;border:1px solid #888888;padding: 0px;margin: 0px"></canvas> 10 </div> 11 </body> 12 <script> 13 var canvas=document.getElementById("renderCanvas"); 14 var ctx=canvas.getContext("2d"); 15 var count_draw=0; 16 var oX=0; 17 var oY=0; 18 ctx.fillStyle="rgba(255,255,255,0)";//透明背景 19 ctx.strokeStyle="#222222"; 20 ctx.fillRect(0,0,1280,384); 21 //ctx.beginPath(); 22 //相對於每個小塊的中心繪製 23 ctx.oArc=function(x,y,r,sAngle,eAngle,counterclockwise) 24 { 25 ctx.arc(x+oX,y+oY,r,sAngle,eAngle,counterclockwise) 26 } 27 ctx.oMoveTo=function(x,y) 28 { 29 ctx.moveTo(x+oX,y+oY); 30 } 31 ctx.oLineTo=function(x,y) 32 { 33 ctx.lineTo(x+oX,y+oY); 34 } 35 ctx.oRect=function(x,y,width,height) 36 { 37 ctx.rect(x+oX,y+oY,width,height); 38 } 39 40 41 //根據頭的位置和四肢角度繪製一個二維的小人兒,角度採用順時針,頭位置,頭左右轉動,頭上下俯仰,身體傾斜,左上臂角度,,,,左大腿角度 42 function DrawaMan(phx,phy,ahy,ahz,ab,alh1,alh2,arh1,arh2,alf1,alf2,arf1,arf2) 43 {//根據引數畫小人兒的方法 44 ctx.beginPath(); 45 oX=count_draw%10*128; 46 oY=Math.floor(count_draw/10)*128; 47 48 var obj_aman={ph:{x:phx,y:phy}}; 49 var rh=5;//頭半徑 50 var lb=20;//身長 51 var lh1=8;// 52 var lh2=8;// 53 var lf1=10;// 54 var lf2=10;// 55 ctx.oArc(phx,phy,rh,0,Math.PI*2); 56 ctx.stroke(); 57 //暫時不畫眼睛,這樣ahy,ahz暫時沒有直接作用 58 var x1=phx,y1=phy+rh; 59 var x2,y2,x3,y3; 60 ctx.oMoveTo(x1,y1); 61 obj_aman.pb1={x:x1,y:y1}; 62 x2=x1-lb*Math.sin(ab); 63 y2=y1+lb*Math.cos(ab); 64 ctx.oLineTo(x2,y2); 65 obj_aman.pb2={x:x2,y:y2}; 66 ctx.stroke(); 67 68 ctx.oMoveTo(x1,y1); 69 x2=x1-lh1*Math.sin(ab+alh1); 70 y2=y1+lh1*Math.cos(ab+alh1); 71 ctx.oLineTo(x2,y2); 72 obj_aman.plh1={x:x2,y:y2}; 73 ctx.stroke(); 74 75 ctx.oMoveTo(x2,y2); 76 x3=x2-lh2*Math.sin(ab+alh1+alh2); 77 y3=y2+lh2*Math.cos(ab+alh1+alh2); 78 ctx.oLineTo(x3,y3); 79 obj_aman.plh2={x:x3,y:y3}; 80 ctx.stroke(); 81 82 ctx.oMoveTo(x1,y1); 83 x2=x1-lh1*Math.sin(ab+arh1); 84 y2=y1+lh1*Math.cos(ab+arh1); 85 ctx.oLineTo(x2,y2); 86 obj_aman.prh1={x:x2,y:y2}; 87 ctx.stroke(); 88 89 ctx.oMoveTo(x2,y2); 90 x3=x2-lh2*Math.sin(ab+arh1+arh2); 91 y3=y2+lh2*Math.cos(ab+arh1+arh2); 92 ctx.oLineTo(x3,y3); 93 obj_aman.prh2={x:x3,y:y3}; 94 ctx.stroke(); 95 96 //開始畫腿 97 x1=obj_aman.pb2.x; 98 y1=obj_aman.pb2.y; 99 100 ctx.oMoveTo(x1,y1); 101 x2=x1-lf1*Math.sin(ab+alf1); 102 y2=y1+lf1*Math.cos(ab+alf1); 103 ctx.oLineTo(x2,y2); 104 obj_aman.plf1={x:x2,y:y2}; 105 ctx.stroke(); 106 107 ctx.oMoveTo(x2,y2); 108 x3=x2-lf2*Math.sin(ab+alf1+alf2); 109 y3=y2+lf2*Math.cos(ab+alf1+alf2); 110 ctx.oLineTo(x3,y3); 111 obj_aman.plf2={x:x3,y:y3}; 112 ctx.stroke(); 113 114 ctx.oMoveTo(x1,y1); 115 x2=x1-lf1*Math.sin(ab+arf1); 116 y2=y1+lf1*Math.cos(ab+arf1); 117 ctx.oLineTo(x2,y2); 118 obj_aman.prf1={x:x2,y:y2}; 119 ctx.stroke(); 120 121 ctx.oMoveTo(x2,y2); 122 x3=x2-lf2*Math.sin(ab+arf1+arf2); 123 y3=y2+lf2*Math.cos(ab+arf1+arf2); 124 ctx.oLineTo(x3,y3); 125 obj_aman.prf2={x:x3,y:y3}; 126 ctx.stroke(); 127 128 count_draw++; 129 //,ab,alh1,alh2,arh1,arh2,alf1,alf2,arf1,arf2 130 obj_aman.ab=ab; 131 obj_aman.alh1=alh1; 132 obj_aman.alh2=alh2; 133 obj_aman.arh1=arh1; 134 obj_aman.arh2=arh2; 135 obj_aman.alf1=alf1; 136 obj_aman.alf2=alf2; 137 obj_aman.arf1=arf1; 138 obj_aman.arf2=arf2; 139 return obj_aman; 140 141 } 142 //第一隻手(右手)的位置,第二隻手的位置(可能為空),第一隻手距矛頭的距離,長矛在世界座標系中距y軸負方向逆時針弧度 143 function DrawaSpear(ph1,ph2,d,a)//根據引數畫長矛的方法 144 { 145 ctx.beginPath(); 146 var obj_spear={} 147 var ls=60; 148 var ss=5;//矛頭的每邊長度 149 var as=Math.PI/6 150 151 if(ph2)//那麼就不需要a 152 { 153 if((ph1.y-ph2.y)>=0) 154 { 155 a=Math.atan((ph1.x-ph2.x)/(ph1.y-ph2.y)); 156 } 157 else 158 { 159 a=Math.PI+Math.atan((ph1.x-ph2.x)/(ph1.y-ph2.y)); 160 } 161 162 } 163 var x1=ph1.x+d*Math.sin(a); 164 var y1=ph1.y+d*Math.cos(a); 165 var x2=ph1.x-(ls-d)*Math.sin(a); 166 var y2=ph1.y-(ls-d)*Math.cos(a); 167 ctx.oMoveTo(x1,y1); 168 ctx.oLineTo(x2,y2); 169 ctx.stroke(); 170 obj_spear.pst={x:x2,y:y2}; 171 172 var lsh=ss*Math.cos(as)*2 173 var x3=x1+lsh*Math.sin(a); 174 var y3=y1+lsh*Math.cos(a); 175 obj_spear.psh={x:x3,y:y3}; 176 177 ctx.oMoveTo(x1,y1); 178 x2=x1+ss*Math.sin(a+as); 179 y2=y1+ss*Math.cos(a+as); 180 ctx.oLineTo(x2,y2); 181 ctx.oLineTo(x3,y3); 182 ctx.stroke(); 183 184 ctx.oMoveTo(x1,y1); 185 x2=x1+ss*Math.sin(a-as); 186 y2=y1+ss*Math.cos(a-as); 187 ctx.oLineTo(x2,y2); 188 ctx.oLineTo(x3,y3); 189 ctx.stroke(); 190 191 return obj_spear 192 } 193 //初始 194 var grid_original=DrawaMan(64,25,0,0,0,Math.PI/6,-Math.PI/6,-Math.PI/6,Math.PI/6,Math.PI/6,-Math.PI/6,-Math.PI/6,Math.PI/6) 195 var spear_original=DrawaSpear(grid_original.prh2,grid_original.plh2,20); 196 197 var grid=DrawaMan(64,28,0,0,0,0.5235987755982988,-1.0471975511965976,-0.6981317007977318,0.17453292519943292,0.6981317007977318,-0.34906585039886584,-0.6981317007977318,1.0471975511965976) 198 var spear=DrawaSpear(grid.prh2,grid.plh2,26); 199 200 var grid=DrawaMan(64,31,0,0,0,0.5235987755982988,-1.5707963267948966,-0.8726646259971647,-0.17453292519943298,0.8726646259971647,-0.17453292519943292,-0.8726646259971647,1.5707963267948966) 201 var spear=DrawaSpear(grid.prh2,grid.plh2,33); 202 203 //先保持頭部高度不變,看腳高了多少,然後再反過來調整頭部高度(攻擊) 204 var grid_3=DrawaMan(64,35,0,0,0,Math.PI/6,-Math.PI*2/3,-Math.PI/3,-Math.PI/6,Math.PI/3,0,-Math.PI/3,Math.PI*2/3) 205 var spear_3=DrawaSpear(grid_3.prh2,grid_3.plh2,40); 206 207 var grid=DrawaMan(64,31,0,0,0,0.5235987755982988,-1.5707963267948966,-0.8726646259971647,-0.17453292519943298,0.8726646259971647,-0.17453292519943292,-0.8726646259971647,1.5707963267948966) 208 var spear=DrawaSpear(grid.prh2,grid.plh2,33); 209 210 var grid=DrawaMan(64,28,0,0,0,0.5235987755982988,-1.0471975511965976,-0.6981317007977318,0.17453292519943292,0.6981317007977318,-0.34906585039886584,-0.6981317007977318,1.0471975511965976) 211 var spear=DrawaSpear(grid.prh2,grid.plh2,26); 212 213 var grid_5=DrawaMan(64,25,0,0,0,Math.PI/6,-Math.PI/6,-Math.PI/6,Math.PI/6,Math.PI/6,-Math.PI/6,-Math.PI/6,Math.PI/6) 214 var spear_5=DrawaSpear(grid_5.prh2,grid_5.plh2,20); 215 216 //行走 217 var grid_5=DrawaMan(64,27,0,0,0,Math.PI/6,-Math.PI/6,-Math.PI/6,Math.PI/6,Math.PI/4,-Math.PI/4,-Math.PI/4,Math.PI/4) 218 var spear_5=DrawaSpear(grid_5.prh2,grid_5.plh2,20); 219 var grid_5=DrawaMan(64,25,0,0,0,Math.PI/6,-Math.PI/6,-Math.PI/6,Math.PI/6,Math.PI/6,-Math.PI/6,-Math.PI/6,Math.PI/6) 220 var spear_5=DrawaSpear(grid_5.prh2,grid_5.plh2,20); 221 222 223 224 linearInterpolation([Math.PI/6,-Math.PI/6,-Math.PI/6,Math.PI/6,Math.PI/6,-Math.PI/6,-Math.PI/6,Math.PI/6] 225 ,[Math.PI/6,-Math.PI*2/3,-Math.PI/3,-Math.PI/6,Math.PI/3,0,-Math.PI/3,Math.PI*2/3],2) 226 //對兩個陣列進行插值,引數分別是初始陣列、截止陣列、插值個數 227 function linearInterpolation(arrStart,arrEnd,count_inter) 228 { 229 var len=arrStart.length; 230 var arr_str=[]; 231 for(var j=0;j<count_inter;j++) 232 { 233 arr_str.push(""); 234 } 235 for(var i=0;i<len;i++)//對於每一個屬性 236 { 237 var start=arrStart[i]; 238 var end=arrEnd[i]; 239 240 var step=(end-start)/(count_inter+1); 241 for(var j=0;j<count_inter;j++)//對於每一個插入的值 242 { 243 arr_str[j]+=(start+step*(j+1)+","); 244 } 245 } 246 return arr_str; 247 248 } 249 250 </script> 251 </html>
這段程式碼首先定義出根據引數繪製人和矛的方法,然後從194行到220行畫出了每一個動畫幀的姿態(注意每繪製一幀後會自動移動繪製位置),linearInterpolation方法的作用是根據兩個關鍵幀的引數插值生成中間幀的引數,比如194和204兩行(對應圖片的第一格和第四格)的引數是人工設計出來的,而197和200行(第二格和第三格)的引數是使用linearInterpolation方法插值得到。將linearInterpolation方法放在另一個html裡可能更加條理分明,但這裡為了省事就放在一個html裡了。
2、主執行緒初始化流程:
下面介紹其中的新增部分
3、為了區分不同勢力,在beforeInit方法中對原始精靈動畫圖進行改造:
1 function beforeInit() 2 { 3 var can_source=document.createElement("canvas"); 4 var img=new Image(); 5 img.src="ASSETS/002.png"; 6 img.onload=function(){ 7 width=img.width; 8 height=img.height; 9 can_source.style.width=width+"px"; 10 can_source.style.height=height+"px"; 11 can_source.width=width; 12 can_source.height=height; 13 var con_source=can_source.getContext("2d"); 14 con_source.drawImage(img,0,0); 15 16 for(var i=0;i<9;i++)//用不同顏色區分不同勢力 17 { 18 con_source.beginPath() 19 con_source.fillStyle="red"; 20 con_source.arc(64+i*128,8,8,0,Math.PI*2,true); 21 con_source.closePath(); 22 con_source.fill(); 23 } 24 obj_png.a=can_source.toDataURL(); 25 for(var i=0;i<9;i++) 26 { 27 con_source.beginPath() 28 con_source.fillStyle="blue"; 29 con_source.arc(64+i*128,8,8,0,Math.PI*2,true); 30 con_source.closePath(); 31 con_source.fill(); 32 } 33 obj_png.b=can_source.toDataURL(); 34 Init(); 35 } 36 37 38 }
這裡因為時間有限只用簡單的紅點和藍點表示不同勢力,更好的方案是修改單位本身的裝備顏色,並且對不同勢力應用不同的“選中框”。
4、webGLStart2方法除了上一篇文章中提到過的建立導航網格和導航群組外,還負責生成兩種精靈管理器和“選中框”的源網格:
新增程式碼:
1 //initThem,兩個精靈管理器分別負責繪製代表兩個勢力單位的精靈 2 var spriteManagerPlayerA = new BABYLON.SpriteManager("playerManagerA", obj_png.a, 200, 128, scene); 3 spriteManagerPlayerA.isPickable = true; 4 spriteManagerPlayerA.renderingGroupId=2; 5 MyGame.spriteManagerPlayerA=spriteManagerPlayerA; 6 var spriteManagerPlayerB = new BABYLON.SpriteManager("playerManagerB", obj_png.b, 200, 128, scene); 7 spriteManagerPlayerB.isPickable = true; 8 spriteManagerPlayerB.renderingGroupId=2; 9 MyGame.spriteManagerPlayerB=spriteManagerPlayerB; 10 obj_owners={a:spriteManagerPlayerA,b:spriteManagerPlayerB} 11 12 // var spriteManagerPlayerK = new BABYLON.SpriteManager("spriteManagerPlayerK", "ASSETS/kuang3.png", 400, 64, scene); 13 // spriteManagerPlayerK.isPickable = false; 14 // spriteManagerPlayerK.renderingGroupId=3; 15 // MyGame.spriteManagerPlayerK=spriteManagerPlayerK; 16 var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene); 17 mat_frame.wireframe = true; 18 var mesh_k1 = BABYLON.MeshBuilder.CreatePlane("mesh_k1", {height:1,width:1}, scene); 19 //var mesh_k1=new BABYLON.MeshBuilder.CreateBox("mesh_k1",{},scene); 20 mesh_k1.renderingGroupId=2; 21 mesh_k1.isVisible=false; 22 mesh_k1.material=mat_frame; 23 mesh_k1.billboardMode=BABYLON.Mesh.BILLBOARDMODE_ALL; 24 MyGame.mesh_k1=mesh_k1;//建立一個邊框材質的“選擇框”
選擇框的作用一是為每個單位提供一個可以被點選的網格例項(Babylon.js的精靈物件不接受射線檢測),二是在選中單位後在單位周圍畫框,表示單位被選中(也就是第三張動圖中的白框),為了提升渲染效率這裡使用網格例項。
5、隨機生成單位
One.js的前半部分程式碼:
1 var One=function(){//單位的屬性 2 this.pos={x:0,y:1,z:0}; 3 this.radius=0.2; 4 this.view=5;//視野(地塊數) 5 this.id=null; 6 this.arr_history=[]; 7 //----20210723RTS 8 this.hp=4; 9 this.at=2; 10 this.owner=null; 11 this.influence=5; 12 } 13 var bing0; 14 //在平面社交網路中建立多個One,注意他們不能重疊 15 //引數:儲存One的陣列,建立個數 16 One.createRandomThem=function(obj_them,count){ 17 // bing0=new BABYLON.Sprite("bing0", obj_owners.a); 18 // bing0.position.y=10; 19 // bing0.playAnimation(0,8,true,100); 20 21 for(var i=0;i<count;i++) 22 { 23 var one=new One(); 24 one.id="One_"+i; 25 var owner=newland.RandomChooseFromObj(obj_owners)//隨機分配勢力 26 one.owner=owner.key; 27 //先關聯群組,然後使用群組的位置設定方法 28 var randomPos=new BABYLON.Vector3(0,0,0); 29 if(one.owner=="a") 30 {//使用導航元件的隨機位置方法,可以避免單位重疊 31 randomPos = navigationPlugin.getRandomPointAround(new BABYLON.Vector3(20.0, 0.2, 0), 0.5); 32 } 33 else 34 { 35 randomPos = navigationPlugin.getRandomPointAround(new BABYLON.Vector3(-20.0, 0.2, 0), 0.5); 36 } 37 //建立一個變換節點,在Babylon.js中表示單位的位置和姿態 38 var transform = new BABYLON.TransformNode(); 39 //agentCube.parent = transform;把單位新增到導航群組,用來進行群組導航 40 var agentIndex = MyGame.crowd.addAgent(randomPos, MyGame.agentParams, transform); 41 //建立一個精靈,用來顯示單位 42 var bing=new BABYLON.Sprite("bing_"+one.id, owner.value); 43 //這裡bing的size是預設的1!! 44 //transform.pathPoints=[transform.position]; 45 var state={//單位的狀態機 46 feeling:"free", 47 wanting:"waiting", 48 doing:"waiting", 49 being:{},//一個單位可能同時受到多種影響 50 } 51 transform.position=randomPos; 52 bing.position=transform.position;//精靈沒有parent屬性!! 53 one.pos={x:transform.position.x,y:transform.position.y,z:transform.position.z} 54 one.idx=agentIndex; 55 one.trf=transform; 56 one.mesh=bing; 57 var kuang=MyGame.mesh_k1.createInstance("k1_"+one.id);//單位的選擇框 58 kuang.isVisible=false; 59 kuang.parent=transform; 60 //var kuang =new BABYLON.Sprite("sprite_kuang_"+one.id, MyGame.spriteManagerPlayerK);//顯示在bing周圍的白框,用來表示選中 61 //kuang.isVisible=false; 62 //kuang.size=0.8; 63 //kuang.position.y=0.5; 64 one.kuang=kuang; 65 kuang.unit=one; 66 one.target=null; 67 one.data={state:state}; 68 bing.unit=one; 69 arr_unit.push(one);//分別用陣列元素和物件屬性的方式儲存單位物件 70 71 //one.arr_history.push(obj);//記錄單位的最初狀態 72 obj_them[one.id]=one; 73 } 74 return obj_them;//既改變又返回 75 76 }
另一種避免單位重疊的方法:
1 One.setPos=function(obj_them,one,arr_x,arr_z) 2 { 3 one.pos.x=newland.RandomBetween(arr_x[0],arr_x[1]); 4 one.pos.z=newland.RandomBetween(arr_z[0],arr_z[1]); 5 for(var key in obj_them) 6 { 7 var olderOne=obj_them[key]; 8 if(vxz.distance(olderOne.pos,one.pos)<(olderOne.radius+one.radius))//如果太近 9 { 10 One.setPos(obj_them,one,arr_x,arr_z);//第一次未失敗的遞迴會設定pos 11 break; 12 } 13 } 14 return one; 15 }
6、啟動邏輯執行緒
1 function initWorker() 2 { 3 //initWorker 4 console.log("開始啟動work執行緒"); 5 worker = new Worker("WORK/worker.js"); 6 7 //與邏輯執行緒通訊的程式碼稍後介紹 8 9 }
第五行程式碼直接執行worker.js,這種用法與nodejs相似。
7、啟動渲染迴圈的程式碼在ControlRTS3.js檔案中,ControlRTS3.js與上篇文章的區別有兩處:
a、點選右鍵時不直接觸發移動,而是將移動命令發給邏輯執行緒,然後由邏輯執行緒決定是否移動:
1 function onContextMenu(evt) 2 { 3 var pickInfo = scene.pick(scene.pointerX, scene.pointerY, (mesh)=>(mesh.id!="mesh_kuang0"), false, MyGame.camera0); 4 if(pickInfo.hit) 5 { 6 var mesh = pickInfo.pickedMesh; 7 //if(mesh.myname=="navmeshdebug")//這是限制只能點選導航網格 8 var startingPoint=pickInfo.pickedPoint; 9 //var agents = MyGame.crowd.getAgents(); 10 var len=arr_selected.length; 11 var i; 12 var obj_selected={}; 13 for (i=0;i<len;i++) {//分別指揮被框選中的每個單位 14 var unit=arr_selected[i]; 15 //var agent=agents[unit.idx]; 16 //移動可以分為攻擊移動和強制移動兩種,預設是攻擊移動? 17 unit.data.state.doing="waiting";//正在移動《-確保開始移動後才能改為walking,也就是說只能由move方法設定! 18 unit.data.state.wanting="Attackto";//想要攻擊移動 19 unit.data.state.feeling="commanded";//收到命令 20 unit.target={x:startingPoint.x,y:startingPoint.y,z:startingPoint.z} 21 //命令發出後,交給邏輯執行緒,邏輯執行緒進行判斷後做出行動 22 //crowd.agentGoto(agent, navigationPlugin.getClosestPoint(startingPoint)); 23 obj_selected[unit.id]=unit; 24 } 25 var obj_units0=One.obj2data(obj_selected); 26 worker.postMessage(JSON.stringify({type:"unitCommand",obj_units0:obj_units0})); 27 } 28 29 }
b、在每一幀渲染前設定精靈的朝向,並且把當前單位狀態同步給邏輯執行緒
1 scene.registerBeforeRender( 2 function(){ 3 //Think();//如果沒有邏輯執行緒,則在這裡進行ai計算 4 var obj_count={a:0,b:0}; 5 if(flag_runningstate=="初始化完成") 6 { 7 var jiao_camera0=camera0.rotation.y%(Math.PI*2);//相機的姿態角 8 if(jiao_camera0<0) 9 { 10 jiao_camera0+=Math.PI*2; 11 } 12 var len = arr_unit.length; 13 //var flag_rest=false;//每個運動群組都要有專屬的運動結束標誌!!!! 14 var obj_send={}; 15 for(let i = 0;i<len;i++)//對於每個單位 16 { 17 var unit = arr_unit[i]; 18 unit.trf.position = MyGame.crowd.getAgentPosition(unit.idx); 19 if(unit.data.state.doing!="attacking") 20 { 21 unit.face=unit.trf.position.subtract(unit.mesh.position);//單位的朝向向量 22 } 23 var jiao_face=Math.atan2(unit.face.x,unit.face.z);//單位的姿態角 24 if(jiao_face<0) 25 { 26 jiao_face+=Math.PI*2; 27 } 28 29 // if(jiao_camera0<0) 30 // { 31 // jiao_camera0+=Math.PI*2; 32 // } 33 // var cface=unit.trf.position.subtract(camera0.position); 34 // var num=vxz.isSameSide(cface,unit.face); 35 // if(num>0) 36 // { 37 // 38 // } 39 if(jiao_camera0-jiao_face>0&&jiao_camera0-jiao_face<Math.PI) 40 {//精靈是平面的,所以只有“向左”和“向右”兩個方向,這可以通過反轉精靈實現 41 unit.mesh.invertU=true;//設定是否要反轉精靈 42 } 43 else 44 {//另一種思路是在精靈動畫圖裡繪製單位朝向不同方向時的畫面,這樣將可以顯示更多的方向! 45 unit.mesh.invertU=false; 46 } 47 48 unit.mesh.position=unit.trf.position; 49 // unit.kuang.position=unit.trf.position; 50 //unit.kuang.position.y=unit.kuang.position.y+0.8; 51 unit.pos={x:unit.trf.position.x,y:unit.trf.position.y,z:unit.trf.position.z} 52 if(true)//死亡的單位也要同步給邏輯執行緒,否則會不斷移動?!如果這個單位在運動,則要把單位的資訊同步給邏輯執行緒 53 { 54 obj_send[unit.id]=unit 55 56 } 57 if(unit.hp>0) 58 { 59 obj_count[unit.owner]+=1; 60 } 61 62 } 63 var obj_units0=One.obj2data(obj_send);//將單位資訊整理為json格式,傳送給邏輯執行緒 64 worker.postMessage(JSON.stringify({type:"updateUnits",obj_units0:obj_units0})); 65 div_middle.innerHTML="紅:"+obj_count.a+",藍:"+obj_count.b;//修改紅藍勢力人數 66 // if(bing0) 67 // { 68 // bing0.position.x+=0.01; 69 // bing0.invertU=!bing0.invertU 70 // } 71 72 } 73 74 } 75 )
需要注意的是,因為群組導航在主執行緒中進行,所以只有主執行緒能保有單位的準確實時狀態,並每幀向邏輯執行緒同步,而邏輯執行緒則根據單位狀態,向主執行緒傳送命令,觸發主執行緒中的動畫或尋路計算。
五、邏輯執行緒
1、在邏輯執行緒中引入更多js檔案
js的work執行緒不能操作window物件,所以引入js檔案的方式與主執行緒有所區別,經過試驗確定可以用以下方法引入其他js檔案:
1 //載入其他js檔案 2 var newland={}//下面程式碼來自百度 3 if(!newland.importScripts){ newland.importScripts=(function(globalEval){ var xhr=new XMLHttpRequest; return function importScripts(){ var args=Array.prototype.slice.call(arguments) ,len=args.length ,i=0 ,meta ,data ,content ; for(;i<len;i++){ if(args[i].substr(0,5).toLowerCase()==="data:"){ data=args[i]; content=data.indexOf(","); meta=data.substr(5,content).toLowerCase(); data=decodeURIComponent(data.substr(content+1)); if(/;\s*base64\s*[;,]/.test(meta)){ data=atob(data); } if(/;\s*charset=[uU][tT][fF]-?8\s*[;,]/.test(meta)){ data=decodeURIComponent(escape(data)); } }else{ xhr.open("GET",args[i],false); xhr.send(null); data=xhr.responseText; } globalEval(data); } }; }(eval)); } 4 newland.importScripts("OneThink.js"); 5 newland.importScripts("../VTools.js");
大致思路是用ajax獲取遠端js檔案的文字,然後用eval方法執行。注意,用這種方法載入的js檔案不會預設出現在Chrome瀏覽器的除錯頁面中,在執行到對應方法時選擇“步入除錯”方可進入js檔案內部。
2、執行緒間通訊:
Chrome不允許多執行緒共享記憶體,所以選擇postMessage方法線上程間傳遞JSON資訊,兩個執行緒的資訊傳送方式如下:
主執行緒:
1 worker.postMessage(JSON.stringify({type:"initWork",obj_units0:obj_units0}));
邏輯執行緒:
1 self.postMessage(JSON.stringify({type:"consoleLog",text:"work執行緒載入其他js檔案完成"}));
資訊中以type屬性表示命令型別,其他屬性儲存命令的引數。
兩個執行緒的資訊接收方式如下:
主執行緒:
1 worker.onmessage=function(e) 2 { 3 var obj_data=JSON.parse(e.data) 4 if(obj_data.type=="consoleLog")//根據type不同使用不同處理方式,輸入日誌資訊 5 { 6 console.log(obj_data.text); 7 } 8 else if(obj_data.type=="consoleError")//輸出異常資訊 9 { 10 console.error(obj_data.text); 11 } 12 else if(obj_data.type=="workInited")//邏輯執行緒初始化完成 13 {// 14 console.log("邏輯初始化完成"); 15 count_init--; 16 17 if(count_init==0) 18 { 19 flag_runningstate="初始化完成"; 20 21 } 22 } 23 else if(obj_data.type=="updateUnits")//hp等量的變化與前端的動畫幀計時關係很大,所以應該用前端渲染更新後端邏輯?! 24 {//由邏輯執行緒向主執行緒同步單位狀態(未使用) 25 var obj_units0=obj_data.obj_units0; 26 for(var key in obj_units0) 27 { 28 var obj0=obj_units0[key];//從Think執行緒傳遞過來的新狀態 29 var obj=obj_units[key]; 30 //obj.pos=obj0.pos;//位置變化由渲染執行緒計算 31 obj.radius=obj0.radius; 32 obj.view=obj0.view; 33 obj.hp=obj0.hp; 34 obj.at=obj0.at; 35 obj.owner=obj0.owner; 36 obj.influence=obj0.influence; 37 obj.arr_history.push(obj0); 38 } 39 console.log("完成一次計算:"+(new Date().getTime()));//obj_data.frameTime 40 } 41 else if(obj_data.type=="unitCommand")//收到邏輯執行緒傳來命令,一般是觸發動畫效果和導航效果 42 { 43 var obj_units0=obj_data.obj_units0; 44 for(var key in obj_units0)//對於每一個帶有命令的單位 45 { 46 var obj0=obj_units0[key];//從Think執行緒傳遞過來的新狀態 47 var obj=obj_units[key];//主執行緒中的單位物件 48 var arr_c=obj0.arr_command;//對於這個單位攜帶的每一條命令 49 var len=arr_c.length; 50 for(var i=0;i<len;i++) 51 { 52 var command=arr_c[i]; 53 //var func=eval(command.func) 54 obj[command.func] (command.obj_p);//方法名(引數) 55 } 56 } 57 //console.log("count_a:"+obj_data.count_a); 58 } 59 }
個人比較喜歡用if else代替switch,因為可以在判斷中使用更復雜的條件。這裡的通訊處理程式碼和WebSocket通訊處理程式碼很像,稍加修改即可將多執行緒計算變為網路計算,更進一步的可以建立多執行緒和網路相結合的“雲端計算”。
邏輯執行緒:
1 self.onmessage=function(e) 2 { 3 var obj_data=JSON.parse(e.data) 4 if(obj_data.type=="initWork")//收到主執行緒的“初始化邏輯執行緒”命令 5 { 6 self.postMessage(JSON.stringify({type:"consoleLog",text:"開始初始化work執行緒"})); 7 var mapWidth=maxx-minx;//地圖的寬度 8 var mapHeight=maxz-minz//高度 9 var partCountX=Math.ceil(mapWidth/partSizeX);//根據預先設定的地塊大小,將地圖劃分為多個地塊 10 var partCountZ=Math.ceil(mapHeight/partSizeZ); 11 for(var i=0;i<partCountX;i++)//為地圖上的每個區域分配一個陣列元素 12 {//俯視來看左下角是第一個區域,之後沿著z軸劃分一系列區域,這一條z軸劃分完畢後,沿x軸移動到下一根z軸 13 var arr=[]; 14 for(var j=0;j<partCountZ;j++) 15 { 16 var obj_part={arr_unit:[],partx:i,partz:j,posx:i*partSizeX,posz:i*partSizeZ}//每個區域是一個物件,其中包括區域內的單位陣列 17 arr.push(obj_part); 18 } 19 arr_part.push(arr); 20 } 21 //為每個區域標明東南西北區域?能夠少量節約計算力 22 arr_part.forEach((arr,i)=>{ 23 arr.forEach((obj_part,j)=>{ 24 if(arr_part[i-1]&&arr_part[i-1][j]) 25 { 26 obj_part[3]=arr_part[i-1][j];//west 27 } 28 else 29 { 30 obj_part[3]=null; 31 } 32 if(arr_part[i+1]&&arr_part[i+1][j])//east 33 { 34 obj_part[0]=arr_part[i+1][j] 35 } 36 else 37 { 38 obj_part[0]=null; 39 } 40 if(arr_part[i][j-1]) 41 { 42 obj_part[2]=arr_part[i][j-1];//north 43 } 44 else 45 { 46 obj_part[2]=null; 47 } 48 if(arr_part[i][j+1]) 49 { 50 obj_part[1]=arr_part[i][j+1];//south 51 } 52 else 53 { 54 obj_part[1]=null; 55 } 56 }) 57 }) 58 59 var obj_units0=obj_data.obj_units0;//從主執行緒傳來的單位資料 60 for(var key in obj_units0)//在work執行緒中為每個單位建立思考物件 61 { 62 var oneThink=new OneThink(obj_units0[key]) 63 obj_units[key]=oneThink; 64 } 65 66 self.postMessage(JSON.stringify({type:"consoleLog",text:"work執行緒單位思考物件初始化完成"})); 67 68 //建立單位區域索引和每個單位的初始影響範圍 69 for(var key in obj_units) 70 { 71 var one=obj_units[key]; 72 one.partx=Math.floor((one.pos.x-minx)/partSizeX); 73 one.partz=Math.floor((one.pos.z-minz)/partSizeZ); 74 arr_part[one.partx][one.partz].arr_unit.push(one);//把單位物件放到地塊物件的arr_unit屬性中 75 76 } 77 self.postMessage(JSON.stringify({type:"consoleLog",text:"work執行緒單位區域索引初始化完畢"})); 78 79 80 //self.postMessage(JSON.stringify({type:"updateUnits",obj_units0:obj_units0})); 81 self.postMessage(JSON.stringify({type:"workInited"})); 82 Loop();//這裡不等主執行緒命令直接啟動邏輯迴圈 83 84 } 85 else if(obj_data.type=="command")//主執行緒發來命令,可以直接執行邏輯執行緒中的全域性方法 86 { 87 var func=eval(obj_data.func);//對於方法物件 88 var obj_p=obj_data.obj_p; 89 //eval(func+"("+obj_p+")");//直接這樣執行obj_p會被強制轉換為字串型別!!!! 90 func(obj_p) 91 } 92 else if(obj_data.type=="setValue")//直接設定邏輯執行緒中的全域性變數 93 { 94 //var key=eval(obj_data.key);//對於直接量則會變成值而非指標!!!! 95 var key=obj_data.key 96 var value=obj_data.value; 97 eval(key+"="+value); 98 //key=value; 99 } 100 else if(obj_data.type=="updateUnits")//主執行緒同步單位狀態 101 { 102 var obj_units0=obj_data.obj_units0; 103 for(var key in obj_units0) 104 { 105 var obj0=obj_units0[key];//從渲染執行緒傳遞過來的新狀態 106 var obj=obj_units[key]; 107 obj.doing=obj0.doing;//正在做的事 108 //obj.wanting=obj0.wanting;//想要做的事 109 //this.being={};//正在遭受 110 //obj.feeling=obj0.feeling;//命令通過unitCommand傳遞! 111 obj.hp=obj0.hp; 112 obj.pos=obj0.pos;//位置變化由渲染執行緒中的群組導航計算 113 var partx=Math.floor((obj.pos.x-minx)/partSizeX); 114 var partz=Math.floor((obj.pos.z-minz)/partSizeZ); 115 if(partx!=obj.partx||partz!=obj.partz)//如果位置索引發生變化,則把單位放到新的地塊中 116 { 117 var arr_unit=arr_part[obj.partx][obj.partz]; 118 var len=arr_unit.length; 119 for(var i=0;i<len;i++) 120 { 121 if(arr_unit[i].id==obj.id) 122 { 123 arr_unit.splice(i,1); 124 break; 125 } 126 } 127 obj.partx=partx; 128 obj.partz=partz; 129 arr_part[partx][partz].arr_unit.push(obj); 130 } 131 132 } 133 } 134 else if(obj_data.type=="unitCommand")//主執行緒發來單位命令 135 { 136 var obj_units0=obj_data.obj_units0; 137 for(var key in obj_units0) 138 { 139 var obj0 = obj_units0[key];//從渲染執行緒傳遞過來的新狀態 140 var obj = obj_units[key]; 141 obj.target=obj0.target;//目標地點 142 obj.doing=obj0.doing;//正在做的事 143 obj.wanting=obj0.wanting;//想要做的事 144 //this.being={};//正在遭受 145 obj.feeling=obj0.feeling; 146 } 147 } 148 }
邏輯執行緒的資訊接收程式碼中也包含了邏輯執行緒的初始化程式碼。邏輯執行緒的初始化除了根據主執行緒發來的單位資訊在邏輯執行緒中建立單位物件外,還將邏輯執行緒中的地圖劃分為棋盤狀排列的地塊,並把每個單位和其所在的地塊關聯,這樣我們就能較為快捷的根據地塊關係找到每個單位“附近的”單位,而不用對地圖上的所有單位進行遍歷。注意,當單位移動到其他地塊時,要修改單位和地塊的對應關係。
另外,因為主執行緒的單位物件中包含不需要傳遞給邏輯執行緒的Babylon.js渲染資料(比如精靈動畫、群組、變換節點),在向邏輯執行緒同步單位狀態前要進行一次簡化處理:
1 //把單位物件叢集轉化為易於傳輸的格式,主要是剔除了網格資訊 2 One.obj2data=function(obj_them){ 3 var obj_send={}; 4 for(var key in obj_them) 5 { 6 var one=obj_them[key]; 7 var obj_p={ 8 pos:one.pos, 9 radius:one.radius, 10 view:one.view, 11 id:one.id, 12 hp:one.hp, 13 at:one.at, 14 owner:one.owner, 15 influence:one.influence, 16 doing:one.data.state.doing, 17 wanting:one.data.state.wanting, 18 feeling:one.data.state.feeling, 19 target:one.target, 20 } 21 obj_send[key]=obj_p; 22 } 23 return obj_send; 24 }
3、邏輯迴圈:
在worker.js中啟動邏輯迴圈:
1 var count_a=0; 2 function runOneStep(){//一次邏輯運算 3 count_a=0; 4 if(flag_thinking)//正在進行上一次思考 5 { 6 self.postMessage(JSON.stringify({type:"consoleError",text:"正在進行上一次思考"})); 7 return; 8 } 9 flag_thinking=true; 10 var startTime=new Date().getTime(); 11 //self.postMessage(JSON.stringify({type:"consoleLog",text:"開始計算影響力:"+startTime})); 12 OneThink.clearAllInfluence(arr_part);//清除每個單位對地塊的影響力 13 for(var key in obj_units)//影響 14 { 15 var unit=obj_units[key]; 16 if(unit.doing!="dead"&&unit.doing!="unconscious") { 17 OneThink.oneMakeInfluence(unit, arr_part, partSize);//重新計算每個單位對地塊的影響力,這件事每一週期都要做 18 } 19 } 20 //self.postMessage(JSON.stringify({type:"consoleLog",text:"開始思考:"+(new Date().getTime())})); 21 var obj_units0={}; 22 for(var key in obj_units)//思考 23 { 24 var unit=obj_units[key]; 25 if(unit.hp<=0) 26 { 27 unit.doing="dead"; 28 } 29 if(unit.doing!="dead"&&unit.doing!="unconscious"&&unit.doing!="attacking") 30 { 31 var arr_command=OneThink.think(unit,obj_units,arr_part,partSize);//單位進行思考,得出要傳送給主執行緒的命令 32 if(arr_command.length>0) 33 { 34 obj_units0[unit.id]={arr_command:arr_command};//.arr_command=arr_command; 35 } 36 37 } 38 } 39 self.postMessage(JSON.stringify({type:"unitCommand",obj_units0:obj_units0,count_a:count_a}));//把命令傳送給主執行緒 40 41 var endTime=new Date().getTime(); 42 //self.postMessage(JSON.stringify({type:"consoleLog",text:"完成思考:"+(endTime-startTime)})); 43 flag_thinking=false; 44 } 45 //Loop指令由前端發來 46 var flag_autorun=true; 47 var lastframe=new Date().getTime(); 48 function Loop()//邏輯迴圈 49 { 50 if(flag_autorun) 51 { 52 runOneStep(); 53 var thisframe=new Date().getTime(); 54 //self.postMessage(JSON.stringify({type:"consoleLog",text:thisframe-lastframe}));//把歷史演變儲存在哪裡? 55 //console.log(thisframe-lastframe,"red:"+obj_owners.red.countAlive,"blue:"+obj_owners.blue.countAlive); 56 lastframe=thisframe; 57 } 58 //self.requestAnimationFrame(function(){Loop()}); 59 self.setTimeout(function(){Loop()},500)//限制邏輯密度 60 }
這裡首先計算每個單位的影響範圍,接著計算單位在受到影響後的行為。
4、計算單位影響範圍
這裡“單位偵察到其他單位”的演算法是:被偵察單位根據其影響力的不同,對不同範圍的地塊造成影響,如果受到影響的地塊在偵察單位的視野範圍內,則認為偵察者可以發現該單位。
因此在每次邏輯計算中都需要計算單位的影響範圍(程式碼在OneThink.js檔案中):
1 //在計算每個單位的當前影響範圍前,先清空舊的影響範圍《-影響範圍變化率如果很低則這個計算會比較冗餘 2 OneThink.clearAllInfluence=function(arr_part){//影響範圍也是儲存在地塊物件中的 3 arr_part.forEach((arr,i)=>{ 4 arr.forEach((obj_part,j)=>{ 5 obj_part.arr_influence=[]; 6 }) 7 }) 8 } 9 OneThink.oneMakeInfluence=function(unit,arr_part,partSize){ 10 //考慮到不同的單位被發現的可能性不同,所以不能只從觀察者向周圍看,要先用被觀察者對周圍造成影響 11 //對於自己影響到的地塊注入影響力 12 var arr_part_found=OneThink.getArrPartbyStep(arr_part[unit.partx][unit.partz],unit.influence); 13 arr_part_found.forEach((obj)=>{//單位對周圍的地塊造成影響 14 for(var xz in obj) 15 { 16 var obj_part=obj[xz]; 17 obj_part.arr_influence.push(unit); 18 } 19 }) 20 }
OneThink.getArrPartbyStep方法的作用是,從一個地塊物件出發,尋找指定距離內的所有地塊:
1 //尋找地塊應該是直接遍歷空間內的所有地塊索引,還是根據規則從起點地塊一圈一圈查詢? 2 //這取決於一個執行緒能夠負擔多少單位的計算,假設一個地塊平均保有100個單位,估計一個執行緒最多負責計算10000個單位,也就是最多可能有100個地塊,10*10排列,最大步數不超過20 3 //根據步數尋找單位一定範圍內的地塊 4 OneThink.getArrPartbyStep=function(obj_part,step_influence) 5 { 6 var arr_part_found=[]; 7 //var depth=0; 8 for(var i=0;i<step_influence;i++)//單位本身所在的地塊也算一步 9 {//初始化查詢結果的結構 10 arr_part_found.push({});//arr_part_found是一個陣列,它的每一個元素代表一個距離上的所有地塊 11 } 12 OneThink.getArrPartbyStep2(obj_part,step_influence,'o',0,arr_part_found); 13 //引數:當前出發地塊,總查詢距離,當前查詢方向,當前已查詢距離,查詢結果物件 14 return arr_part_found; 15 } 16 OneThink.arr_direction=[0,1,2,3]//[東南北西]這樣相反的方向相加結果都是3!//["west","north","east","south"];//{"west":0,"north":1,"east":2,"south":3,"o":999}// 17 OneThink.getArrPartbyStep2=function(obj_part,step_influence,from,depth,arr_part_found) 18 { 19 if(depth<step_influence)//如果尚未達到總查詢距離 20 { 21 // var index_from=arr_direction[from]; 22 // for(var to in arr_direction) 23 // { 24 // if(to!=from) 25 // { 26 // if(obj_part[to]) 27 // { 28 // arr_part_found[depth].push(obj_part[to]); 29 // var int_temp=Math.abs(index_from-arr_direction[to]); 30 // if(int_temp==2&&int_temp>900) 31 // { 32 // depth+=2 33 // } 34 // } 35 // } 36 // } 37 OneThink.arr_direction.forEach((to,index)=>{//對於地塊的東南北西每個方向 38 if((to+from)!=3||from=="o")//比如從左向右進入下一個地塊,則下一個地塊不應重複檢測自身的西方,而從起點出發時要檢測所有方向 39 { 40 var obj_part2=obj_part[to];//之前初始化邏輯執行緒時儲存了每個地塊的周邊地塊資訊 41 if(obj_part2)出發地塊旁邊的一個地塊 42 { 43 if(!arr_part_found[depth][obj_part2.partx+"_"+obj_part2.partz])//每個地塊只新增一次 44 {//用地塊索引作為屬性名! 45 arr_part_found[depth][obj_part2.partx+"_"+obj_part2.partz]=(obj_part2); 46 //depth+=1;//再取下一層地塊 47 OneThink.getArrPartbyStep2(obj_part2,step_influence,to,depth+1,arr_part_found); 48 } 49 50 } 51 } 52 }) 53 54 55 } 56 57 }
如果使用東南北西的計算方法,通過數字索引的加減也可以遍歷周圍的地塊,但程式碼會更復雜一些。
5、單位的思考
1 //引數:單位物件、其他所有單位物件、地塊索引表 2 OneThink.think=function(unit,obj_units,arr_part){ 3 var arr_command=[];//返回的命令陣列 4 if(unit.feeling=="free")//單位自由行動時 5 { 6 var obj_part=arr_part[unit.partx][unit.partz]; 7 //遍歷所有和自己處於同一地塊的單位,以及雖處於其他地塊但影響到這一地塊的單位,這裡假定view為1?! 8 //var arr_neighbor=obj_part.arr_unit;//單位肯定會影響自己所在的地塊,所以這個其實沒有用 9 var dis_min=9999;//最小距離 10 var unit_nearest=null;//最近單位 11 12 // arr_neighbor.forEach((neighbor,i)=>{ 13 // if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner) 14 // { 15 // var dis=vxz.distance(unit.pos,neighbor.pos); 16 // if(dis_min>dis) 17 // { 18 // dis_min=dis; 19 // unit_nearest=neighbor; 20 // } 21 // } 22 // }) 23 var arr_part_found=OneThink.getArrPartbyStep(obj_part,unit.view);//這個是從0層到4層的!《-可以優化 24 var len =arr_part_found.length; 25 for(var i=0;i<len;i+=2) 26 {//距離一個單位最近的單位可能在本地塊內,也可能在距離為一的水平相鄰地塊內,也可能在距離為二的斜相鄰地塊內 27 var obj1=arr_part_found[i]; 28 var obj2=arr_part_found[i+1]; 29 for(var xz in obj1) 30 { 31 var obj_part2=obj1[xz]; 32 var arr_star=obj_part2.arr_influence; 33 arr_star.forEach((neighbor,j)=>{ 34 if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner)//找到的不是自己,且不是本勢力 35 { 36 var dis=vxz.distance(unit.pos,neighbor.pos);//兩點間距離 37 if(dis_min>dis) 38 { 39 dis_min=dis; 40 unit_nearest=neighbor; 41 } 42 } 43 }) 44 } 45 if(obj2) 46 { 47 for(var xz in obj2) 48 { 49 var obj_part2=obj2[xz]; 50 var arr_star=obj_part2.arr_influence; 51 arr_star.forEach((neighbor,j)=>{ 52 if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner) 53 { 54 var dis=vxz.distance(unit.pos,neighbor.pos); 55 if(dis_min>dis) 56 { 57 dis_min=dis; 58 unit_nearest=neighbor; 59 } 60 } 61 }) 62 } 63 } 64 if(unit_nearest)//如果在內層找到最近單位,就不用去外層檢視了 65 {//正確的做法應該是先檢視0、1、2三層地塊,如果沒找到再迴圈+2向外遍歷,這裡的做法是不正確的! 66 break; 67 } 68 } 69 // arr_part_found.forEach((obj)=>{ 70 // for(var xz in obj) 71 // { 72 // var obj_part2=obj[xz]; 73 // var arr_star=obj_part2.arr_influence; 74 // arr_star.forEach((neighbor,i)=>{ 75 // if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner) 76 // { 77 // var dis=vxz.distance(unit.pos,neighbor.pos); 78 // if(dis_min>dis) 79 // { 80 // dis_min=dis; 81 // unit_nearest=neighbor; 82 // } 83 // } 84 // }) 85 // } 86 // }) 87 88 89 if(unit_nearest)//如果找到了最近敵對單位 90 { //如果在攻擊範圍內 91 if(dis_min<unit.radius*2)//攻擊行為的優先度高於移動,但如果已經在進行其他攻擊行為則要等待上次完成 92 {//前端的動畫執行完畢,後端才扣血 93 arr_command.push({func:"attack",obj_p:unit_nearest.id});//釋出攻擊命令 94 count_a++; 95 } 96 else//如果在攻擊範圍外,則釋出移動到目標附近的命令 97 { 98 99 if(unit.doing=="walking")//如果正在走,則要保證新的目標點變化很大 100 { 101 if(vxz.distance(unit.last_post,unit_nearest.pos)>0.1) 102 {//兩次移動目標有一定差別(比如原先要去的地方的敵人已經死了)才重新發布移動命令,否則保持原移動命令不變 103 arr_command.push({func:"move",obj_p:unit_nearest.pos}) 104 } 105 } 106 else 107 { 108 arr_command.push({func:"move",obj_p:unit_nearest.pos}) 109 } 110 unit.last_post=unit_nearest.pos; 111 } 112 } 113 } 114 else if(unit.feeling=="commanded")//如果目前單位正在執行命令 115 { 116 if(unit.wanting=="Attackto")//攻擊移動 117 { 118 //先判斷到沒到目標位置! 119 var dis=vxz.distance(unit.pos,unit.target); 120 if(dis<unit.radius)//到達目標附近 121 { 122 unit.wanting="waiting";//解除命令狀態,讓單位自己決定行為 123 unit.feeling="free";// 124 return []; 125 } 126 //如果還沒到命令中的目的地,接著檢查周圍有無可攻擊單位 127 var obj_part=arr_part[unit.partx][unit.partz]; 128 var dis_min=9999; 129 var unit_nearest=null; 130 var arr_part_found=OneThink.getArrPartbyStep(obj_part,3); 131 var len =arr_part_found.length;//這個要全遍歷到 132 for(var i=0;i<len;i+=1) 133 { 134 var obj1=arr_part_found[i]; 135 for(var xz in obj1) 136 { 137 var obj_part2=obj1[xz]; 138 var arr_star=obj_part2.arr_influence; 139 arr_star.forEach((neighbor,j)=>{ 140 if(neighbor.id!=unit.id&&neighbor.owner!=unit.owner) 141 { 142 var dis=vxz.distance(unit.pos,neighbor.pos); 143 if(dis<unit.radius*2)//在攻擊範圍內 144 { 145 if(dis_min>dis) 146 { 147 dis_min=dis; 148 unit_nearest=neighbor; 149 } 150 } 151 } 152 }) 153 } 154 } 155 if(unit_nearest)//如果找到了最近敵對單位 156 { 157 arr_command.push({func:"attack",obj_p:unit_nearest.id}); 158 //count_a++;//注意,單位的wanting並未變化,所以在幹掉周圍的敵人後應該會繼續向命令目的地攻擊移動 159 } 160 else//移動到目標附近 161 { 162 163 if(unit.doing!="walking")//如果正在移動,則不重複命令 164 { 165 arr_command.push({func:"move",obj_p:unit.target}); 166 } 167 168 } 169 } 170 else if(unit.wanting=="Forceto"){//強制移動 171 //先判斷到沒到目標位置! 172 var dis=vxz.distance(unit.pos,unit.target); 173 if(dis<unit.radius)//到達目標附近 174 { 175 unit.wanting="waiting"; 176 unit.feeling="free"; 177 return []; 178 } 179 if(unit.doing!="walking")//如果正在移動,則不重複命令 180 { 181 arr_command.push({func:"move",obj_p:unit.target}); 182 } 183 } 184 } 185 return arr_command; 186 187 }
總的來講就是根據單位的不同狀態,尋找不同的目標,移動或攻擊或什麼也不做。
6、主執行緒根據單位的思考結果觸發動畫或導航指令(在One.js檔案中):
1 One.prototype.attack=function(targetid)//攻擊指令 2 { 3 var that=this; 4 var target=obj_units[targetid]; 5 if(target.hp<=0)//如果攻擊目標已經死亡 6 { 7 this.data.state.doing="waiting"; 8 return; 9 } 10 this.face=target.trf.position.subtract(this.trf.position); 11 // if(target.trf.position.x-this.trf.position.x<0) 12 // { 13 // this.mesh.invertU=true; 14 // } 15 // else 16 // { 17 // this.mesh.invertU=false; 18 // } 19 //this.mesh.stopAnimation();//停止之前的行走動畫,否則無法進入攻擊動畫《-啟動下個play會自動停止上一個 20 if(this.data.state.doing!="attacking")//等待上次攻擊完成 21 { 22 this.data.state.doing="attacking"; 23 //同時也應該停止尋路移動!?《-是的,否則會繼續向遠處走 24 MyGame.crowd.agentTeleport(this.idx, this.mesh.position);//recast的群組並沒有“停止導航”的功能,但可以使用“傳送”方法使導航停止 25 this.mesh.playAnimation(0, 6, false, 200,function(){//動畫結束後恢復思考能力 26 that.data.state.doing="waiting"; 27 var target=obj_units[targetid]; 28 if(target.data.state.doing!="dead") 29 { 30 target.hp-=2; 31 var side; 32 if(that.mesh.invertU==true) 33 { 34 side="左";//攻擊方向,受到攻擊的單位會倒向對應方向。 35 } 36 else 37 { 38 side="右"; 39 } 40 if(target.hp>0) 41 {//如果攻擊目標的生命大於0觸發受傷動畫 42 target.hurt(side); 43 } 44 else 45 {//否則觸發死亡處理 46 target.mesh.stopAnimation(); 47 target.data.state.doing="dead"; 48 target.data.state.being={}; 49 target.data.state.wanting="dead"; 50 target.data.state.feeling="dead"; 51 target.dead(side); 52 } 53 } 54 }); 55 } 56 57 58 59 } 60 One.prototype.move=function(pos)//移動命令 61 { 62 var agents = MyGame.crowd.getAgents(); 63 64 var pos_t=new BABYLON.Vector3(pos.x,pos.y,pos.z); 65 //this.face=target.trf.position.subtract(this.trf.position); 66 // if(pos_t.x-this.trf.position.x<0) 67 // { 68 // this.mesh.invertU=true; 69 // } 70 // else 71 // { 72 // this.mesh.invertU=false; 73 // } 74 if(!this.mesh.animationStarted)//過於頻繁的呼叫動畫,看起來就像沒有動畫!!並且每個動畫都不會結束,動畫的回撥也不會觸發! 75 {// 76 this.mesh.playAnimation(6, 8, true, 100); 77 } 78 79 //MyGame.crowd.agentGoto(agents[this.idx], navigationPlugin.getClosestPoint(pos_t)); 80 MyGame.crowd.agentGoto(this.idx, navigationPlugin.getClosestPoint(pos_t));//群組導航 81 } 82 One.prototype.hurt=function(side){//受傷動畫 83 //scene.stopAnimation(); 84 var ani=new BABYLON.Animation("animation_hurt_"+this.id,"angle",30 85 ,BABYLON.Animation.ANIMATIONTYPE_FLOAT,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 86 var keys=[{frame:0,value:0} 87 ,{frame:15,value:side=="左"?Math.PI/4:-Math.PI/4},{frame:30,value:0}]; 88 ani.setKeys(keys); 89 this.mesh.animations.push(ani);//把精靈旋轉一下再回來表示受到攻擊,注意這種“網格的”動畫和精靈動畫的區別 90 scene.beginAnimation(this.mesh,0,30,false,1 ); 91 } 92 One.prototype.dead=function(side){//死亡動畫 93 var ani=new BABYLON.Animation("animation_hurt_"+this.id,"angle",30 94 ,BABYLON.Animation.ANIMATIONTYPE_FLOAT,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 95 var keys=[{frame:0,value:0} 96 ,{frame:30,value:side=="左"?Math.PI/2:-Math.PI/2}]; 97 ani.setKeys(keys); 98 this.mesh.animations.push(ani); 99 var that=this; 100 scene.beginAnimation(this.mesh,0,30,false,1 ,function(){//死亡動畫結束後釋放這個單位的資源 101 // this.data.state.doing="dead"; 102 // this.data.state.being={}; 103 // this.data.state.wanting="dead"; 104 // this.data.state.feeling="dead"; 105 MyGame.crowd.removeAgent(that.idx);//移除導航物體(否則屍體會阻礙尋路),但仍保持精靈顯示 106 MyGame.crowd.update(); 107 108 setTimeout(function(){ 109 that.trf.dispose(); 110 that.mesh.dispose(); 111 that.kuang.dispose(); 112 },2000) 113 }); 114 }
絕大部分單位屬性由主執行緒修改,所以在主執行緒插入程式碼即可實現作弊。
六、總結
如此,實現了單位動畫和ai功能,下一步可以嘗試新增射彈類範圍攻擊效果和村民的建築、採集效果。
附錄一:
在國內訪問github的一種方法:
訪問DNS查詢服務,比如http://www.webkaka.com/dns/
查詢github.global.ssl.fastly.net的DNS解析:
檢視下面的列表:
發現北京電信能夠解析這個域名,於是將本機網路連線的DNS設為,北京電信的IP(203.196.0.6)