1、實驗目的:html
一、在上一篇的「RTS式單位控制」的基礎上添加邏輯線程,爲每一個單位實現ai計算;前端
二、用精靈動畫爲單位的行爲顯示對應的動做效果。node
2、運行效果:git
一、場景中的單位分爲紅藍兩方,單位在發現敵對單位後向敵人移動:github
二、進入攻擊範圍後對敵對單位發起攻擊:web
注意,單位在「移動」、「攻擊」、「受傷」、「死亡」時分別播放不一樣的動畫。ajax
三、切換爲RTS式控制後,能夠選擇單位併發布「移動攻擊」命令:算法
有一些單位已經與敵人接觸,優先執行攻擊動做。json
3、程序結構:canvas
一、工程目錄:
外層ASSETS目錄保存了場景的地面貼圖、天空盒、地形資源,內層ASSETS目錄下是單位的精靈動畫資源。(實際的github倉庫裏還有上篇文章的代碼)
二、線程結構:
主線程中的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進行通訊。
4、主線程初始化:
在以前的文章中介紹過的內容再也不贅述,這裏只討論新增的部分,可在https://github.com/ljzc002/ControlRTS下載代碼,國內訪問github的一種方法見附錄一。
一、生成單位的精靈動畫圖片:
場景中使用的精靈動畫圖片以下:
這是一張透明背景的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裏了。
二、主線程初始化流程:
下面介紹其中的新增部分
三、爲了區分不一樣勢力,在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 }
這裏由於時間有限只用簡單的紅點和藍點表示不一樣勢力,更好的方案是修改單位自己的裝備顏色,而且對不一樣勢力應用不一樣的「選中框」。
四、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的精靈對象不接受射線檢測),二是在選中單位後在單位周圍畫框,表示單位被選中(也就是第三張動圖中的白框),爲了提高渲染效率這裏使用網格實例。
五、隨機生成單位
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 }
六、啓動邏輯線程
1 function initWorker() 2 { 3 //initWorker 4 console.log("開始啓動work線程"); 5 worker = new Worker("WORK/worker.js"); 6 7 //與邏輯線程通訊的代碼稍後介紹 8 9 }
第五行代碼直接執行worker.js,這種用法與nodejs類似。
七、啓動渲染循環的代碼在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 )
須要注意的是,由於羣組導航在主線程中進行,因此只有主線程能保有單位的準確實時狀態,並每幀向邏輯線程同步,而邏輯線程則根據單位狀態,向主線程發送命令,觸發主線程中的動畫或尋路計算。
5、邏輯線程
一、在邏輯線程中引入更多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文件內部。
二、線程間通訊:
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 }
三、邏輯循環:
在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 }
這裏首先計算每一個單位的影響範圍,接着計算單位在受到影響後的行爲。
四、計算單位影響範圍
這裏「單位偵察到其餘單位」的算法是:被偵察單位根據其影響力的不一樣,對不一樣範圍的地塊形成影響,若是受到影響的地塊在偵察單位的視野範圍內,則認爲偵察者能夠發現該單位。
所以在每次邏輯計算中都須要計算單位的影響範圍(代碼在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 }
若是使用東南北西的計算方法,經過數字索引的加減也能夠遍歷周圍的地塊,但代碼會更復雜一些。
五、單位的思考
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、一、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 }
總的來說就是根據單位的不一樣狀態,尋找不一樣的目標,移動或攻擊或什麼也不作。
六、主線程根據單位的思考結果觸發動畫或導航指令(在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 }
絕大部分單位屬性由主線程修改,因此在主線程插入代碼便可實現做弊。
6、總結
如此,實現了單位動畫和ai功能,下一步能夠嘗試添加射彈類範圍攻擊效果和村民的建築、採集效果。
附錄一:
在國內訪問github的一種方法:
訪問DNS查詢服務,好比http://www.webkaka.com/dns/
查詢github.global.ssl.fastly.net的DNS解析:
查看下面的列表:
發現北京電信可以解析這個域名,因而將本機網絡鏈接的DNS設爲,北京電信的IP(203.196.0.6)