在WebGL場景中進行棋盤操作的實驗
這篇文章討論如何在基於Babylon.js的WebGL場景中,建立棋盤狀的地塊和多個可選擇的棋子物件,在點選棋子時顯示棋子的移動範圍,並且在點選移動範圍內的空白地塊時向目標地塊移動棋子。在這一過程中要考慮不同棋子的移動力和影響範圍不同,以及不同地塊的移動力消耗不同。
一、顯示效果:
1、訪問https://ljzc002.github.io/CardSimulate/HTML/TEST3tiled.html檢視“棋盤測試頁面”:
場景中是一個20*20的棋盤,地塊隨機為草地、土地、雪地,棋盤中央是四個“棋子”(用卡牌網格物件客串)。使用滑鼠和wasd、Shift、空格鍵控制相機網格物件在場景中漫遊,4個棋子會努力讓自己面朝相機。
2、點選一個棋子或棋子所在的地塊,棋子將被選中並顯示棋子的可移動範圍和影響範圍:
棋子可以到達的地塊覆蓋藍色半透明遮罩,棋子不能到達但可以影響的地塊覆蓋紅色半透明遮罩。這裡規定“銅卡”的移動力為10、影響範圍為2,“銀卡”的移動力為15、影響範圍為3,草地、泥地、雪地對移動力的消耗分別為2、3、4.
3、點選藍色地塊,將用黃色半透明遮罩標出棋子到達目標地塊的路徑,點選紅色地塊,則標出到達距這個紅色地塊最近的藍色地塊的路徑:
4、點選黃色地塊,則棋子緩緩移動到目標黃色地塊,到達目標後,在棋子周圍顯示棋子的影響範圍:
在更新版的程式裡,取消了棋子下面的紅色遮罩
5、點選棋子可以將鏡頭拉近到棋子附近;點選影響範圍外的地塊,可以取消棋子的選定;點選其他的棋子或者含有其他棋子的地塊,可以改變選定的棋子。
二、程式碼實現:
這個棋盤場景是在上一個卡牌場景(https://www.cnblogs.com/ljzc002/p/9660676.html)的基礎上改進而來的,這裡只討論改變和新增的程式碼中比較重要的部分,大部分新增方法在Tiled.js中。
1、在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生成半透明圖片的程式碼如下:
View Code
其大概思路是首先用特定顏色在canvas中標誌出一塊區域,然後遍歷canvas中的畫素,將符合特定顏色的畫素修改為需要的rgba顏色。
接下來根據設定的尺寸,為每個地塊生成一個網格,再為每個地塊網格生成一個遮罩網格,通過為遮罩網格設定不同的材質來表示地塊網格的不同狀態,而遮罩網格比地塊網格略小一點,這可以讓遮罩之間的界限更清晰。生成地塊時用到的這種按行、按列遍歷計算元素位置的演算法,在影象處理和表格繪製程式中也很常用,要考慮將它封裝為一個通用方法。
值得注意的是,Babylon.js內部也封裝有一個建立“棋盤網格”的方法,但是我並沒有發現這個方法的優勢在哪裡,反而因為所有地塊無差別的合併在一個網格物件中導致物件選取困難,同時這種內建的棋盤網格只支援方形棋盤無法自定義棋盤形狀。
RandomChooseFromObj方法的作用是,隨機選擇物件屬性中的一個返回,其程式碼如下:
View Code
2、將滑鼠點選事件重構為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 }
3、點選棋子的處理:
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類的介紹。
4、計算並顯示棋子的移動範圍和影響範圍:
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 }
5、點選地塊的處理:
考慮到點選棋子可能比較困難,這裡設定為點選棋子所在的地塊也能選中棋子;另外,遮罩網格只是起顯示作用,在選中棋子之後,也要通過監聽地塊的點選事件來決定棋子的移動目標。
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 }
是前面範圍演算法的簡化版。
如此,完成了上述棋盤場景。
三、下一步
接下來計劃嘗試用eval函式編寫即時計算的技能模組,併為場景新增簡單的規則,然後參考Babylon.js文件嘗試進行渲染優化提高幀數;再下一步計劃引入以前編寫的WebSocket元件,為場景新增多人互動控制。
來源:https://www.cnblogs.com/ljzc002/p/9778855.html