在前三篇文章的基礎上,為基於Babylon.js的WebGL場景添加了類似戰棋游戲的基本操作流程,包括從手中選擇單位放入棋盤、顯示單位具有的技能、選擇技能、不同單位通過技能進行交互、處理交互結果以及進入下一回合恢復棋子的移動力。因為時間有限,這一階段的目的只是實現基本規則的貫通,沒有關注場景的美觀性和操作的便捷性,也沒有進行充分的測試。
一、顯示效果:
1、訪問https://ljzc002.github.io/CardSimulate2/HTML/TEST4rule.html查看“規則測試頁面”:
天空盒內有一個隨機生成的棋盤(未來計划編寫更復雜棋盤的生成方法),棋盤上有兩個預先放置的棋子,屏幕中央是一個藍色邊框的准星。
2、按alt鍵展開手牌,拉近手牌中的某個單位后會在屏幕左側顯示“落子”按鈕,點擊落子按鈕后,准星將變為橙色邊框(再按alt返回手牌可以將准星變回藍色),在橙色准星狀態下點擊地塊則可將選定的手牌放入棋盤,放入棋盤后(未來計划一個單位在手牌中以卡牌方式顯示,放入棋盤后改為3D模型)立即顯示棋子的移動范圍,並且在屏幕的左上角顯示棋子的狀態和技能列表(計划優化這個表格的布局)。
選中手牌:
准星變為橙色:
落子后顯示移動范圍和狀態技能列表:
為了能用鼠標選取技能,這里調整了單位選取規則,現在只要選中單位,場景瀏覽方式就會從first_lock(鼠標鎖定在屏幕中心)切換為first_pick(鼠標可以在屏幕中自由移動選取)以釋放光標,取消單位選中后,自動切換回first_lock。
鼠標移入技能單元格,顯示技能的說明:
在這種狀態下,點擊紅色范圍外的地塊或者按alt鍵或者點擊棋子本身,都可以解除棋子的選中狀態,並隱藏移動范圍和技能列表。
3、在移動棋子之后,棋子會從wait狀態變為moved狀態,如果棋子具備nattack(普通攻擊)技能,將自動顯示棋子的普通攻擊范圍;按alt鍵,在手牌菜單里點擊“下一回合”,將把所有棋子恢復為wait狀態(這里還需要一個明確的回合結束生效效果),並且增加需要冷卻的技能的裝填計數並減少持續時間有限的技能的持續時間(尚未測試)。
完成移動之后:
右側的Octocat正處於moved狀態,它周圍是nattack技能的釋放范圍(可以看到skill_current項顯示為“nattack”,表示移動完成后默認選取了nattack技能),此時的Octocat不能再移動,可以通過點擊沒有遮罩的地塊取消對他的選取。
點擊下一回合按鈕后,再選中Octocat單位:
發現Octocat又可以再次移動,並且冷卻時間為2的test2技能進行了一次裝填。
4、單位移動完畢之后會自動選擇nattack作為當前技能,或者在技能列表里點選技能做為當前技能(目前只完成了nattack的編寫),選擇完畢后會在單位周圍用紅色遮罩標示技能的釋放范圍,點擊紅色遮罩,則以綠色遮罩顯示技能的影響范圍。再次點擊綠色遮罩,則在這個位置釋放當前技能,釋放技能時技能釋放者和釋放目標按順序執行相應的動畫效果。
5、當單位的血量耗盡時,會變成灰色返回手牌:
在手牌的末尾能夠看到灰色的Octocat,它無法被再次放入棋盤。
6、AOE技能:
可以看到,技能范圍內的單位都受到AOE影響
7、說明:
事實上,上面的游戲規則代碼已經被前人用各種方式實現很多遍,可以說每一個成熟的游戲開發團隊都有其精雕細琢的規則代碼,但絕大部分這類代碼都是閉源或者存在獲取障礙的,因此我自己用JavaScript實現了這一套規則代碼並把它開源。其實,Babylon.js的開發團隊也在做類似的事情——將各種商業3D引擎的成熟技術移植到WebGL平台並開源。
有人會問,花費很多精力用低效的方式做一個別人做過多次的“輪子”有什么用?確實,和成熟的商業3D引擎相比,WebGL技術在性能和操作性上還存在明顯的缺陷,但WebGL技術的兩個獨有特性是傳統商業引擎所無法比擬的:一是網頁端應用的強制開源性,因為所有JavaScript代碼最終都以明文方式在瀏覽器中執行,所以任何人都能夠獲取WebGL程序的代碼並直接使用瀏覽器進行調試,這使得WebGL中用到的技術和知識可以不受壟斷的自由傳播;其二,JavaScript語言的學習難度和傳統的3D開發語言C++不在同一量級,瀏覽器也為開發者解決了適配各種運行環境時遇到的諸多難題,WebGL技術的出現使得3D編程的入門前所未有的簡單。
對於擁有大量高端人才、以盈利為目的商業性游戲公司,強制開源和低技術門檻並沒有太大意義,所以WebGL技術注定難以成為商業游戲開發的主流,但是對於不以盈利為目的的人士和非職業編程者來說WebGL技術正預示着一種新的、不受現有條框束縛的表達方式,而准確且豐富的表達正是人們相互理解進而平等相待的基礎之一。使用WebGL技術,學生、教師、傳統信息系統操作員乃至無法忍受劣質商業化游戲的玩家都可能做出兼具外在表象和內在邏輯的3D程序。
二、代碼實現:
1、整理前面的代碼:
在編寫規則代碼之前,首先對https://www.cnblogs.com/ljzc002/p/9660676.html和https://www.cnblogs.com/ljzc002/p/9778855.html中建立的工程進行整理,經過整理后的js文件結構如下:
首先把BallMan、CameraMesh、CardMesh三個類分離到三個單獨的js文件里,置於Character文件夾中,用以實例化場景中比較復雜的幾種物體;
接着把所有和鍵盤鼠標響應有關的代碼放到Control.js中;
FullUI.js里包含所有與Babylon.js GUI和Att7.js Table相關的內容;
Game.js改動不大,仍起到全局變量管理的作用;
HandleCard2.js里是和手牌有關的規則代碼;
Move.js是CameraMesh的移動控制方法;
rule.js是一部分和場景初始化和GUI操作有關的規則代碼;
tab_carddata.js里是卡牌定義;
tab_skilldata.js里是技能定義,並且包含了和技能有關的規則代碼;
tab_somedata.js里是一些其他定義;
Tiled.js是和棋盤有關的規則代碼。
整理之后的部分文件內容如下:(只總結了前兩篇文章里的內容)
圖一:
圖二:
圖中列出了每個文件中的屬性和方法,大部分可以在前兩篇文章中找到對應的說明,如果哪里沒有說清,請在評論區留言。因為時間有限,新增加的規則代碼並沒有畫入,因為手機性能有限,有些文字略顯模糊。
2、手牌管理:https://www.cnblogs.com/ljzc002/p/9660676.html
3、從手牌放入棋盤:
a、在FullUI.js中添加“落子”按鈕
1 var UiPanel2 = new BABYLON.GUI.StackPanel();
2 UiPanel2.width = "220px"; 3 UiPanel2.fontSize = "14px"; 4 UiPanel2.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; 5 UiPanel2.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER; 6 UiPanel2.color = "white"; 7 advancedTexture.addControl(UiPanel2); 8 var button3 = BABYLON.GUI.Button.CreateSimpleButton("button3", "落子"); 9 button3.paddingTop = "10px"; 10 button3.width = "100px"; 11 button3.height = "50px"; 12 button3.background = "green"; 13 button3.isVisible=false;//這個按鈕默認不可見,選中並放大一張手牌后可見 14 button3.onPointerDownObservable.add(function(state,info,coordinates) { 15 if(MyGame.init_state==1&&card_Closed&&card_Closed.workstate!="dust")//如果完成了場景的虛擬化 16 { 17 Card2Chess();//將當前選中的手牌和光標關聯起來,換回first_lock,並改變光標的顏色,點擊空白地塊時落下棋子, 18 } 19 });
b、按下按鈕后將准星顏色改為橙色(考慮更改准星形狀?),在rule.js文件中
1 function Card2Chess()//將當前選中的手牌設為手中棋子 2 { 3 MyGame.player.centercursor.color="orange"; 4 MyGame.player.changePointerLock2("first_lock");//將瀏覽方式改為first_lock 5 HandCard(1);//經過動畫隱藏手牌 6 7 //切換回first_lock狀態 8 }
c、在准星邊緣為橙色時點擊地塊,則把手牌轉化為棋子放入棋盤里:
首先在Tiled.js文件的PickTiled方法里響應地塊點擊:
1 if(MyGame.player.centercursor.color=="orange")//如果當前是落子狀態 2 {//mesh是棋盤中的一個地塊 3 if(card_Closed&&!TiledHasCard(mesh))//如果存在選定的手牌並且點擊的格子沒有其他棋子,則把棋子放到這個格子里 4 { 5 Card2Chess2(mesh);//具體代碼在rule.js里 6 } 7 else 8 { 9 MyGame.player.centercursor.color=="blue"//點已經有棋子的地方,則取消落子 10 } 11 }
然后在rule.js里正式將棋子放入棋盤:
1 function Card2Chess2(mesh)//將手中棋子放在棋盤上 2 { 3 if(card_Closed.num_group>-1&&card_Closed.num_group<5)//如果卡片在手牌的某個分組中 4 {//從小組里刪除 5 delete arr_cardgroup[card_Closed.num_group][card_Closed.mesh.name]; 6 /*if(Object.getOwnPropertyNames(arr_cardgroup[card.num_group]).length==0) 7 { 8 arr_mesh_groupicon[card.num_group].isVisible=false; 9 }*/ 10 } 11 card_Closed.mesh.parent=null;//card_Closed是手牌中選中的對象, 12 card_Closed.mesh.parent=mesh_tiledCard; 13 card_Closed.mesh.scaling=new BABYLON.Vector3(0.1,0.1,0.1); 14 card_Closed.mesh.position=mesh.position.clone();//棋子放在地塊位置。 15 card_Closed.mesh.position.y=0; 16 card_Closed.workstate="wait"; 17 noPicked(card_Closed); 18 card_Closed2=card_Closed;//將它設為棋盤中的一個棋子 19 card_Closed2.display();//將棋子設為可見 20 PickCard2(card_Closed2);//將它設為選中的棋子 21 card_Closed=null;//取消手牌中的選中對象 22 MyGame.player.centercursor.color="blue";//准星重新變藍 23 }
4、棋子移動:https://www.cnblogs.com/ljzc002/p/9778855.html
5、選中棋子:
a、HandleCard2.js文件中PickCard2方法以棋子對象為參數,用來在棋盤上選中棋子:
1 function PickCard2(card)//點擊一下選中,高亮邊緣,再點擊也不放大?-》再點擊則拉近鏡頭后恢復first_lock!! 2 //同時還要在卡片附近建立一層藍色或紅色的半透明遮罩網格,表示移動及影響范圍 3 {//如果再次點擊有已選中卡片,則把相機移到卡片面前 4 if(card.isPicked) 5 { 6 GetCardClose2(card);//將相機拉近到選中卡牌面前,並取消卡牌的選定 7 //規定點擊藍色遮罩時計算到達路徑,點擊空處時清空范圍,點擊其他卡牌時切換范圍,切換成手牌時清空范圍 8 } 9 else//如果這個棋子沒有被選中 10 { 11 12 if(card.workstate=="wait")//如果棋子正等待移動,則顯示棋子的移動范圍 13 { 14 DisplayRange(card);//這里面包含了清除已有遮罩並且保證棋子的選中 15 } 16 else if(card.workstate=="moved")//如果棋子已經移動,但還未工作 17 { 18 //首先要檢查是否有已經顯示的遮罩 19 if(arr_DisplayedMasks.length>0)//清空所有遮罩和棋子選定以及技能列表 20 { 21 HideAllMask();//這里也會清空card_Closed2 22 } 23 card_Closed2=card; 24 getPicked(card_Closed2); 25 card.isPicked=true; 26 if(card_Closed2.skills["nattack"]) 27 {//如果這個單位具有普通攻擊技能,則顯示普通攻擊范圍 28 skill_current=card_Closed2.skills["nattack"];//如果單位具有nattack技能 29 document.getElementById("str_sc").innerHTML="nattack"; 30 canvas.style.cursor="crosshair"; 31 DisplayRange2(card_Closed2,card_Closed2.skills["nattack"].range);//默認顯示nattack技能的范圍 32 } 33 } 34 //如果是worked則什么也不做->還是要顯示信息的 35 else if(card.workstate=="worked")//如果已經工作過 36 { 37 if(arr_DisplayedMasks.length>0) 38 { 39 HideAllMask();//這里也會清空card_Closed2 40 } 41 card_Closed2=card; 42 getPicked(card_Closed2); 43 card.isPicked=true; 44 document.getElementById("str_sc").innerHTML="Worked"; 45 } 46 MyGame.player.changePointerLock2("first_pick");//如果棋子沒有被選中,則瀏覽方式改為first_pick 47 DisplayUnitUI();//同時也要顯示棋子操縱ui->這里使用html dom table 48 } 49 }
b、DisplayUnitUI方法顯示當前選中棋子的技能列表,其代碼位於FullUI.js文件中:
1 function DisplayUnitUI() 2 { 3 //MyGame.SkillTable 4 if(card_Closed2)//如果這時已經有選中的單位,則顯示單位的效果列表 5 { 6 document.getElementById("all_base").style.display="block";//使技能列表元素可見 7 var data=MyGame.SkillTable.data;//獲取技能列表的數據 8 data.splice(4);//清空舊的技能列表 9 var card=card_Closed2; 10 document.getElementById("str_chp").innerHTML=card.chp;//當前hp 11 document.getElementById("str_thp").innerHTML=card.hp;//總hp 12 document.getElementById("str_cmp").innerHTML=card.cmp;//當前mp 13 document.getElementById("str_tmp").innerHTML=card.mp;//總mp 14 document.getElementById("str_atk").innerHTML=card.attack;//攻擊 15 document.getElementById("str_speed").innerHTML=card.speed;//移動力 16 //document.getElementById("str_range").innerHTML=card.range; 17 var skills=card.skills; 18 for(key in skills)//遍歷顯示單位所有的技能 19 { 20 var skill=skills[key];//單位現在具有的技能 21 var skill2=arr_skilldata[key];//技能列表里的技能描述 22 var str1=key,str2="full"; 23 if(skill.last!="forever")//如果不是永久持續,要在括號里顯示持續時間 24 { 25 str1+=("("+skill.last+")"); 26 } 27 if(skill.reload!="full")//如果沒有裝填完成,要顯示裝填進度 28 { 29 str2=skill.reload+"/"+skill2.reloadp; 30 } 31 data.push([str1 32 ,str2]); 33 } 34 MyGame.SkillTable.draw(data,0);//繪制表格 35 requestAnimFrame(function(){MyGame.SkillTable.AdjustWidth();}); 36 } 37 }
對應的,DisposeUnitUI方法用來隱藏技能列表:
1 function DisposeUnitUI() 2 { 3 skill_current=null;//清空當前選中的技能 4 document.getElementById("str_sc").innerHTML="";//當前技能 5 canvas.style.cursor="default"; 6 arr_cardTarget=[];//清空當前選擇的技能目標 7 fightDistance=0; 8 if(document.getElementById("div_thmask"))//刪除鎖定表頭的遮罩層 9 { 10 var div =document.getElementById("div_thmask"); 11 div.parentNode.removeChild(div); 12 } 13 if(document.getElementById(MyGame.SkillTable.id))//刪除表體 14 { 15 var tab =document.getElementById(MyGame.SkillTable.id); 16 tab.parentNode.removeChild(tab); 17 } 18 document.getElementById("all_base").style.display="none";//隱藏表格 19 }
c、FullUI.js文件中還設置了技能列表的單元格的鼠標響應:
鼠標移入:
1 function SkillTableOver()//在鼠標移入時先隱藏可能存在的舊的描述文字,然后顯示懸浮顯示描述文字 2 { 3 //console.log("SkillTableOver"); 4 var evt=evt||window.event||arguments[0]; 5 cancelPropagation(evt); 6 var obj=evt.currentTarget?evt.currentTarget:evt.srcElement; 7 delete_div("div_bz"); 8 Open_div("", "div_bz", 240, 120, 0, 0, obj, "div_tab"); 9 document.querySelectorAll("#div_bz")[0].innerHTML = MyGame.SkillTable.html_onmouseover;//向彈出項里寫入結構 10 document.querySelectorAll("#div_bz .div_inmod_lim_content")[0].innerHTML = card_Closed2.skills[obj.innerHTML.split("(")[0]].describe;//顯示描述文字 11 }
鼠標移出:
1 function SkillTableOut()//鼠標移出時隱藏所有描述文字 2 { 3 //console.log("SkillTableOut"); 4 var evt=evt||window.event||arguments[0]; 5 cancelPropagation(evt); 6 delete_div("div_bz"); 7 }
點擊技能單元格:
1 function SkillTableClick()//點擊時觸發技能的eval 2 { 3 var evt=evt||window.event||arguments[0]; 4 cancelPropagation(evt); 5 var obj=evt.currentTarget?evt.currentTarget:evt.srcElement; 6 delete_div("div_bz"); 7 if(card_Closed2.workstate!="worked")//如果單位還沒有進行工作 8 { 9 var skillName=obj.innerHTML.split("(")[0];//從單元格中提取技能名 10 if(card_Closed2.cmp>=card_Closed2.skills[skillName].cost)//如果有足夠的mp 11 { 12 skill_current=card_Closed2.skills[skillName];//skill_current表示當前技能對象 13 document.getElementById("str_sc").innerHTML=skillName; 14 //console.log("SkillTableClick"); 15 //還要顯示這個技能的釋放范圍 16 var len=arr_DisplayedMasks.length; 17 for(var i=0;i<len;i++)//隱藏已有的遮罩 18 { 19 arr_DisplayedMasks[i].material=MyGame.materials.mat_alpha_null;//這個數組里存的真的只是遮罩 20 } 21 arr_DisplayedMasks=[]; 22 canvas.style.cursor="crosshair"; 23 DisplayRange2(card_Closed2,skill_current.range);//在單位周圍顯示當前技能的釋放范圍 24 } 25 26 } 27 28 }
6、顯示當前技能的影響范圍,並查找范圍內的可能目標:
a、在Tiled.js中響應地塊點擊事件:
1 if(skill_current!=null)//如果當前技能不為空 2 { 3 if(mesh.mask.material.name == "mat_alpha_red")//如果點擊的是紅色遮罩 4 { 5 //有選擇的單位和技能,點擊紅色遮罩,則先清空已選擇目標,以點擊位置為中心顯示綠色遮罩群表示瞄准,如果瞄准范圍內存在單位,則放入target 6 arr_cardTarget=[]; 7 var len=arr_DisplayedMasks.length; 8 for(var i=0;i<len;i++)//隱藏所有遮罩 9 { 10 arr_DisplayedMasks[i].material=MyGame.materials.mat_alpha_null; 11 } 12 arr_DisplayedMasks=[]; 13 DisplayRange2(card_Closed2,skill_current.range);//重新顯示一次釋放范圍 14 DisplayRange3(mesh);//根據當前技能,在瞄准地塊周圍顯示綠色遮罩群表示影響范圍,要先調用一次DisplayRange2, 15 } 16 else if(mesh.mask.material.name == "mat_alpha_green") 17 {//如果點擊的是綠色遮罩 18 if (card_Closed2.workstate == "wait"||card_Closed2.workstate == "moved") 19 {//如果單位還沒有工作 20 card_Closed2.cmp-=skill_current.cost;//消耗mp 21 document.getElementById("str_cmp").innerHTML=card_Closed2.cmp; 22 eval(skill_current.eval);//執行技能效果 23 fightDistance=arr_noderange3[mesh.name].cost;//fight雙方的距離 24 //HideAllMask(); 25 //MyGame.player.changePointerLock2("first_lock"); 26 27 } 28 } 29 else//點擊影響范圍外的點 30 { 31 HideAllMask();//取消選中 32 MyGame.player.changePointerLock2("first_lock"); 33 } 34 }
b、DisplayRange3方法的參數是地塊的網格,表示在這個地塊釋放當前選中技能時的影響范圍:
1 function DisplayRange3(mesh) 2 { 3 //var card=card_Closed2; 4 var range=0; 5 range=skill_current.range2;//range2是技能的影響范圍,注意不要和釋放范圍range混淆 6 //算法和前兩個名稱類似的方法相似 7 var node_start=mesh; 8 arr_noderange3={}; 9 arr_noderange3[node_start.name]={cost:0,path:[node_start.name],node:node_start}; 10 var costg=0; 11 //var range=card.range; 12 var list_noderange=[node_start]; 13 for(var i=0;i<list_noderange.length;i++) 14 { 15 var arr_node_neighbor=FindNeighbor(list_noderange[i]); 16 var len=arr_node_neighbor.length; 17 for(var j=0;j<len;j++) 18 { 19 costg=arr_noderange3[list_noderange[i].name].cost; 20 costg+=1; 21 if(costg>range) 22 { 23 break;//因為影響范圍的cost都是相同的,所以只要有一個鄰居超過限度,則所有鄰居都不可用 24 } 25 //如果沒有超限 26 var nextnode = arr_node_neighbor[j]; 27 var path2=arr_noderange3[list_noderange[i].name].path.concat(); 28 path2.push(nextnode.name); 29 if(arr_noderange3[nextnode.name])//如果以前曾經到達這個節點 30 { 31 if(arr_noderange3[nextnode.name].cost>costg)//這里還是否有必要計算路徑?? 32 { 33 arr_noderange3[nextnode.name]={cost:costg,path:path2,node:nextnode}; 34 } 35 else 36 { 37 continue; 38 } 39 } 40 else 41 { 42 arr_noderange3[nextnode.name]={cost:costg,path:path2,node:nextnode}; 43 list_noderange.push(nextnode); 44 } 45 } 46 } 47 for(var key in arr_noderange3)//對於每一個綠色遮罩的地塊 48 { 49 //if(arr_noderange3[key].cost>0) 50 //{ 51 arr_noderange3[key].node.mask.material=MyGame.materials.mat_alpha_green; 52 var mesh_unit = TiledHasCard(arr_noderange3[key].node);//如果這個綠色地塊中存在單位 53 if(mesh_unit)//如果瞄准范圍內存在一個單位,從理論上說也可能是自己!!!! 54 { 55 arr_cardTarget.push(mesh_unit.card);//則把這個單位放入目標列表 56 } 57 //} 58 59 arr_DisplayedMasks.push(arr_noderange3[key].node.mask); 60 } 61 }
7、執行技能效果:
a、在tab_skilldata.js文件中定義了技能的eval屬性,它是以字符串形式存儲的可執行代碼,以普通攻擊技能為例:
1 nattack: 2 { 3 name:"nattack" 4 ,ap:"a" 5 ,start:"wait" 6 ,end:"worked" 7 ,reloadp:0 8 ,range:1 9 ,range2:0 10 ,cost:0 11 ,eval:"func_skills.nattack()" 12 ,describe:"普通攻擊,是默認的影響方式" 13 }
其中,ap屬性是“主被動標記”,取值范圍如下:
a主動、p被動,p_all在所有環節生效,p_param影響單位屬性,p_work在工作環節生效,p_next在點擊下一回合時生效,p_weak在下一回合開始時生效(與p_next等效?),p_sleep在工作結束后立即生效,p_destoryed在被破壞時生效,p_beattack被影響時生效
b、nattack方法的代碼在下面:
1 nattack:function()//一次普通攻擊行為 2 { 3 var len=arr_cardTarget.length; 4 //var count_ani=0 5 if(len>0)//如果目標數大於零 6 { 7 MyGame.flag_view="first_ani"; 8 card_Closed2.count_ani=len;//動畫計數器,認為每一個目標都有一系列的技能流程, 9 }//這一次行為中的所有技能流程都結束,這個行為才結束。 10 11 for(var i=0;i<len;i++)//對於每一個目標,認為普通攻擊只會有一個目標! 12 { 13 var target=arr_cardTarget[i]; 14 if(target.mesh.id==card_Closed2.mesh.id)//規定自己不能nattack自己?? 15 { 16 func_skills.ani_final();//什么也不做,結束這個技能流程 17 continue; 18 } 19 var skills=card_Closed2.skills;//當前選中棋子的技能列表 20 var skillst=target.skills;//目標的技能列表 21 func_skills.beforeFight(target,skills,skillst);//執行一些在fight開始前生效的被動技能 22 //超多層function嵌套,有沒有更先進的解決方法?開始進入回調地獄 23 card_Closed2.ani_beat(target,function(){//撞擊動畫 24 target.chp-=card_Closed2.attack;//技能目標的當前hp減少量等於選中棋子的攻擊力 25 target.ani_floatstr("-"+card_Closed2.attack,[],function(){//文字上浮動畫 26 if(target.chp>0)//如果目標還活着 27 { 28 if(skillst["nattack"])//如果target具備nattack能力則反擊之 29 { 30 target.ani_beat(card_Closed2,function(){ 31 card_Closed2.chp-=target.attack; 32 card_Closed2.ani_floatstr("-"+target.attack,[],function(){ 33 if(card_Closed2.chp>0) 34 { 35 document.getElementById("str_chp").innerHTML=card_Closed2.chp;//更新當前hp顯示 36 card_Closed2.workstate="worked"; 37 func_skills.ani_final(target,skills,skillst);//結束這個技能流程 38 } 39 else 40 { 41 func_skills.unitDestory(card_Closed2,skills,skillst);//搶救 42 } 43 }); 44 }); 45 } 46 } 47 else 48 { 49 card_Closed2.workstate="worked";//當前狀態為工作完畢 50 func_skills.unitDestory(target,skills,skillst);//在target死前檢查有沒有可以自救的被動技能 51 } 52 }); 53 }); 54 55 if(target.range>=fightDistance)//如果在target的nattack范圍內 56 { 57 //card_Closed2.chp-=target.attack; 58 } 59 60 // 61 } 62 },
因為要等到一個動畫環節(比如撞擊)結束后才能進行下一個環節(比如上浮傷害數字),所以需要把后一個環節的調用放在前一個環節的回調函數里,有人認為這種連續回調環節很多時程序會非常難以閱讀,故將這種情況稱為“回調地獄”,但是我感覺還好。
c、ani_final方法用來結束行為中的一個流程:
1 ani_final:function(target,skills,skillst)//在所有效果動畫結束后恢復為first_lock 2 { 3 4 card_Closed2.count_ani--; 5 if(card_Closed2.count_ani<=0)//假設一個aoe有多個回調線路,要確保每個回調線路都結束,再判定動作結束 6 { 7 /*if(target) 8 {//如果目標還活着 9 func_skills.afterFight(target,skills,skillst); 10 }*/ 11 HideAllMask();//動作結束解除所有鎖定 12 MyGame.player.changePointerLock2("first_lock");// 13 } 14 15 }
d、unitDestory方法會查看單位是否有自救技能:
1 unitDestory:function(target,skills,skillst) 2 { 3 for(key in skillst) 4 { 5 var skill=skillst[key]; 6 if(skill.ap=="p_destoryed"&&skill.eval) 7 { 8 eval(skill.eval);//如果有自救能力則跳到另一個效果方法里,nattack的效果則終結 9 } 10 } 11 if(target.chp<=0)//如果沒搶救過來 12 { 13 target.ani_destory(function(){//執行死亡動畫 14 func_skills.ani_final(target,skills,skillst); 15 }); 16 } 17 },
8、為單位建立動畫:
在CardMesh.js文件里為卡牌型單位建立了幾種簡單的行為動畫
a、撞擊目標:
1 //下面計划要添加震動方法和被破壞方法 2 CardMesh.prototype.ani_beat=function(target,callback)// 3 { 4 var mesh=this.mesh; 5 mesh.animations=[]; 6 var animation=new BABYLON.Animation("animation","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 7 var pos1=mesh.position.clone(); 8 var pos2=target.mesh.position.clone(); 9 var keys=[{frame:0,value:pos1},{frame:15,value:pos2},{frame:30,value:pos1}]; 10 animation.setKeys(keys); 11 mesh.animations.push(animation); 12 scene.beginAnimation(mesh, 0, 30, false,1,function(){ 13 callback(); 14 }); 15 }
b、向目標發射一個“子彈”:
1 CardMesh.prototype.ani_fire=function(target,cursor,callback) 2 {//建立一個精靈對象(或者是粒子對象?),讓它飛向目標 3 var mesh=this.mesh; 4 var sprite_bullet=new BABYLON.Sprite("sprite_bullet", cursor);//cursor是MyGame.SpriteManager 5 sprite_bullet.parent=mesh.parent; 6 sprite_bullet.position =mesh.position.clone(); 7 sprite_bullet.position.y+=2 8 9 var animation=new BABYLON.Animation("animation","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 10 var pos1=sprite_bullet.position.clone(); 11 var pos2=target.mesh.position.clone(); 12 var keys=[{frame:0,value:pos1},{frame:30,value:pos2}]; 13 animation.setKeys(keys); 14 sprite_bullet.animations.push(animation); 15 scene.beginAnimation(sprite_bullet, 0, 30, false,1,function(){ 16 sprite_bullet.dispose(); 17 callback(); 18 }); 19 }
c、一個從單位身上飄起的字符串:
1 CardMesh.prototype.ani_floatstr=function(str,styles,callback) 2 {//建立一個基於canvas紋理的對象,讓它飄起來然后消失 3 var mesh=this.mesh; 4 str+="";//前面如果傳來的是數字,則取不到length!!-》顯示轉換為字符串 5 var size_x=str.length*30; 6 var mesh_str = new BABYLON.MeshBuilder.CreateGround(this.name + "mesh_str", { 7 width: size_x/2.5, 8 height: 16 9 }, scene); 10 mesh_str.parent=mesh; 11 //mesh_str.position =new BABYLON.Vector3(0,0,0); 12 mesh_str.renderingGroupId = 3;//這些文字是特別強調內容,使用最高級的渲染組 13 var mat_str = new BABYLON.StandardMaterial(this.name + "mat_str", scene); 14 var texture_str = new BABYLON.DynamicTexture(this.name + "texture_str", { 15 width: size_x, 16 height: 40 17 }, scene); 18 mat_str.diffuseTexture = texture_str; 19 mesh_str.material = mat_str; 20 mesh_str.rotation.x = -Math.PI / 2; 21 mesh_str.isPickable = false; 22 texture_str.hasAlpha=true; 23 mat_str.useAlphaFromDiffuseTexture=true; 24 25 //經過測試發現,在Chrome中canvas的繪圖是以圖像的左上角定位的,而文字繪制則是以文字的左下角定位的!!!! 26 var context_comment = texture_str.getContext(); 27 context_comment.fillStyle = "rgba(255,255,255,0)";//"transparent"; 28 context_comment.fillRect(0, 0, size_x, 40); 29 //context_comment.fillStyle = "#ffffff"; 30 context_comment.fillStyle = "#ff0000"; 31 context_comment.font = "bold 30px monospace"; 32 var len=styles.length; 33 for(var i=0;i<len;i++) 34 { 35 context_comment[styles[i][0]]=styles[i][1]; 36 } 37 //newland.canvasTextAutoLine(str, context_comment, 1, 30, 35, 34); 38 context_comment.fillText(str,0,30);//y坐標偏離一個字高 39 texture_str.update(); 40 41 var animation=new BABYLON.Animation("animation","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 42 //var pos1=mesh_str.position.clone(); 43 //var pos2=mesh_str.position.clone(); 44 //pos2.y+=2; 45 var keys=[{frame:0,value:new BABYLON.Vector3(0,0,0)},{frame:30,value:new BABYLON.Vector3(0,20,0)}]; 46 animation.setKeys(keys); 47 mesh_str.animations.push(animation); 48 scene.beginAnimation(mesh_str, 0, 30, false,1,function(){ 49 mesh_str.dispose(); 50 mat_str.dispose(); 51 texture_str.dispose(); 52 callback(); 53 }); 54 55 }
d、單位變成黑白色,然后升天
1 CardMesh.prototype.ani_destory=function(callback) 2 {//先換成灰白色圖片,然后上浮 3 var mesh=this.mesh; 4 this.workstate="dust" 5 6 var mat_dust = new BABYLON.StandardMaterial(this.name + "mat_dust", this.scene);//測試用卡片紋理 7 mat_dust.diffuseTexture = new BABYLON.Texture(this.imagedust, this.scene);//實現已經准備好了黑白色的圖片,可以用MakeDust.html工具生成 8 mat_dust.diffuseTexture.hasAlpha = false; 9 mat_dust.backFaceCulling = true; 10 mat_dust.useLogarithmicDepth = true;//使用對數式深度緩存避免“Z-fighting” 11 mat_dust.freeze(); 12 13 this.mesh_mainpic.material = mat_dust; 14 15 var animation=new BABYLON.Animation("animation","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 16 var pos1=this.mesh.position.clone(); 17 var pos2=this.mesh.position.clone(); 18 pos2.y+=2; 19 var keys=[{frame:0,value:pos1},{frame:30,value:pos2}]; 20 animation.setKeys(keys); 21 mesh.animations=[]; 22 mesh.animations.push(animation); 23 scene.beginAnimation(mesh, 0, 30, false,1,function(){ 24 //把dust的card收回手牌 25 noPicked(mesh.card); 26 mesh.parent=null; 27 mesh.parent=mesh_arr_cards; 28 mesh.scaling=new BABYLON.Vector3(0.1,0.1,0.1); 29 mesh.rotation.y=0; 30 mesh.card.num_group==999; 31 mesh.card.dispose(); 32 callback(); 33 }); 34 }
e、單位跳動一下:
1 CardMesh.prototype.ani_shake=function(callback)//上下晃動一下 2 { 3 var mesh=this.mesh; 4 mesh.animations=[]; 5 var animation=new BABYLON.Animation("animation","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 6 var pos1=mesh.position.clone(); 7 var pos2=mesh.position.clone(); 8 pos2.y+=1; 9 var keys=[{frame:0,value:pos1},{frame:15,value:pos2},{frame:30,value:pos1}]; 10 animation.setKeys(keys); 11 mesh.animations.push(animation); 12 scene.beginAnimation(mesh, 0, 30, false,1,function(){ 13 callback(); 14 }); 15 }
9、AOE
同時攻擊多個目標意味着要同時開啟多個回調流程,修改一下上面的nattack方法:
1 aoe:function(range2,atk,isSafe,arr_state)//造成aoe傷害,參數:影響距離、攻擊力、是否會傷害本方、[[給目標添加的效果1、生效的概率、持續的時間],[],[]] 2 { 3 var len=arr_cardTarget.length; 4 if(len>0) 5 { 6 MyGame.flag_view="first_ani"; 7 card_Closed2.count_ani=len;//有幾個目標,就設置幾個動畫計數 8 } 9 else 10 { 11 //return; 12 } 13 card_Closed2.ani_shake(function(){//自己先晃動一下表示發出aoe 14 var skills=card_Closed2.skills; 15 16 func_skills.beforeFight(null,skills,{})//如果下面的target使用var類型變量,因為js的變量提升特性,前面的target也會被自動聲明!!,但是let並不具備變量提升功能!!!! 17 for(var i=0;i<len;i++) 18 {//這里要使用let型變量,否則所有的target變量都會被設為最后定義的target導致程序出錯 19 let target=arr_cardTarget[i]; 20 if(isSafe&&target.belongto==card_Closed2.belongto)//如果是安全aoe則跳過本方單位 21 { 22 func_skills.ani_final(); 23 continue; 24 } 25 26 var skillst=target.skills; 27 //func_skills.beforeFight(target,{},skillst); 28 target.chp-=atk; 29 target.ani_floatstr("-"+atk,[],function() { 30 if(target.chp>0)//如果還活着 31 { 32 var len2=arr_state.length; 33 for(var j=0;j<len2;j++) 34 { 35 var state=arr_state[j] 36 if(newland.RandomBool(state[1]))//如果通過概率判定 37 { 38 if(skillst[state[0]])//如果已經有這一效果,則延長持續時間 39 { 40 skillst[state[0]].last+=state[2]; 41 } 42 else//否則添加這個效果 43 { 44 skillst[state[0]]={last:state[2],reload:"full"}; 45 } 46 } 47 } 48 func_skills.ani_final(target,skills,skillst); 49 } 50 else 51 { 52 53 func_skills.unitDestory(target,skills,skillst); 54 } 55 }); 56 57 } 58 if(card_Closed2.workstate!="dust") 59 { 60 document.getElementById("str_chp").innerHTML=card_Closed2.chp; 61 card_Closed2.workstate="worked"; 62 } 63 64 }) 65 66 67 },
10、進入下一回合:
a、在FullUI.js中添加“下一回合”按鈕:
1 var button4 = BABYLON.GUI.Button.CreateSimpleButton("button4", "下一回合"); 2 button4.paddingTop = "10px"; 3 button4.width = "100px"; 4 button4.height = "50px"; 5 button4.background = "green"; 6 button4.isVisible=false; 7 button4.onPointerDownObservable.add(function(state,info,coordinates) { 8 if(MyGame.init_state==1)//如果完成了場景的虛擬化 9 { 10 NextRound();//所有棋子的狀態變為wait,特殊狀態的除外 11 } 12 }); 13 UiPanel2.addControl(button4); 14 UiPanel2.buttonnextr=button4;
b、NextRound方法位於rule.js文件中:
1 function NextRound()//將所有棋子的狀態置為wait(后續添加對特殊狀態的處理) 2 { 3 var units=mesh_tiledCard._children; 4 var len=units.length; 5 for(var i=0;i<len;i++)//對於棋盤上的每個棋子 6 { 7 var unit=units[i]; 8 card_Closed2=unit;//選中這個單位 9 var skills=unit.card.skills;//更新每個reload和last的技能時間,還要令回合結束時的被動技能生效 10 for(var key in skills) 11 { 12 var skill=skills[key]; 13 var skill2=arr_skilldata[key]; 14 if(skill.ap=="p_next")//對於每一個在跨越回合時生效的被動技能 15 { 16 if(skill.eval) 17 { 18 eval(skill.eval); 19 } 20 } 21 if(skill.reload!="full") 22 { 23 skill.reload++; 24 if(skill.reload>=skill2.reloadp)//如果裝填完畢 25 { 26 skill.reload="full" 27 } 28 } 29 if(skill.last!="forever"&&skill.ap!="p_wake") 30 { 31 skill.last--; 32 if(skill.last<=0)//如果持續時間結束 33 { 34 if(skill.eval2) 35 { 36 eval(skill.eval2); 37 } 38 39 delete skills[key];//刪除這個效果 40 } 41 } 42 } 43 44 unit.card.workstate="wait"; 45 /*for(var key in skills) 46 { 47 if(skill.ap=="p_wake")//p_wake的是觸發式的狀態,它的last由技能自身控制? 48 { 49 if(skill.eval) 50 { 51 eval(skill.eval); 52 skill.last--; 53 if(skill.last==0)//如果持續時間結束 54 { 55 if(skill.eval2) 56 { 57 eval(skill.eval2); 58 } 59 60 delete skills[key];//刪除這個效果 61 } 62 } 63 } 64 }*/ 65 } 66 card_Closed2=null;//解除選定 67 }
但是現在的進入下一回合還缺少足夠醒目的回合提示。
這樣就完成了一個最基礎的類似戰棋游戲的操作流程,因為時間有限只介紹了主干代碼,更多的細節還需要通過調試獲取。目前還沒有聲音、AI和網絡的設定,未來應該會添加WebSocket多人聯網功能和Babylon.js內置的3D音效功能,但AI很難說。