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