在WebGL場景中建立遊戲規則
在前三篇文章的基礎上,為基於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(); 2UiPanel2.width = "220px"; 3UiPanel2.fontSize = "14px"; 4UiPanel2.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT; 5UiPanel2.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER; 6UiPanel2.color = "white"; 7advancedTexture.addControl(UiPanel2); 8var button3 = BABYLON.GUI.Button.CreateSimpleButton("button3", "落子"); 9button3.paddingTop = "10px"; 10button3.width = "100px"; 11button3.height = "50px"; 12button3.background = "green"; 13button3.isVisible=false;//這個按鈕預設不可見,選中並放大一張手牌後可見 14button3.onPointerDownObservable.add(function(state,info,coordinates) { 15if(MyGame.init_state==1&&card_Closed&&card_Closed.workstate!="dust")//如果完成了場景的虛擬化 16{ 17Card2Chess();//將當前選中的手牌和游標關聯起來,換回first_lock,並改變游標的顏色,點選空白地塊時落下棋子, 18} 19});
b、按下按鈕後將準星顏色改為橙色(考慮更改準星形狀?),在rule.js檔案中
1 function Card2Chess()//將當前選中的手牌設為手中棋子 2 { 3MyGame.player.centercursor.color="orange"; 4MyGame.player.changePointerLock2("first_lock");//將瀏覽方式改為first_lock 5HandCard(1);//經過動畫隱藏手牌 6 7//切換回first_lock狀態 8 }
c、在準星邊緣為橙色時點選地塊,則把手牌轉化為棋子放入棋盤裡:
首先在Tiled.js檔案的PickTiled方法裡響應地塊點選:
1 if(MyGame.player.centercursor.color=="orange")//如果當前是落子狀態 2{//mesh是棋盤中的一個地塊 3if(card_Closed&&!TiledHasCard(mesh))//如果存在選定的手牌並且點選的格子沒有其他棋子,則把棋子放到這個格子裡 4{ 5Card2Chess2(mesh);//具體程式碼在rule.js裡 6} 7else 8{ 9MyGame.player.centercursor.color=="blue"//點已經有棋子的地方,則取消落子 10} 11}
然後在rule.js里正式將棋子放入棋盤:
1 function Card2Chess2(mesh)//將手中棋子放在棋盤上 2 { 3if(card_Closed.num_group>-1&&card_Closed.num_group<5)//如果卡片在手牌的某個分組中 4{//從小組裡刪除 5delete arr_cardgroup[card_Closed.num_group][card_Closed.mesh.name]; 6/*if(Object.getOwnPropertyNames(arr_cardgroup[card.num_group]).length==0) 7{ 8arr_mesh_groupicon[card.num_group].isVisible=false; 9}*/ 10} 11card_Closed.mesh.parent=null;//card_Closed是手牌中選中的物件, 12card_Closed.mesh.parent=mesh_tiledCard; 13card_Closed.mesh.scaling=new BABYLON.Vector3(0.1,0.1,0.1); 14card_Closed.mesh.position=mesh.position.clone();//棋子放在地塊位置。 15card_Closed.mesh.position.y=0; 16card_Closed.workstate="wait"; 17noPicked(card_Closed); 18card_Closed2=card_Closed;//將它設為棋盤中的一個棋子 19card_Closed2.display();//將棋子設為可見 20PickCard2(card_Closed2);//將它設為選中的棋子 21card_Closed=null;//取消手牌中的選中物件 22MyGame.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 {//如果再次點選有已選中卡片,則把相機移到卡片面前 4if(card.isPicked) 5{ 6GetCardClose2(card);//將相機拉近到選中卡牌面前,並取消卡牌的選定 7//規定點選藍色遮罩時計算到達路徑,點選空處時清空範圍,點選其他卡牌時切換範圍,切換成手牌時清空範圍 8} 9else//如果這個棋子沒有被選中 10{ 11 12if(card.workstate=="wait")//如果棋子正等待移動,則顯示棋子的移動範圍 13{ 14DisplayRange(card);//這裡麵包含了清除已有遮罩並且保證棋子的選中 15} 16else if(card.workstate=="moved")//如果棋子已經移動,但還未工作 17{ 18//首先要檢查是否有已經顯示的遮罩 19if(arr_DisplayedMasks.length>0)//清空所有遮罩和棋子選定以及技能列表 20{ 21HideAllMask();//這裡也會清空card_Closed2 22} 23card_Closed2=card; 24getPicked(card_Closed2); 25card.isPicked=true; 26if(card_Closed2.skills["nattack"]) 27{//如果這個單位具有普通攻擊技能,則顯示普通攻擊範圍 28skill_current=card_Closed2.skills["nattack"];//如果單位具有nattack技能 29document.getElementById("str_sc").innerHTML="nattack"; 30canvas.style.cursor="crosshair"; 31DisplayRange2(card_Closed2,card_Closed2.skills["nattack"].range);//預設顯示nattack技能的範圍 32} 33} 34//如果是worked則什麼也不做->還是要顯示資訊的 35else if(card.workstate=="worked")//如果已經工作過 36{ 37if(arr_DisplayedMasks.length>0) 38{ 39HideAllMask();//這裡也會清空card_Closed2 40} 41card_Closed2=card; 42getPicked(card_Closed2); 43card.isPicked=true; 44document.getElementById("str_sc").innerHTML="Worked"; 45} 46MyGame.player.changePointerLock2("first_pick");//如果棋子沒有被選中,則瀏覽方式改為first_pick 47DisplayUnitUI();//同時也要顯示棋子操縱ui->這裡使用html dom table 48} 49 }
b、DisplayUnitUI方法顯示當前選中棋子的技能列表,其程式碼位於FullUI.js檔案中:
1 function DisplayUnitUI() 2 { 3//MyGame.SkillTable 4if(card_Closed2)//如果這時已經有選中的單位,則顯示單位的效果列表 5{ 6document.getElementById("all_base").style.display="block";//使技能列表元素可見 7var data=MyGame.SkillTable.data;//獲取技能列表的資料 8data.splice(4);//清空舊的技能列表 9var card=card_Closed2; 10document.getElementById("str_chp").innerHTML=card.chp;//當前hp 11document.getElementById("str_thp").innerHTML=card.hp;//總hp 12document.getElementById("str_cmp").innerHTML=card.cmp;//當前mp 13document.getElementById("str_tmp").innerHTML=card.mp;//總mp 14document.getElementById("str_atk").innerHTML=card.attack;//攻擊 15document.getElementById("str_speed").innerHTML=card.speed;//移動力 16//document.getElementById("str_range").innerHTML=card.range; 17var skills=card.skills; 18for(key in skills)//遍歷顯示單位所有的技能 19{ 20var skill=skills[key];//單位現在具有的技能 21var skill2=arr_skilldata[key];//技能列表裡的技能描述 22var str1=key,str2="full"; 23if(skill.last!="forever")//如果不是永久持續,要在括號裡顯示持續時間 24{ 25str1+=("("+skill.last+")"); 26} 27if(skill.reload!="full")//如果沒有裝填完成,要顯示裝填進度 28{ 29str2=skill.reload+"/"+skill2.reloadp; 30} 31data.push([str1 32,str2]); 33} 34MyGame.SkillTable.draw(data,0);//繪製表格 35requestAnimFrame(function(){MyGame.SkillTable.AdjustWidth();}); 36} 37 }
對應的,DisposeUnitUI方法用來隱藏技能列表:
1 function DisposeUnitUI() 2 { 3skill_current=null;//清空當前選中的技能 4document.getElementById("str_sc").innerHTML="";//當前技能 5canvas.style.cursor="default"; 6arr_cardTarget=[];//清空當前選擇的技能目標 7fightDistance=0; 8if(document.getElementById("div_thmask"))//刪除鎖定表頭的遮罩層 9{ 10var div =document.getElementById("div_thmask"); 11div.parentNode.removeChild(div); 12} 13if(document.getElementById(MyGame.SkillTable.id))//刪除表體 14{ 15var tab =document.getElementById(MyGame.SkillTable.id); 16tab.parentNode.removeChild(tab); 17} 18document.getElementById("all_base").style.display="none";//隱藏表格 19 }
c、FullUI.js檔案中還設定了技能列表的單元格的滑鼠響應:
滑鼠移入:
1 function SkillTableOver()//在滑鼠移入時先隱藏可能存在的舊的描述文字,然後顯示懸浮顯示描述文字 2 { 3//console.log("SkillTableOver"); 4var evt=evt||window.event||arguments[0]; 5cancelPropagation(evt); 6var obj=evt.currentTarget?evt.currentTarget:evt.srcElement; 7delete_div("div_bz"); 8Open_div("", "div_bz", 240, 120, 0, 0, obj, "div_tab"); 9document.querySelectorAll("#div_bz")[0].innerHTML = MyGame.SkillTable.html_onmouseover;//向彈出項裡寫入結構 10document.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"); 4var evt=evt||window.event||arguments[0]; 5cancelPropagation(evt); 6delete_div("div_bz"); 7 }
點選技能單元格:
1 function SkillTableClick()//點選時觸發技能的eval 2 { 3var evt=evt||window.event||arguments[0]; 4cancelPropagation(evt); 5var obj=evt.currentTarget?evt.currentTarget:evt.srcElement; 6delete_div("div_bz"); 7if(card_Closed2.workstate!="worked")//如果單位還沒有進行工作 8{ 9var skillName=obj.innerHTML.split("(")[0];//從單元格中提取技能名 10if(card_Closed2.cmp>=card_Closed2.skills[skillName].cost)//如果有足夠的mp 11{ 12skill_current=card_Closed2.skills[skillName];//skill_current表示當前技能物件 13document.getElementById("str_sc").innerHTML=skillName; 14//console.log("SkillTableClick"); 15//還要顯示這個技能的釋放範圍 16var len=arr_DisplayedMasks.length; 17for(var i=0;i<len;i++)//隱藏已有的遮罩 18{ 19arr_DisplayedMasks[i].material=MyGame.materials.mat_alpha_null;//這個數組裡存的真的只是遮罩 20} 21arr_DisplayedMasks=[]; 22canvas.style.cursor="crosshair"; 23DisplayRange2(card_Closed2,skill_current.range);//在單位周圍顯示當前技能的釋放範圍 24} 25 26} 27 28 }
6、顯示當前技能的影響範圍,並查詢範圍內的可能目標:
a、在Tiled.js中響應地塊點選事件:
1 if(skill_current!=null)//如果當前技能不為空 2{ 3if(mesh.mask.material.name == "mat_alpha_red")//如果點選的是紅色遮罩 4{ 5//有選擇的單位和技能,點選紅色遮罩,則先清空已選擇目標,以點選位置為中心顯示綠色遮罩群表示瞄準,如果瞄準範圍內存在單位,則放入target 6arr_cardTarget=[]; 7var len=arr_DisplayedMasks.length; 8for(var i=0;i<len;i++)//隱藏所有遮罩 9{ 10arr_DisplayedMasks[i].material=MyGame.materials.mat_alpha_null; 11} 12arr_DisplayedMasks=[]; 13DisplayRange2(card_Closed2,skill_current.range);//重新顯示一次釋放範圍 14DisplayRange3(mesh);//根據當前技能,在瞄準地塊周圍顯示綠色遮罩群表示影響範圍,要先呼叫一次DisplayRange2, 15} 16else if(mesh.mask.material.name == "mat_alpha_green") 17{//如果點選的是綠色遮罩 18if (card_Closed2.workstate == "wait"||card_Closed2.workstate == "moved") 19{//如果單位還沒有工作 20card_Closed2.cmp-=skill_current.cost;//消耗mp 21document.getElementById("str_cmp").innerHTML=card_Closed2.cmp; 22eval(skill_current.eval);//執行技能效果 23fightDistance=arr_noderange3[mesh.name].cost;//fight雙方的距離 24//HideAllMask(); 25//MyGame.player.changePointerLock2("first_lock"); 26 27} 28} 29else//點選影響範圍外的點 30{ 31HideAllMask();//取消選中 32MyGame.player.changePointerLock2("first_lock"); 33} 34}
b、DisplayRange3方法的引數是地塊的網格,表示在這個地塊釋放當前選中技能時的影響範圍:
1 function DisplayRange3(mesh) 2 { 3//var card=card_Closed2; 4var range=0; 5range=skill_current.range2;//range2是技能的影響範圍,注意不要和釋放範圍range混淆 6 //演算法和前兩個名稱類似的方法相似 7var node_start=mesh; 8arr_noderange3={}; 9arr_noderange3[node_start.name]={cost:0,path:[node_start.name],node:node_start}; 10var costg=0; 11//var range=card.range; 12var list_noderange=[node_start]; 13for(var i=0;i<list_noderange.length;i++) 14{ 15var arr_node_neighbor=FindNeighbor(list_noderange[i]); 16var len=arr_node_neighbor.length; 17for(var j=0;j<len;j++) 18{ 19costg=arr_noderange3[list_noderange[i].name].cost; 20costg+=1; 21if(costg>range) 22{ 23break;//因為影響範圍的cost都是相同的,所以只要有一個鄰居超過限度,則所有鄰居都不可用 24} 25//如果沒有超限 26var nextnode = arr_node_neighbor[j]; 27var path2=arr_noderange3[list_noderange[i].name].path.concat(); 28path2.push(nextnode.name); 29if(arr_noderange3[nextnode.name])//如果以前曾經到達這個節點 30{ 31if(arr_noderange3[nextnode.name].cost>costg)//這裡還是否有必要計算路徑?? 32{ 33arr_noderange3[nextnode.name]={cost:costg,path:path2,node:nextnode}; 34} 35else 36{ 37continue; 38} 39} 40else 41{ 42arr_noderange3[nextnode.name]={cost:costg,path:path2,node:nextnode}; 43list_noderange.push(nextnode); 44} 45} 46} 47for(var key in arr_noderange3)//對於每一個綠色遮罩的地塊 48{ 49//if(arr_noderange3[key].cost>0) 50//{ 51arr_noderange3[key].node.mask.material=MyGame.materials.mat_alpha_green; 52var mesh_unit = TiledHasCard(arr_noderange3[key].node);//如果這個綠色地塊中存在單位 53if(mesh_unit)//如果瞄準範圍內存在一個單位,從理論上說也可能是自己!!!! 54{ 55arr_cardTarget.push(mesh_unit.card);//則把這個單位放入目標列表 56} 57//} 58 59arr_DisplayedMasks.push(arr_noderange3[key].node.mask); 60} 61 }
7、執行技能效果:
a、在tab_skilldata.js檔案中定義了技能的eval屬性,它是以字串形式儲存的可執行程式碼,以普通攻擊技能為例:
1 nattack: 2{ 3name:"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{ 3var len=arr_cardTarget.length; 4//var count_ani=0 5if(len>0)//如果目標數大於零 6{ 7MyGame.flag_view="first_ani"; 8card_Closed2.count_ani=len;//動畫計數器,認為每一個目標都有一系列的技能流程, 9}//這一次行為中的所有技能流程都結束,這個行為才結束。 10 11for(var i=0;i<len;i++)//對於每一個目標,認為普通攻擊只會有一個目標! 12{ 13var target=arr_cardTarget[i]; 14if(target.mesh.id==card_Closed2.mesh.id)//規定自己不能nattack自己?? 15{ 16func_skills.ani_final();//什麼也不做,結束這個技能流程 17continue; 18} 19var skills=card_Closed2.skills;//當前選中棋子的技能列表 20var skillst=target.skills;//目標的技能列表 21func_skills.beforeFight(target,skills,skillst);//執行一些在fight開始前生效的被動技能 22//超多層function巢狀,有沒有更先進的解決方法?開始進入回撥地獄 23card_Closed2.ani_beat(target,function(){//撞擊動畫 24target.chp-=card_Closed2.attack;//技能目標的當前hp減少量等於選中棋子的攻擊力 25target.ani_floatstr("-"+card_Closed2.attack,[],function(){//文字上浮動畫 26if(target.chp>0)//如果目標還活著 27{ 28if(skillst["nattack"])//如果target具備nattack能力則反擊之 29{ 30target.ani_beat(card_Closed2,function(){ 31card_Closed2.chp-=target.attack; 32card_Closed2.ani_floatstr("-"+target.attack,[],function(){ 33if(card_Closed2.chp>0) 34{ 35document.getElementById("str_chp").innerHTML=card_Closed2.chp;//更新當前hp顯示 36card_Closed2.workstate="worked"; 37func_skills.ani_final(target,skills,skillst);//結束這個技能流程 38} 39else 40{ 41func_skills.unitDestory(card_Closed2,skills,skillst);//搶救 42} 43}); 44}); 45} 46} 47else 48{ 49card_Closed2.workstate="worked";//當前狀態為工作完畢 50func_skills.unitDestory(target,skills,skillst);//在target死前檢查有沒有可以自救的被動技能 51} 52}); 53}); 54 55if(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 4card_Closed2.count_ani--; 5if(card_Closed2.count_ani<=0)//假設一個aoe有多個回撥線路,要確保每個回撥線路都結束,再判定動作結束 6{ 7/*if(target) 8{//如果目標還活著 9func_skills.afterFight(target,skills,skillst); 10}*/ 11HideAllMask();//動作結束解除所有鎖定 12MyGame.player.changePointerLock2("first_lock");// 13} 14 15}
d、unitDestory方法會檢視單位是否有自救技能:
1 unitDestory:function(target,skills,skillst) 2{ 3for(key in skillst) 4{ 5var skill=skillst[key]; 6if(skill.ap=="p_destoryed"&&skill.eval) 7{ 8eval(skill.eval);//如果有自救能力則跳到另一個效果方法裡,nattack的效果則終結 9} 10} 11if(target.chp<=0)//如果沒搶救過來 12{ 13target.ani_destory(function(){//執行死亡動畫 14func_skills.ani_final(target,skills,skillst); 15}); 16} 17},
8、為單位建立動畫:
在CardMesh.js檔案裡為卡牌型單位建立了幾種簡單的行為動畫
a、撞擊目標:
1 //下面計劃要新增震動方法和被破壞方法 2 CardMesh.prototype.ani_beat=function(target,callback)// 3 { 4var mesh=this.mesh; 5mesh.animations=[]; 6var animation=new BABYLON.Animation("animation","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 7var pos1=mesh.position.clone(); 8var pos2=target.mesh.position.clone(); 9var keys=[{frame:0,value:pos1},{frame:15,value:pos2},{frame:30,value:pos1}]; 10animation.setKeys(keys); 11mesh.animations.push(animation); 12scene.beginAnimation(mesh, 0, 30, false,1,function(){ 13callback(); 14}); 15 }
b、向目標發射一個“子彈”:
1 CardMesh.prototype.ani_fire=function(target,cursor,callback) 2 {//建立一個精靈物件(或者是粒子物件?),讓它飛向目標 3var mesh=this.mesh; 4var sprite_bullet=new BABYLON.Sprite("sprite_bullet", cursor);//cursor是MyGame.SpriteManager 5sprite_bullet.parent=mesh.parent; 6sprite_bullet.position =mesh.position.clone(); 7sprite_bullet.position.y+=2 8 9var animation=new BABYLON.Animation("animation","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 10var pos1=sprite_bullet.position.clone(); 11var pos2=target.mesh.position.clone(); 12var keys=[{frame:0,value:pos1},{frame:30,value:pos2}]; 13animation.setKeys(keys); 14sprite_bullet.animations.push(animation); 15scene.beginAnimation(sprite_bullet, 0, 30, false,1,function(){ 16sprite_bullet.dispose(); 17callback(); 18}); 19 }
c、一個從單位身上飄起的字串:
1 CardMesh.prototype.ani_floatstr=function(str,styles,callback) 2 {//建立一個基於canvas紋理的物件,讓它飄起來然後消失 3var mesh=this.mesh; 4str+="";//前面如果傳來的是數字,則取不到length!!-》顯示轉換為字串 5var size_x=str.length*30; 6var mesh_str = new BABYLON.MeshBuilder.CreateGround(this.name + "mesh_str", { 7width: size_x/2.5, 8height: 16 9}, scene); 10mesh_str.parent=mesh; 11//mesh_str.position =new BABYLON.Vector3(0,0,0); 12mesh_str.renderingGroupId = 3;//這些文字是特別強調內容,使用最高階的渲染組 13var mat_str = new BABYLON.StandardMaterial(this.name + "mat_str", scene); 14var texture_str = new BABYLON.DynamicTexture(this.name + "texture_str", { 15width: size_x, 16height: 40 17}, scene); 18mat_str.diffuseTexture = texture_str; 19mesh_str.material = mat_str; 20mesh_str.rotation.x = -Math.PI / 2; 21mesh_str.isPickable = false; 22texture_str.hasAlpha=true; 23mat_str.useAlphaFromDiffuseTexture=true; 24 25//經過測試發現,在Chrome中canvas的繪圖是以影象的左上角定位的,而文字繪製則是以文字的左下角定位的!!!! 26var context_comment = texture_str.getContext(); 27context_comment.fillStyle = "rgba(255,255,255,0)";//"transparent"; 28context_comment.fillRect(0, 0, size_x, 40); 29//context_comment.fillStyle = "#ffffff"; 30context_comment.fillStyle = "#ff0000"; 31context_comment.font = "bold 30px monospace"; 32var len=styles.length; 33for(var i=0;i<len;i++) 34{ 35context_comment[styles[i][0]]=styles[i][1]; 36} 37//newland.canvasTextAutoLine(str, context_comment, 1, 30, 35, 34); 38context_comment.fillText(str,0,30);//y座標偏離一個字高 39texture_str.update(); 40 41var 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; 45var keys=[{frame:0,value:new BABYLON.Vector3(0,0,0)},{frame:30,value:new BABYLON.Vector3(0,20,0)}]; 46animation.setKeys(keys); 47mesh_str.animations.push(animation); 48scene.beginAnimation(mesh_str, 0, 30, false,1,function(){ 49mesh_str.dispose(); 50mat_str.dispose(); 51texture_str.dispose(); 52callback(); 53}); 54 55 }
d、單位變成黑白色,然後昇天
1 CardMesh.prototype.ani_destory=function(callback) 2 {//先換成灰白色圖片,然後上浮 3var mesh=this.mesh; 4this.workstate="dust" 5 6var mat_dust = new BABYLON.StandardMaterial(this.name + "mat_dust", this.scene);//測試用卡片紋理 7mat_dust.diffuseTexture = new BABYLON.Texture(this.imagedust, this.scene);//實現已經準備好了黑白色的圖片,可以用MakeDust.html工具生成 8mat_dust.diffuseTexture.hasAlpha = false; 9mat_dust.backFaceCulling = true; 10mat_dust.useLogarithmicDepth = true;//使用對數式深度快取避免“Z-fighting” 11mat_dust.freeze(); 12 13this.mesh_mainpic.material = mat_dust; 14 15var animation=new BABYLON.Animation("animation","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 16var pos1=this.mesh.position.clone(); 17var pos2=this.mesh.position.clone(); 18pos2.y+=2; 19var keys=[{frame:0,value:pos1},{frame:30,value:pos2}]; 20animation.setKeys(keys); 21mesh.animations=[]; 22mesh.animations.push(animation); 23scene.beginAnimation(mesh, 0, 30, false,1,function(){ 24//把dust的card收回手牌 25noPicked(mesh.card); 26mesh.parent=null; 27mesh.parent=mesh_arr_cards; 28mesh.scaling=new BABYLON.Vector3(0.1,0.1,0.1); 29mesh.rotation.y=0; 30mesh.card.num_group==999; 31mesh.card.dispose(); 32callback(); 33}); 34 }
e、單位跳動一下:
1 CardMesh.prototype.ani_shake=function(callback)//上下晃動一下 2 { 3var mesh=this.mesh; 4mesh.animations=[]; 5var animation=new BABYLON.Animation("animation","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT); 6var pos1=mesh.position.clone(); 7var pos2=mesh.position.clone(); 8pos2.y+=1; 9var keys=[{frame:0,value:pos1},{frame:15,value:pos2},{frame:30,value:pos1}]; 10animation.setKeys(keys); 11mesh.animations.push(animation); 12scene.beginAnimation(mesh, 0, 30, false,1,function(){ 13callback(); 14}); 15 }
9、AOE
同時攻擊多個目標意味著要同時開啟多個回撥流程,修改一下上面的nattack方法:
1 aoe:function(range2,atk,isSafe,arr_state)//造成aoe傷害,引數:影響距離、攻擊力、是否會傷害本方、[[給目標新增的效果1、生效的概率、持續的時間],[],[]] 2{ 3var len=arr_cardTarget.length; 4if(len>0) 5{ 6MyGame.flag_view="first_ani"; 7card_Closed2.count_ani=len;//有幾個目標,就設定幾個動畫計數 8} 9else 10{ 11//return; 12} 13card_Closed2.ani_shake(function(){//自己先晃動一下表示發出aoe 14var skills=card_Closed2.skills; 15 16func_skills.beforeFight(null,skills,{})//如果下面的target使用var型別變數,因為js的變數提升特性,前面的target也會被自動宣告!!,但是let並不具備變數提升功能!!!! 17for(var i=0;i<len;i++) 18{//這裡要使用let型變數,否則所有的target變數都會被設為最後定義的target導致程式出錯 19let target=arr_cardTarget[i]; 20if(isSafe&⌖.belongto==card_Closed2.belongto)//如果是安全aoe則跳過本方單位 21{ 22func_skills.ani_final(); 23continue; 24} 25 26var skillst=target.skills; 27//func_skills.beforeFight(target,{},skillst); 28target.chp-=atk; 29target.ani_floatstr("-"+atk,[],function() { 30if(target.chp>0)//如果還活著 31{ 32var len2=arr_state.length; 33for(var j=0;j<len2;j++) 34{ 35var state=arr_state[j] 36if(newland.RandomBool(state[1]))//如果通過概率判定 37{ 38if(skillst[state[0]])//如果已經有這一效果,則延長持續時間 39{ 40skillst[state[0]].last+=state[2]; 41} 42else//否則新增這個效果 43{ 44skillst[state[0]]={last:state[2],reload:"full"}; 45} 46} 47} 48func_skills.ani_final(target,skills,skillst); 49} 50else 51{ 52 53func_skills.unitDestory(target,skills,skillst); 54} 55}); 56 57} 58if(card_Closed2.workstate!="dust") 59{ 60document.getElementById("str_chp").innerHTML=card_Closed2.chp; 61card_Closed2.workstate="worked"; 62} 63 64}) 65 66 67},
10、進入下一回合:
a、在FullUI.js中新增“下一回合”按鈕:
1 var button4 = BABYLON.GUI.Button.CreateSimpleButton("button4", "下一回合"); 2button4.paddingTop = "10px"; 3button4.width = "100px"; 4button4.height = "50px"; 5button4.background = "green"; 6button4.isVisible=false; 7button4.onPointerDownObservable.add(function(state,info,coordinates) { 8if(MyGame.init_state==1)//如果完成了場景的虛擬化 9{ 10NextRound();//所有棋子的狀態變為wait,特殊狀態的除外 11} 12}); 13UiPanel2.addControl(button4); 14UiPanel2.buttonnextr=button4;
b、NextRound方法位於rule.js檔案中:
1 function NextRound()//將所有棋子的狀態置為wait(後續新增對特殊狀態的處理) 2 { 3var units=mesh_tiledCard._children; 4var len=units.length; 5for(var i=0;i<len;i++)//對於棋盤上的每個棋子 6{ 7var unit=units[i]; 8card_Closed2=unit;//選中這個單位 9var skills=unit.card.skills;//更新每個reload和last的技能時間,還要令回合結束時的被動技能生效 10for(var key in skills) 11{ 12var skill=skills[key]; 13var skill2=arr_skilldata[key]; 14if(skill.ap=="p_next")//對於每一個在跨越回合時生效的被動技能 15{ 16if(skill.eval) 17{ 18eval(skill.eval); 19} 20} 21if(skill.reload!="full") 22{ 23skill.reload++; 24if(skill.reload>=skill2.reloadp)//如果裝填完畢 25{ 26skill.reload="full" 27} 28} 29if(skill.last!="forever"&&skill.ap!="p_wake") 30{ 31skill.last--; 32if(skill.last<=0)//如果持續時間結束 33{ 34if(skill.eval2) 35{ 36eval(skill.eval2); 37} 38 39delete skills[key];//刪除這個效果 40} 41} 42} 43 44unit.card.workstate="wait"; 45/*for(var key in skills) 46{ 47if(skill.ap=="p_wake")//p_wake的是觸發式的狀態,它的last由技能自身控制? 48{ 49if(skill.eval) 50{ 51eval(skill.eval); 52skill.last--; 53if(skill.last==0)//如果持續時間結束 54{ 55if(skill.eval2) 56{ 57eval(skill.eval2); 58} 59 60delete skills[key];//刪除這個效果 61} 62} 63} 64}*/ 65} 66card_Closed2=null;//解除選定 67 }
但是現在的進入下一回合還缺少足夠醒目的回合提示。
這樣就完成了一個最基礎的類似戰棋遊戲的操作流程,因為時間有限只介紹了主幹程式碼,更多的細節還需要通過除錯獲取。目前還沒有聲音、AI和網路的設定,未來應該會新增Socket/">WebSocket多人聯網功能和Babylon.js內建的3D音效功能,但AI很難說。