在前三篇文章的基础上,为基于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 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 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很难说。