這篇文章討論如何在基於Babylon.js的WebGL場景中,創建棋盤狀的地塊和多個可選擇的棋子對象,在點選棋子時顯示棋子的移動範圍,而且在點擊移動範圍內的空白地塊時向目標地塊移動棋子。在這一過程當中要考慮不一樣棋子的移動力和影響範圍不一樣,以及不一樣地塊的移動力消耗不一樣。html
1、顯示效果:node
一、訪問https://ljzc002.github.io/CardSimulate/HTML/TEST3tiled.html查看「棋盤測試頁面」:git
場景中是一個20*20的棋盤,地塊隨機爲草地、土地、雪地,棋盤中央是四個「棋子」(用卡牌網格對象客串)。使用鼠標和wasd、Shift、空格鍵控制相機網格對象在場景中漫遊,4個棋子會努力讓本身面朝相機。github
二、點擊一個棋子或棋子所在的地塊,棋子將被選中並顯示棋子的可移動範圍和影響範圍:web
棋子能夠到達的地塊覆蓋藍色半透明遮罩,棋子不能到達但能夠影響的地塊覆蓋紅色半透明遮罩。這裏規定「銅卡」的移動力爲十、影響範圍爲2,「銀卡」的移動力爲1五、影響範圍爲3,草地、泥地、雪地對移動力的消耗分別爲二、三、4.算法
三、點擊藍色地塊,將用黃色半透明遮罩標出棋子到達目標地塊的路徑,點擊紅色地塊,則標出到達距這個紅色地塊最近的藍色地塊的路徑:編程
四、點擊黃色地塊,則棋子緩緩移動到目標黃色地塊,到達目標後,在棋子周圍顯示棋子的影響範圍:canvas
在更新版的程序裏,取消了棋子下面的紅色遮罩數組
五、點擊棋子能夠將鏡頭拉近到棋子附近;點擊影響範圍外的地塊,能夠取消棋子的選定;點擊其餘的棋子或者含有其餘棋子的地塊,能夠改變選定的棋子。瀏覽器
2、代碼實現:
這個棋盤場景是在上一個卡牌場景(https://www.cnblogs.com/ljzc002/p/9660676.html)的基礎上改進而來的,這裏只討論改變和新增的代碼中比較重要的部分,大部分新增方法在Tiled.js中。
一、在initArena中創建棋盤:
1 mesh_tiledGround=new BABYLON.Mesh("mesh_tiledGround", scene);//這是全部地塊的父網格也是全部棋盤上的棋子的爺爺網格 2 mesh_tiledGround.position.y=-7;//設定棋盤高度 3 MakeTileds2(0,20,20);//產生正方形的棋盤網格,20*20大小。
MakeTileds2方法內容以下:
1 arr_tilednodes=[];//一個二維數組,保存棋盤中的每一個地塊對象 2 mesh_tiledCard=null;//全部棋子對象的父網格,是mesh_tiledGround的子元素 3 function MakeTileds2(type,sizex,sizez)//換一種地塊構造方式,想到tiledGround事實上並無必要性,若是忽略掉性能上可能存在的優點 4 { 5 //給幾種遮罩層創建材質:藍色、紅色、黃色、綠色、全透明 6 var mat_alpha_blue=new BABYLON.StandardMaterial("mat_alpha_blue", scene); 7 mat_alpha_blue.diffuseTexture = new BABYLON.Texture("../ASSETS/IMAGE/LANDTYPE/alpha_blue.png",scene); 8 mat_alpha_blue.diffuseTexture.hasAlpha=true;//聲明漫反射紋理圖片具備透明度 9 mat_alpha_blue.useAlphaFromDiffuseTexture=true;//啓用漫反射紋理的透明度 10 //mat_alpha_blue.hasVertexAlpha=true; 11 //mat_alpha_blue.diffuseColor = new BABYLON.Color3(0, 0,1); 12 //mat_alpha_blue.alpha=0.2;//不透明度 13 mat_alpha_blue.useLogarithmicDepth=true;//爲了和卡牌之間正常顯示,它也必須這樣設置深度? 14 MyGame.materials.mat_alpha_blue=mat_alpha_blue; 15 var mat_alpha_red=new BABYLON.StandardMaterial("mat_alpha_red", scene); 16 mat_alpha_red.diffuseTexture = new BABYLON.Texture("../ASSETS/IMAGE/LANDTYPE/alpha_red.png",scene); 17 mat_alpha_red.diffuseTexture.hasAlpha=true; 18 mat_alpha_red.useAlphaFromDiffuseTexture=true; 19 //mat_alpha_red.diffuseColor = new BABYLON.Color3(1, 0,0); 20 //mat_alpha_red.alpha=0.2;//不透明度 21 mat_alpha_red.useLogarithmicDepth=true; 22 MyGame.materials.mat_alpha_red=mat_alpha_red; 23 var mat_alpha_green=new BABYLON.StandardMaterial("mat_alpha_green", scene); 24 mat_alpha_green.diffuseTexture = new BABYLON.Texture("../ASSETS/IMAGE/LANDTYPE/alpha_green.png",scene); 25 mat_alpha_green.diffuseTexture.hasAlpha=true; 26 mat_alpha_green.useAlphaFromDiffuseTexture=true; 27 //mat_alpha_green.diffuseColor = new BABYLON.Color3(0, 1,0); 28 //mat_alpha_green.alpha=0.2;//不透明度 29 mat_alpha_green.useLogarithmicDepth=true; 30 MyGame.materials.mat_alpha_green=mat_alpha_green; 31 var mat_alpha_yellow=new BABYLON.StandardMaterial("mat_alpha_yellow", scene); 32 mat_alpha_yellow.diffuseTexture = new BABYLON.Texture("../ASSETS/IMAGE/LANDTYPE/alpha_yellow.png",scene); 33 mat_alpha_yellow.diffuseTexture.hasAlpha=true; 34 mat_alpha_yellow.useAlphaFromDiffuseTexture=true; 35 //mat_alpha_yellow.diffuseColor = new BABYLON.Color3(1, 1,0); 36 //mat_alpha_yellow.alpha=0.2;//不透明度 37 mat_alpha_yellow.useLogarithmicDepth=true; 38 MyGame.materials.mat_alpha_yellow=mat_alpha_yellow; 39 var mat_alpha_null=new BABYLON.StandardMaterial("mat_alpha_null", scene);//或者直接將遮罩設爲不可見? 40 mat_alpha_null.diffuseColor = new BABYLON.Color3(1, 1,1); 41 mat_alpha_null.alpha=0;//不透明度 42 mat_alpha_null.useLogarithmicDepth=true; 43 MyGame.materials.mat_alpha_null=mat_alpha_null; 44 45 mesh_tiledCard=new BABYLON.Mesh("mesh_tiledCard",scene);//全部單位的父元素 46 mesh_tiledCard.parent=mesh_tiledGround; 47 if(type==0)// 兩層循環 48 { 49 var obj_p={xmin:-30,xmax:30,zmin:-30,zmax:30,precision :{"w" : 2,"h" : 2},subdivisions:{"w" : sizex,"h" : sizez} 50 }; 51 var heightp=(obj_p.zmax-obj_p.zmin)/sizez;//每個小塊的高度 52 var widthp=(obj_p.xmax-obj_p.xmin)/sizex; 53 obj_p.heightp=heightp; 54 obj_p.widthp=widthp; 55 mesh_tiledGround.obj_p=obj_p;//將地塊的初始化參數記錄下來 56 57 //認爲行數從上向下延伸,列數從左向右延伸 58 for(var i=0;i<sizez;i++)//從0開始仍是從1開始?? 59 {//對於每一列?->仍是一行一行處理更好 60 var z=obj_p.zmax-(heightp*i+0.5*heightp); 61 var arr_rownodes=[]; 62 for(var j=0;j<sizex;j++) 63 { 64 var x=obj_p.xmin+(widthp*j+0.5*widthp); 65 //創建一個顯示地面紋理的地塊,須要把地塊也作成一個類嗎? 66 var mesh_tiled=new BABYLON.MeshBuilder.CreateGround("mesh_tiled_"+i+"_"+j 67 ,{width:widthp,height:heightp,subdivisionsX : 2,subdivisionsY : 2,updatable:false},scene); 68 mesh_tiled.index_row=i; 69 mesh_tiled.index_col=j; 70 mesh_tiled.heightp=heightp; 71 mesh_tiled.widthp=widthp; 72 mesh_tiled.position.z=z; 73 mesh_tiled.position.x=x; 74 mesh_tiled.position.y=-1;//略低一點,使地塊位於棋子的下面 75 mesh_tiled.parent=mesh_tiledGround; 76 mesh_tiled.renderingGroupId=2; 77 //隨機給這個地塊分配一種地形,參考DataWar的方式?? 78 var landtype=newland.RandomChooseFromObj(arr_landtypes);//從地形列表裏等機率的選取一種地形 79 mesh_tiled.landtype=landtype.name;//地形名稱 80 mesh_tiled.cost=arr_landtypes[landtype.name].cost;//這種地形的消耗 81 if(MyGame.materials["mat_"+landtype.name])//若是已經建立過這種類型的材質,則直接將材質交給網格 82 { 83 mesh_tiled.material=MyGame.materials["mat_"+landtype.name]; 84 } 85 else 86 {//不然創建這種地形材質並交個網格 87 var mat_tiled = new BABYLON.StandardMaterial("mat_"+landtype.name,scene); 88 mat_tiled.diffuseTexture = new BABYLON.Texture(landtype.Url,scene); 89 mat_tiled.useLogarithmicDepth=true; 90 MyGame.materials["mat_"+landtype.name]=mat_tiled; 91 mesh_tiled.material=mat_tiled; 92 } 93 var mesh_mask=new BABYLON.MeshBuilder.CreatePlane("mesh_mask_"+i+"_"+j 94 ,{width:widthp-0.1,height:heightp-0.1},scene);//爲每個地塊創建一個遮罩網格 95 mesh_mask.material=MyGame.materials.mat_alpha_null;//在不顯示範圍時,全部的遮罩默認不可見 96 mesh_mask.parent=mesh_tiled; 97 mesh_tiled.mask=mesh_mask; 98 mesh_mask.rotation.x=Math.PI*0.5; 99 mesh_mask.position.y=0.1; 100 mesh_mask.renderingGroupId=2; 101 mesh_mask.isPickable=false;//遮罩只用來顯示,是不接收鼠標點擊事件的 102 arr_rownodes.push(mesh_tiled); 103 } 104 arr_tilednodes.push(arr_rownodes); 105 } 106 } 107 }
這段代碼首先設計了棋盤、地塊、棋子網格之間的從屬關係。而後在5到43行創建了表示地塊不一樣狀態的幾種遮罩材質,最初使用
帶有透明度的純色材質(被註釋掉的部分),後來發現純色的半透明遮罩容易和地塊顏色混淆,而且不一樣地塊之間的邊界不夠分明,因而改成使用半透明圖片做爲遮罩的紋理。使用canvas生成半透明圖片的代碼以下:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>用canvas生成含有半透明度的PNG圖片</title> 6 </head> 7 <body> 8 <div id="div_allbase"> 9 <canvas style="width: 512px;height: 512px" width="512" height="512" id="can_pic"> 10 11 </canvas> 12 </div> 13 </body> 14 <script> 15 var canvas=document.getElementById("can_pic"); 16 window.onload=loadImage; 17 function loadImage() 18 { 19 var context=canvas.getContext("2d"); 20 context.fillStyle="rgb(0,255,0)"; 21 context.fillRect(0,0,512,512); 22 drawRoundRect(context, 16, 16, 480, 480, 32);//創建一個路徑,上下文、距canvas左上角距離、寬高、圓角半徑 23 //context.strokeStyle = "#ff0000"; 24 context.fillStyle="rgb(255,255,255)"; 25 //context.stroke();//繪製路徑 26 context.fill();//填充路徑,關閉了路徑也能夠填充和繪製路徑 27 //帶有透明度的覆蓋並非替換顏色,而是混合顏色,好比0 0 255 255與0 255 0 64的混合結果是0 64 255 255,其實質應該是根據不透明度加權求和 28 //因此爲了精確的設置顏色,仍是經過ImageData逐像素設置比較好。 29 30 var imagedata_temp=context.getImageData(0,0,512,512);//規定地貌塊紋理圖片的寬高是512 31 var data=imagedata_temp.data; 32 var len=data.length; 33 for(var i=0;i<len;i+=4)//對於每個像素 34 { 35 if(data[i]==255&&data[i+1]==255&&data[i+2]==255)//若是是純白色 36 { 37 data[i]=0; 38 data[i+1]=255; 39 data[i+2]=0; 40 data[i+3]=64; 41 } 42 else 43 { 44 data[i+3]=192; 45 } 46 47 } 48 context.putImageData(imagedata_temp,0,0); 49 } 50 //網上找到的生成圓角矩形路徑的方法 51 function drawRoundRect(cxt, x, y, width, height, radius){ 52 cxt.beginPath(); 53 cxt.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 3 / 2); 54 cxt.lineTo(width - radius + x, y); 55 cxt.arc(width - radius + x, radius + y, radius, Math.PI * 3 / 2, Math.PI * 2); 56 cxt.lineTo(width + x, height + y - radius); 57 cxt.arc(width - radius + x, height - radius + y, radius, 0, Math.PI * 1 / 2); 58 cxt.lineTo(radius + x, height +y); 59 cxt.arc(radius + x, height - radius + y, radius, Math.PI * 1 / 2, Math.PI); 60 cxt.closePath(); 61 } 62 63 </script> 64 </html>
其大概思路是首先用特定顏色在canvas中標誌出一塊區域,而後遍歷canvas中的像素,將符合特定顏色的像素修改成須要的rgba顏色。
接下來根據設定的尺寸,爲每一個地塊生成一個網格,再爲每一個地塊網格生成一個遮罩網格,經過爲遮罩網格設置不一樣的材質來表示地塊網格的不一樣狀態,而遮罩網格比地塊網格略小一點,這可讓遮罩之間的界限更清晰。生成地塊時用到的這種按行、按列遍歷計算元素位置的算法,在圖像處理和表格繪製程序中也很經常使用,要考慮將它封裝爲一個通用方法。
值得注意的是,Babylon.js內部也封裝有一個創建「棋盤網格」的方法,可是我並無發現這個方法的優點在哪裏,反而由於全部地塊無差異的合併在一個網格對象中致使對象選取困難,同時這種內置的棋盤網格只支持方形棋盤沒法自定義棋盤形狀。
RandomChooseFromObj方法的做用是,隨機選擇對象屬性中的一個返回,其代碼以下:
1 newland.RandomChooseFromObj=function(obj)//隨機從一個對象的全部屬性中按照機率選擇一個屬性 2 { 3 var len=Object.getOwnPropertyNames(obj).length;//全部屬性的個數 4 var count_rate=0; 5 var num=Math.random(); 6 var result=null; 7 for(var key in obj) 8 { 9 var ratep=1/len; 10 var pro=obj[key]; 11 if(pro.rate) 12 { 13 ratep=pro.rate; 14 } 15 count_rate+=ratep; 16 if(count_rate>num) 17 { 18 result=pro; 19 return result;//理論上講總會從這裏返回一個 20 } 21 } 22 return "fault"; 23 }
二、將鼠標點擊事件重構爲CameraClick方法,將代碼從CameraMesh類裏移出,放置在CameraClick.js文件中:
1 //專門處理相機點擊事件 2 function CameraClick(_this,evt) 3 { 4 if(MyGame.init_state==1||MyGame.init_state==2)//點擊canvas則鎖定光標,在由於某種緣由在first_lock狀態脫離焦點後用來恢復焦點 5 {//不鎖定指針時,這個監聽什麼也不作 6 if(MyGame.flag_view!="first_pick") 7 { 8 canvas.requestPointerLock = canvas.requestPointerLock || canvas.msRequestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock; 9 if (canvas.requestPointerLock) {//用於鼠標意外離開瀏覽器後從新鎖定光標 10 canvas.requestPointerLock(); 11 12 MyGame.flag_view="first_lock"; 13 14 _this.centercursor.isVisible=true; 15 } 16 if(MyGame.init_state==1) 17 { 18 var width = engine.getRenderWidth(); 19 var height = engine.getRenderHeight(); 20 var pickInfo = scene.pick(width/2, height/2, null, false, MyGame.Cameras.camera0); 21 if(pickInfo.hit&&pickInfo.pickedMesh.name.substr(0,5)=="card_")//根據網格的名字判斷 22 {//點擊棋盤上的一張卡,認爲這時不可多選,而且一樣能夠點擊其餘人的卡片,但只能控制本身的卡片(?) 23 cancelPropagation(evt); 24 cancelEvent(evt); 25 var mesh=pickInfo.pickedMesh; 26 var card=mesh.card; 27 PickCard2(card);//在棋盤上點擊卡片 28 } 29 else if(pickInfo.hit&&pickInfo.pickedMesh.name.substr(0,6)=="mesh_t") 30 {//若是點擊在地塊上,若是是第一次點擊則顯示路徑,用粒子效果?若是已經計算了路徑則表示路徑確認,經過動畫按路徑移動 31 PickTiled(pickInfo); 32 } 33 } 34 } 35 else//在非鎖定光標(first_pick)時,click監聽彷佛不會被相機阻斷,而mousedown會被相機阻斷 36 { 37 if(MyGame.flag_view=="first_ani")//由程序控制視角的動畫時間 38 { 39 cancelPropagation(evt); 40 cancelEvent(evt); 41 return; 42 } 43 //var width = engine.getRenderWidth(); 44 //var height = engine.getRenderHeight(); 45 var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, MyGame.Cameras.camera0);//點擊信息,取屏幕中心信息而不是鼠標信息!! 46 if(MyGame.init_state==1&&MyGame.flag_view=="first_pick" 47 &&pickInfo.hit&&pickInfo.pickedMesh.name.substr(0,5)=="card_"&&pickInfo.pickedMesh.card.belongto==MyGame.WhoAmI)//在一個卡片上按下鼠標,按下即被選中 48 {//點擊手牌中的一張卡片 49 cancelPropagation(evt); 50 cancelEvent(evt); 51 //releaseKeyState(); 52 var mesh=pickInfo.pickedMesh; 53 var card=mesh.card; 54 PickCard(card); 55 } 56 57 } 58 } 59 }
三、點擊棋子的處理:
1 function PickCard2(card)//點擊一下選中,高亮邊緣,在非選中狀態使用2D視角跟隨,仍是3D視角跟隨?,再點擊一下則拉近放大,是否要調整視角跟隨方式? 2 //同時還要在卡片附近創建一層藍色或紅色的半透明遮罩網格,表示移動及影響範圍 3 {//若是再次點擊有已選中卡片,則把相機移到卡片面前 4 if(card.isPicked) 5 { 6 GetCardClose2(card); 7 //DisposeRange();//隱藏範圍顯示,規定點擊棋盤時計算到達路徑,點擊空處時清空範圍,點擊其餘卡牌時切換範圍,切換成手牌時清空範圍 8 } 9 else 10 { 11 //getPicked(card);//考慮到選擇新的棋子前要先清空已選中的棋子,這三句放在後面執行 12 //card.isPicked=true;//設爲被選中卡片併爲它計算範圍 13 //card_Closed2=card;//card_Closed2是保存當前選定的棋子的全局變量 14 DisplayRange(card); 15 } 16 }
當這個棋子已經被選中時,再次點擊這個棋子將把相機移動到棋子面前,其代碼以下:
1 function GetCardClose2(card)//讓相機靠近card!!?? 2 { 3 MyGame.flag_view="first_ani"; 4 MyGame.anicount=2;//若是開啓了多個物體的動畫,要肯定這些物體的動畫都結束再退出動畫狀態 5 var pos_card=card.mesh._absolutePosition.clone();//獲取相機對象的世界座標系位置 6 var pos_camera=MyGame.player.mesh.position.clone();//相機對象的局部座標系位置,應該等於世界座標系位置 7 var pos=pos_card.clone().add(pos_camera.clone().subtract(pos_card).normalize().scale(3)); 8 var animation3=new BABYLON.Animation("animation3","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 9 var keys1=[{frame:0,value:MyGame.player.mesh.position.clone()},{frame:30,value:pos}]; 10 animation3.setKeys(keys1); 11 12 13 var rot_camera=MyGame.player.mesh.rotation.clone(); 14 var tran_temp=new BABYLON.Mesh("tran_temp",scene);//Babylon.js的「變換節點」類對象可能更適合 15 tran_temp.position=pos;//建立一個位於棋子面前的「暫時網格」,讓這個網格朝向棋子,而後獲取這個網格的姿態 16 tran_temp.lookAt(pos_card,Math.PI,0,0);//,Math.PI,Math.PI);YXZ? 17 var rot=tran_temp.rotation.clone();//看起來這個rot是反向的,如何把它正過來? 18 rot.x=-rot.x; 19 //MyGame.PI2=Math.PI*2; 20 //rot.x=(rot.x-Math.PI)%MyGame.PI2; 21 //rot.y=(rot.y-Math.PI)%MyGame.PI2; 22 //rot.z=0;//出現了奇怪的座標反向 23 tran_temp.dispose(); 24 var animation4=new BABYLON.Animation("animation4","rotation",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 25 var keys2=[{frame:0,value:rot_camera},{frame:30,value:rot}]; 26 animation4.setKeys(keys2); 27 MyGame.player.mesh.animations.push(animation3);//mesh和camera必須使用相同的動畫? 28 //MyGame.Cameras.camera0.animations.push(animation3); 29 MyGame.Cameras.camera0.animations.push(animation4); 30 //MyGame.player.mesh.animations.push(animation4); 31 scene.beginAnimation(MyGame.player.mesh, 0, 30, false,1,function(){ 32 MyGame.anicount--; 33 if(MyGame.anicount==0) 34 { 35 MyGame.flag_view="first_lock"; 36 } 37 }); 38 scene.beginAnimation(MyGame.Cameras.camera0, 0, 30, false,1,function(){ 39 MyGame.anicount--; 40 if(MyGame.anicount==0) 41 { 42 MyGame.flag_view="first_lock"; 43 } 44 }); 45 }
代碼的第七行計算了「棋子面前」的位置,其位置是「從棋子位置出發,向相機位置移動3單位距離」。接下來是計算相機移動到棋子面前時的朝向,Babylon.js中的lookAt方法可使網格朝向某個指定的世界座標系位置,可是其實際效果彷佛和文檔存在出入,通過反覆試驗找到了一種可行的用法,但並不肯定原理。上述變換的示意圖以下:
而後是把相機移動後的位置 設爲「相機網格類」的網格 的位置動畫的關鍵幀,將相機移動後的姿態 設爲相機網格類的相機 的姿態動畫的關鍵幀,並執行動畫。在渲染循環中,相機網格對象的相機會應用網格的位置,而網格則會應用相機的姿態。這樣設置的緣由能夠參考上篇文章中對CameraMesh類的介紹。
四、計算並顯示棋子的移動範圍和影響範圍:
a、準備工做:
1 arr_nodepath={};//使用它保存移動範圍內每個節點的消耗值與移動路徑。這個變量是冗餘的嗎?-》不是 2 arr_DisplayedMasks=[];//保存每一個顯示的遮罩對象 3 arr_noderange={};//保存每一個可能被影響的節點(紅色材質),它不能夠包含arr_nodepath中的節點 4 function DisplayRange(card)//顯示這個card的範圍 5 { 6 //首先要檢查是否有已經顯示的遮罩 7 if(arr_DisplayedMasks.length>0) 8 { 9 HideAllMask();//這裏也會清空card_Closed2 10 } 11 card_Closed2=card;//由於HideAllMask會清空已選中的棋子,因此切換棋子時的棋子選定代碼應放在這裏。 12 getPicked(card_Closed2); 13 card.isPicked=true; 14 if(card.workstate!="wait") 15 { 16 return;//若是不在待命狀態則不予顯示範圍遮罩 17 } 18 var node_start=FindNode(card.mesh.position);//找到點擊的棋子所在的格子 19 //var str=node_start.name; 20 arr_nodepath={};//將移動範圍數據清空,而後將第一個地塊(節點)放入 21 arr_noderange={};//將影響範圍數據清空 22 arr_nodepath[node_start.name]={cost:0,path:[node_start.name],node:node_start}; 23 //arr_nodepath={str:{cost:0,path:[node_start.name]}}; 24 //node_start.open=true; 25 var list_node=[];//須要依次計算的節點列表 26 list_node.push(node_start);//一開始節點列表裏只有第一個地塊(起點) 27 var power=card.speed;//把卡牌的速度屬性做爲移動力 28 var costg=0;//消耗計量器,計算要分紅兩段,第一段是移動範圍,第二段是影響範圍(超過移動範圍以後,全部地塊消耗都視爲1) 29 //var path=[node_start.name];//只在路徑裏保存名稱,這樣能夠用concat??
這一段代碼初始化了範圍計算所需的各項數據,其中FindNode方法的做用是根據棋子的位置尋找棋子所在的地塊,其代碼以下:
1 function FindNode(pos)//根據pos找到對應的地塊 2 { 3 var obj_p=mesh_tiledGround.obj_p; 4 var num_row=Math.floor((obj_p.zmax-pos.z)/obj_p.heightp);//暫時不考慮卡牌脫出棋盤以外的狀況 5 var num_col=Math.floor((pos.x-obj_p.xmin)/obj_p.widthp); 6 var node=arr_tilednodes[num_row][num_col]; 7 return node; 8 }
b、計算棋子的移動範圍:
在編程過程當中我發現list_node和arr_nodepath保存的數據存在重合,可是一方面要經過list_node順序的遍歷節點,另外一方面又要在後續的代碼中經過名字訪問arr_nodepath中的數據,因此決定同時使用數組和對象這兩種數據結構。
1 for(var i=0;i<list_node.length;i++)//這種變長的順序遍歷須要使用數組,然後面的按名稱選擇又要用到對象屬性-》因此保持兩套變量???? 2 {//對於節點列表中的每一個節點,把它叫作「中央節點」把 3 var arr_node_neighbor=FindNeighbor(list_node[i]);//找到它周圍的全部節點 4 var len=arr_node_neighbor.length;// 5 for(var j=0;j<len;j++)//對於每個鄰居節點 6 { 7 var nextnode=arr_node_neighbor[j]; 8 costg=arr_nodepath[list_node[i].name].cost;//到達中央節點的消耗 9 //在計算移動時有兩個思路,一是設定每一種地面的行動力消耗,二是設定每一種單位對每一種地形的行動能力,看來第一種更簡單 10 //認爲最初的起點消耗爲0 11 costg+=nextnode.cost;//認爲到達這個鄰居節點的消耗是:到達中央節點的消耗+這個鄰居節點的消耗 12 //path.push(nextnode); 13 var path2=arr_nodepath[list_node[i].name].path.concat();//到達中央節點的路徑 14 path2.push(nextnode.name);//加入這個鄰居節點 15 if(costg>power)//若是消耗超過了移動力,則認爲這個鄰居節點是經過這條路徑所沒法到達的 16 { 17 if(arr_nodepath[nextnode.name])//若是使用其餘路徑可以到達這個節點 18 { 19 continue;//考慮下一個鄰居 20 } 21 else//若是超過移動範圍,則將這個移動邊界節點做爲考慮影響範圍時的一個起點 22 { 23 arr_noderange[nextnode.name]={cost:1,path:[nextnode.name],path0:path2,node:nextnode};//那麼這個點多是影響範圍內的起始節點 24 } 25 } 26 else 27 {//若是能夠到達這個節點 28 29 30 if(arr_nodepath[nextnode.name])//若是已經到達過這個節點,則要對消耗進行比較 31 { 32 if(arr_nodepath[nextnode.name].cost>costg)//找到了到達這個點的更優方式 33 {//替換原先記錄的到達這個節點的路徑和消耗 34 arr_nodepath[nextnode.name]={cost:costg,path:path2,node:nextnode}; 35 } 36 else{//新的到達這個節點的方式並不更優 37 continue;//考慮下一個鄰居 38 } 39 } 40 else//若是從未到達這個節點,則要計算到這個節點爲止的消耗 41 { 42 if(arr_noderange[nextnode.name])//若是這個節點在之前被設爲移動邊界節點,但又被證實能夠達到 43 { 44 delete arr_noderange[nextnode.name]; 45 } 46 arr_nodepath[nextnode.name]={cost:costg,path:path2,node:nextnode}; 47 list_node.push(nextnode);//第一次到達這個節點,則把這個節點加入節點列表,節點列表長度加一,接下來再使用這些新加入的節點做爲中央節點計算範圍 48 } 49 } 50 } 51 }//節點列表遍歷完成時,arr_nodepath中就保存了到達移動範圍內的每一個節點的路徑和消耗
其中FindNeighbor方法用來尋找中央節點上下左右的四個「鄰居節點」:
1 function FindNeighbor(node)//尋找一個地塊周圍的全部地塊(最多四個) 2 { 3 var arr_node_neighbor=[] 4 var total_row=arr_tilednodes.length;//棋盤有多少行 5 var total_col=arr_tilednodes[0].length;//棋盤有多少列 6 var index_row=node.index_row; 7 var index_col=node.index_col; 8 //上面的 9 var i=index_row-1; 10 if(i>=0)//若是不超出棋盤範圍 11 { 12 arr_node_neighbor.push(arr_tilednodes[i][index_col]); 13 } 14 //右面的 15 i=index_col+1; 16 if(i<total_col) 17 { 18 arr_node_neighbor.push(arr_tilednodes[index_row][i]); 19 } 20 //下面的 21 i=index_row+1; 22 if(i<total_row) 23 { 24 arr_node_neighbor.push(arr_tilednodes[i][index_col]); 25 } 26 //左面的 27 i=index_col-1; 28 if(i>=0) 29 { 30 arr_node_neighbor.push(arr_tilednodes[index_row][i]); 31 } 32 return arr_node_neighbor; 33 }
c、計算棋子影響範圍:
計算方式和前面計算移動範圍的算法是類似的,只有一點小區別。
1 //尋找單位的影響範圍 2 var range=card.range;//將卡牌對象的範圍屬性做爲棋子的影響範圍 3 var list_noderange=[];//計算範圍的節點列表 4 for(var key in arr_noderange) 5 {//將前面收集的邊界節點放入節點列表 6 list_noderange.push(arr_noderange[key].node) 7 } 8 for(var i=0;i<list_noderange.length;i++)//遍歷節點列表 9 { 10 var arr_node_neighbor=FindNeighbor(list_noderange[i]); 11 var len=arr_node_neighbor.length; 12 for(var j=0;j<len;j++)//對於每個鄰居節點 13 { 14 costg=arr_noderange[list_noderange[i].name].cost; 15 costg+=1;//認爲每一個地塊的影響消耗都爲1 16 if(costg>range) 17 { 18 break;//由於影響範圍的cost都是相同的,因此只要有一個鄰居超過限度,則全部鄰居都不可用 19 } 20 //若是沒有超限 21 var nextnode = arr_node_neighbor[j]; 22 if(arr_nodepath[nextnode.name])//若是這個節點在可到達區域,則必然不在範圍區域 23 { 24 continue; 25 } 26 else 27 { 28 var path2=arr_noderange[list_noderange[i].name].path.concat();//從起始點去這個中央節點的路徑 29 path2.push(nextnode.name); 30 if(arr_noderange[nextnode.name])//若是之前曾經到達這個節點 31 { 32 if(arr_noderange[nextnode.name].cost>costg) 33 { 34 arr_noderange[nextnode.name]={cost:costg,path:path2,node:nextnode,path0:arr_noderange[list_noderange[i].name].path0}; 35 } 36 else 37 { 38 continue; 39 } 40 } 41 else 42 { 43 arr_noderange[nextnode.name]={cost:costg,path:path2,node:nextnode,path0:arr_noderange[list_noderange[i].name].path0}; 44 list_noderange.push(nextnode); 45 } 46 } 47 } 48 }//遍歷完成時arr_noderange裏包含了影響範圍內的每一個節點的信息,其中path0是到達最近的(之一)邊界節點的路徑,path2是到達影響節點的路徑。 49 DisplayAllMask() 50 51 }
計算完成後使用DisplayAllMask方法,將移動範圍和影響範圍顯示出來:
1 function DisplayAllMask()//繪製出移動範圍和影響範圍的遮罩 2 { 3 for(var key in arr_nodepath) 4 { 5 if(arr_nodepath[key].cost>0) 6 { 7 arr_nodepath[key].node.mask.material=MyGame.materials.mat_alpha_blue;//藍色表示移動範圍 8 } 9 arr_DisplayedMasks.push(arr_nodepath[key].node.mask); 10 } 11 for(var key in arr_noderange) 12 { 13 arr_noderange[key].node.mask.material=MyGame.materials.mat_alpha_red;//紅色表示影響範圍 14 arr_DisplayedMasks.push(arr_noderange[key].node.mask); 15 } 16 }
五、點擊地塊的處理:
考慮到點擊棋子可能比較困難,這裏設定爲點擊棋子所在的地塊也能選中棋子;另外,遮罩網格只是起顯示做用,在選中棋子以後,也要經過監聽地塊的點擊事件來決定棋子的移動目標。
1 function PickTiled(pickInfo)//點擊地塊 2 { 3 //不管是否有範圍遮罩,點擊地塊就顯示地塊屬性?-》下一步添加 4 var mesh=pickInfo.pickedMesh; 5 if(arr_DisplayedMasks.length>0&&card_Closed2)//若是存在地塊遮罩,而且有選中的單位 6 { 7 //若是點擊的另外一個地塊裏已經有一個單位,這裏認爲一個地塊只能有一個單位,因此要切換被選中的單位 8 var mesh_unit=TiledHasCard(mesh);//找到被點擊的地塊中的棋子 9 if(mesh_unit)//若是找到了 10 { 11 if(mesh_unit.name!=card_Closed2.mesh.name) 12 { 13 PickCard2(mesh_unit.card);//替換選中的棋子 14 } 15 else//若是點擊的是本身的地塊!!拉近卡片 16 { 17 GetCardClose2(mesh_unit.card); 18 } 19 return; 20 } 21 //若是沒有點擊到別的單位的地塊 22 //點擊影響範圍也自動尋路過去? 23 //if(arr_noderange[mesh.name])//若是在影響範圍內 24 if(mesh.mask.material.name=="mat_alpha_red")//若是點擊到紅色地塊 25 { 26 //先清空可能存在的黃色路徑 27 for(var key in arr_noderange) 28 { 29 var node=arr_noderange[key].node; 30 if(node.mask.material.name=="mat_alpha_yellow") 31 { 32 node.mask.material=MyGame.materials.mat_alpha_blue; 33 } 34 } 35 if(card_Closed2.workstate=="wait")//若是棋子處在等待狀態,點擊紅地塊是移動到相應移動邊界的意思 36 { 37 var path=arr_noderange[mesh.name].path0;//取到達這一點的路徑,將對應地塊置爲黃色 38 var len=path.length; 39 for(var i=0;i<len;i++) 40 { 41 if(arr_nodepath[path[i]]&&!TiledHasCard(arr_nodepath[path[i]].node)) 42 { 43 arr_nodepath[path[i]].node.mask.material=MyGame.materials.mat_alpha_yellow;//走過的路徑地塊標爲黃色 44 } 45 } 46 } 47 else if(card_Closed2.workstate=="moved")//若是已經移動了,那麼此次點擊就是發動效果 48 { 49 50 } 51 } 52 else if(mesh.mask.material.name=="mat_alpha_blue")//若是這個被點擊的地塊在選中單位的移動範圍內 53 {//點擊了藍色地塊 54 //先清空可能存在的黃色路徑 55 for(var key in arr_noderange) 56 { 57 var node=arr_noderange[key].node; 58 if(node.mask.material.name=="mat_alpha_yellow") 59 { 60 node.mask.material=MyGame.materials.mat_alpha_blue; 61 } 62 } 63 var path=arr_nodepath[mesh.name].path;//取到達這一點的路徑 64 var len=path.length; 65 for(var i=0;i<len;i++) 66 { 67 if(arr_nodepath[path[i]]&&!TiledHasCard(arr_nodepath[path[i]].node))//有單位存在的格子不置黃 68 { 69 arr_nodepath[path[i]].node.mask.material=MyGame.materials.mat_alpha_yellow;//走過的路徑地塊標爲黃色 70 } 71 } 72 } 73 else if(mesh.mask.material.name=="mat_alpha_yellow")//若是點擊的是黃色地塊,則移動到目標地塊 74 { 75 var path=arr_nodepath[mesh.name].path;//取到達這一點的路徑,點到黃色地塊的路徑必然是可通行的?? 76 CardMove2Tiled(path); 77 } 78 else//點擊移動範圍外的點 79 { 80 HideAllMask();//取消棋子選定並隱藏全部遮罩 81 } 82 } 83 else{ 84 //若是在沒有選中棋子時,點擊了一個地塊 85 var mesh_unit=TiledHasCard(mesh); 86 if(mesh_unit)//若是這個地塊中存在棋子 87 { 88 if(mesh_unit.card) 89 { 90 PickCard2(mesh_unit.card);//等同於點擊棋子 91 } 92 return; 93 } 94 } 95 }
這段代碼經過一系列條件判斷,規定了每一種點擊狀況的處理方式,具體規則參考代碼註釋。
其中TiledHasCard方法用來尋找地塊中可能存在的棋子:
1 function TiledHasCard(node)//尋找這個地塊以內的單位,參數是地塊對象 2 { 3 var units=mesh_tiledCard._children;//這裏存儲的是卡牌對象的網格 4 var len=units.length; 5 var xmin=node.position.x-node.widthp/2;//這個地塊的範圍 6 var xmax=node.position.x+node.widthp/2; 7 var zmin=node.position.z-node.heightp/2; 8 var zmax=node.position.z+node.heightp/2; 9 for(var i=0;i<len;i++) 10 { 11 var unit=units[i]; 12 var pos=unit.position; 13 if(pos.x<xmax&&pos.x>xmin&&pos.z>zmin&&pos.z<zmax)//若是發現這個單位在這個地塊之內 14 { 15 return unit 16 } 17 } 18 return false; 19 }
HideAllMask方法隱藏全部遮罩,並取消當前棋子的選中:
1 function HideAllMask()//隱藏全部已經顯示的mask,而且取消單位的選中 2 { 3 var len=arr_DisplayedMasks.length; 4 for(var i=0;i<len;i++) 5 { 6 arr_DisplayedMasks[i].material=MyGame.materials.mat_alpha_null; 7 } 8 arr_DisplayedMasks=[]; 9 arr_nodepath={}; 10 arr_noderange={}; 11 noPicked(card_Closed2); 12 card_Closed2=null; 13 }
CardMove2Tiled方法用來沿黃色路徑移動棋子:
1 function CardMove2Tiled(path) 2 { 3 MyGame.flag_view="first_ani"; 4 var len=path.length; 5 //設計走一格用0.5秒分15幀 6 var frame_total=len*15; 7 var animation3=new BABYLON.Animation("animation3","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 8 var keys1=[]; 9 for(var i=0;i<len;i++)//對於路徑中的每一個節點 10 { 11 var pos=arr_nodepath[path[i]].node.position.clone(); 12 pos.y=0; 13 keys1.push({frame:i*15,value:pos});添加對應的關鍵幀 14 } 15 //var keys1=[{frame:0,value:MyGame.player.mesh.position.clone()},{frame:30,value:pos}]; 16 animation3.setKeys(keys1); 17 card_Closed2.mesh.animations.push(animation3); 18 MyGame.anicount=1; 19 var len=arr_DisplayedMasks.length; 20 for(var i=0;i<len;i++)//執行動畫時把各類顏色的遮罩都取消 21 { 22 arr_DisplayedMasks[i].material=MyGame.materials.mat_alpha_null;//這個數組裏存的真的只是遮罩 23 } 24 arr_DisplayedMasks=[];//清空它並不會影響移動和影響範圍的保存!!!! 25 scene.beginAnimation(card_Closed2.mesh, 0, frame_total, false,1,function(){ 26 MyGame.anicount--; 27 if(MyGame.anicount==0) 28 { 29 MyGame.flag_view="first_lock"; 30 //HideAllMask(); 31 32 card_Closed2.workstate="moved";//移動完成以後將選中的棋子狀態置爲「已經移動」 33 DisplayRange2(card_Closed2,card_Closed2.range);//同一個單位使用不一樣技能可能有不一樣的影響範圍 34 } 35 }); 36 }
執行動畫的方式與前面基本相同,惟一的區別在於這裏的關鍵幀是根據棋子移動路徑生成的。動畫完成以後執行DisplayRange2方法,顯示棋子移動以後的影響範圍,其代碼以下:
1 var arr_noderange2={}//移動以後計算範圍用的數據結構 2 function DisplayRange2(card,range)//只顯示移動後的影響範圍 3 { 4 var node_start=FindNode(card.mesh.position); 5 arr_noderange2={}; 6 arr_noderange2[node_start.name]={cost:0,path:[node_start.name],node:node_start}; 7 var costg=0; 8 var range=card.range; 9 var list_noderange=[node_start]; 10 for(var i=0;i<list_noderange.length;i++) 11 { 12 var arr_node_neighbor=FindNeighbor(list_noderange[i]); 13 var len=arr_node_neighbor.length; 14 for(var j=0;j<len;j++) 15 { 16 costg=arr_noderange2[list_noderange[i].name].cost; 17 costg+=1; 18 if(costg>range) 19 { 20 break;//由於影響範圍的cost都是相同的,因此只要有一個鄰居超過限度,則全部鄰居都不可用 21 } 22 //若是沒有超限 23 var nextnode = arr_node_neighbor[j]; 24 var path2=arr_noderange2[list_noderange[i].name].path.concat(); 25 path2.push(nextnode.name); 26 if(arr_noderange2[nextnode.name])//若是之前曾經到達這個節點 27 { 28 if(arr_noderange2[nextnode.name].cost>costg)//這裏仍是否有必要計算路徑?? 29 { 30 arr_noderange2[nextnode.name]={cost:costg,path:path2,node:nextnode}; 31 } 32 else 33 { 34 continue; 35 } 36 } 37 else 38 { 39 arr_noderange2[nextnode.name]={cost:costg,path:path2,node:nextnode}; 40 list_noderange.push(nextnode); 41 } 42 } 43 } 44 for(var key in arr_noderange2) 45 { 46 if(arr_noderange2[key].cost>0) 47 { 48 arr_noderange2[key].node.mask.material=MyGame.materials.mat_alpha_red; 49 } 50 51 arr_DisplayedMasks.push(arr_noderange2[key].node.mask); 52 } 53 }
是前面範圍算法的簡化版。
如此,完成了上述棋盤場景。
3、下一步
接下來計劃嘗試用eval函數編寫即時計算的技能模塊,併爲場景添加簡單的規則,而後參考Babylon.js文檔嘗試進行渲染優化提升幀數;再下一步計劃引入之前編寫的WebSocket組件,爲場景添加多人交互控制。