在WebGL场景中管理多个卡牌对象的实验
这篇文章讨论如何在基于babylon.js的webgl场景中,实现多个简单卡牌类对象的显示、选择、分组、排序,同时建立一套实用的3d场景代码框架。由于作者美工能力有限,所以示例场景视觉效果可能欠佳,本文的重点在于对相关技术的探讨。
因为文章比较长,读者可以考虑将网页导出为mhtml格式,使用word浏览。chrome浏览器导出mhtml文件的方法见末尾。
一、显示效果:
1、访问https://ljzc002.github.io/cardsimulate/html/test2.html查看“卡牌模拟页面”:
场景中间是三个作为参照物的小球,视口平面的中间是一个用babylon.js gui制作的准星,默认鼠标与准星锁定在一起,直接移动鼠标即可改变相机视角,使用wasd shift 空格键可以控制相机前、左、后、右、下、上运动(可能将ctrl键设为向下更符合传统,但是没有找到禁用浏览器ctrl+s快捷键的方法,只好用shift代替)。因为光标被锁定,将这种浏览状态命名为“first_lock”。
2、按下alt键,75张卡片通过动画移入相机视野,同时相机的位置被固定(但仍可以通过拖动鼠标改变视角):
点击右侧的“向上两行”和“向下两行”按钮可以上下滚动卡片,再次按下alt键将隐藏卡片,同时恢复相机的移动和光标的锁定。因为这种浏览状态主要用来点选场景中的物体,将它命名为“first_pick”。
3、鼠标左键单击一张卡片,卡片将处于“选中状态”(绿色边缘),再次左键单击处于选中状态的卡片,卡片将被放大拉近显示,再左键单击将恢复原位:
执行动画时会禁用用户的控制,完全由动画控制视角,所以将这种浏览状态命名为“first_ani”。
4、模仿windows的文件多选编写了卡片多选功能,按下ctrl时可以点选多个卡片,按下shift时可以选取首尾之间的所有卡片:
5、选中若干张卡片后,按1-5键可以将被选中的卡片编为1-5队,被编队的卡片将按编队顺序显示在最高处,同时编队的前面会显示队号标记:
6、在first_pick状态可以使用上下左右方向键进行场景漫游,可以看到场景中的所有对象:
二、代码实现:
1、文件结构:
cardsimulate工程的文件结构如下图所示:
其中lib目录下是从网上下载的代码库
babylon.32.all.maxs.js是babylon.js引擎库
earcut.dev.js是一个babylon.js扩展,其功能是在网格上挖洞
stat.js是用来显示帧数的代码
mylib是自己编写的代码库
events.js是一些用来处理事件的方法
filetext.js是与文件处理相关的代码
newland.js是自己编写的一些babylon.js辅助类
view.js是html视图的一些相关方法
page是直接操纵这个页面(webgl场景)的代码库
character.js是场景中出现的各种对象的类(比如卡牌网格、相机网格)
control20180312.js是用来处理鼠标键盘输入的代码
drawcard.js是用来绘制卡牌的代码
fullui.js是用来绘制全局(全屏)ui的代码
game.js是游戏类,存储用来调度整个场景的信息
handlecard.js是用来处理已经绘制出的卡牌的代码,后期考虑和drawcard.js整合在一起
handlecard2.js是一个分枝修改版
moves.js是运动计算代码
tab_carddata.js里是卡牌种类信息
tab_somedata.js里是其他辅助信息
2、代码入口与场景初始化:
a、代码由test2.html开始执行,其中一部分和前面几篇文章用到的相似:
1 <!doctype html> 2 <html lang="en"> 3 <head> 4 <meta charset="utf-8"> 5 <title>第二个场景测试,手牌的显示、排列、分组排序,显示瓷砖地面</title> 6 <link href="../css/newland.css" rel="stylesheet"> 7 <link href="../css/stat.css" rel="stylesheet"> 8 <script src="../js/lib/babylon.32.all.maxs.js"></script> 9 <script src="../js/lib/stat.js"></script> 10 <script src="../js/mylib/events.js"></script> 11 <script src="../js/mylib/filetext.js"></script> 12 <script src="../js/mylib/newland.js"></script> 13 <script src="../js/mylib/view.js"></script> 14 <script src="../js/page/game.js"></script> 15 <script src="../js/page/character.js"></script> 16 <script src="../js/page/control20180312.js"></script> 17 <script src="../js/page/moves.js"></script> 18 <script src="../js/page/drawcard.js"></script> 19 <script src="../js/page/tab_carddata.js"></script> 20 <script src="../js/page/tab_somedata.js"></script> 21 <script src="../js/page/handlecard2.js"></script> 22 <script src="../js/page/fullui.js"></script> 23 </head> 24 <body> 25 <div id="div_allbase"> 26 <canvas id="rendercanvas"></canvas> 27 <div id="fps" style="z-index: 301;"></div> 28 </div> 29 </body> 30 <script> 31 var version=1.0,author="lz_newland@163.com"; 32 var machine,canvas,engine,scene,gl,mygame={}; 33 canvas = document.getelementbyid("rendercanvas"); 34 engine = new babylon.engine(canvas, true); 35 engine.displayloadingui(); 36 gl=engine._gl;//决定在这里结合使用原生opengl和babylon.js; 37 scene = new babylon.scene(engine); 38 var divfps = document.getelementbyid("fps"); 39 40 var mygame={}; 41 window.onload=beforewebgl; 42 function beforewebgl() 43 { 44 if(engine._webglversion==2.0)//输出es版本 45 { 46 console.log("es3.0"); 47 } 48 else{ 49 console.log("es2.0"); 50 } 51 mygame=new game(0,"first_pick","","http://127.0.0.1:8082/");//建立mygame对象用来进行全局调度 52 /*0-startwebgl 53 * */ 54 webglstart(); 55 }
。。。
56 </script> 57 </html>
但与前面的简单场景将主要代码都写在webglstart方法中不同,对于较为复杂的流程最好将流程的每个阶段写在单独的方法里,对于较多的对象则最好提取对象的共同点作为一个“类”,将每个对象作为类的实例。这样可以将程序的复杂度分解,每次只关注其中的一小部分,降低编程难度。(设计模式的本质是对变量名进行管理,理论上讲,如果编程者的记忆力足够强、编程者之间的沟通效率足够高,则这些所谓的“设计模式”都可以省略)
b、在webglstart方法中对场景初始化流程进行了划分,各个流程如注释所示:
1 //对象框架架构 2 function webglstart() 3 { 4 //initwebsocket();//如何确保上一环结成功才开启下一环节? 5 initscene();//初始化场景,包括最初入门教程里的那些东西 6 initarena();//初始化地形,包括天空盒,参照物等 7 initevent();//初始化事件 8 initui();//初始化场景ui 9 initobj();//初始化一开始存在的可交互的物体 10 initloop();//初始化渲染循环 11 mygame.init_state=1;//更新初始化状态 12 engine.hideloadingui();//隐藏载入ui 13 //mygame.flag_startr=1;//这个是通过nohurry计时器自动启动的,不需要手动启动 14 }
c、初始化场景
1 function initscene() 2 {//光照 3 var light0 = new babylon.hemisphericlight("light0", new babylon.vector3(0, 1, 0), scene); 4 light0.diffuse = new babylon.color3(1,1,1);//这道“颜色”是从上向下的,底部收到100%,侧方收到50%,顶部没有 5 light0.specular = new babylon.color3(0,0,0); 6 light0.groundcolor = new babylon.color3(1,1,1);//这个与第一道正相反 7 mygame.lights.light0=light0;//将光照变量交给mygame对象管理 8 mesh_arr_cards=new babylon.mesh("mesh_arr_cards", scene); 9 //相机对象 10 var camera0= new babylon.freecamera("freecamera", new babylon.vector3(0, 0, 0), scene); 11 //camera0.layermask = 2; 12 //camera0.position=new babylon.vector3(0, 0, -20); 13 camera0.minz=0.001; 14 scene.activecameras.push(camera0); 15 16 //用ballman作为cameramesh的mesh 17 var player = new ballman(); 18 var obj_p={};//初始化参数 19 //计划不使用物理引擎 20 var mesh_ballman=new babylon.mesh("mesh_ballman",scene); 21 obj_p.mesh=mesh_ballman; 22 obj_p.name="本机";//显示的名字 23 obj_p.id="本机";//websocket sessionid 24 obj_p.image="../assets/image/rainbow.jpg"; 25 player.init( 26 obj_p,scene 27 ); 28 29 var cameramesh=new cameramesh(); 30 var obj_p={};//初始化参数 31 obj_p.mesh=mesh_ballman; 32 obj_p.mesh.isvisible=false; 33 obj_p.mesh.position=new babylon.vector3(0,0,-20); 34 if(obj_p.mesh.ballman) 35 { 36 obj_p.mesh.ballman.head.position=obj_p.mesh.position.clone(); 37 } 38 obj_p.methodofmove="host20171018"; 39 obj_p.name="freecamera";//显示的名字 40 obj_p.id="freecamera";//websocket sessionid 41 obj_p.camera=camera0; 42 //obj_p.image="assets/image/play.png"; 43 obj_p.flag_objfast=5; 44 cameramesh.init( 45 obj_p,scene 46 ); 47 mygame.arr_myplayers[obj_p.name]=cameramesh; 48 mygame.player=cameramesh; 49 mygame.cameras.camera0=camera0; 50 camera0.position=cameramesh.mesh.position.clone(); 51 cameramesh.mesh.rotation=camera0.rotation.clone(); 52 mesh_arr_cards.position=mygame.player.mesh.ballman.backview._absoluteposition.clone(); 53 }
其中mesh_arr_cards是所有手牌的父网格,用来对手牌进行定位,事实上这个对象放在initarena或者initobj阶段更加合理,但是因为相机对象的一些事件和这个网格有关,只好放在场景初始化阶段。ballman的外观是一个球体网格,用来代表场景中的玩家,其用法可以参考https://www.cnblogs.com/ljzc002/p/7274455.html;cameramesh是一个网格与相机的结合体,在第三人称时用户将能看见自己操纵的单位(关于ballman和cameramesh类的参数将在后面详细介绍)。最后把各种对象都交给mygame统一管理。
d、初始化环境
1 function initarena() 2 { 3 var mesh_base=new babylon.meshbuilder.createsphere("mesh_base",{diameter:1},scene); 4 mesh_base.material=mygame.materials.mat_green; 5 mesh_base.position.x=0; 6 mesh_base.renderinggroupid=2; 7 //mesh_base.layermask=2; 8 var mesh_base1=new babylon.meshbuilder.createsphere("mesh_base1",{diameter:1},scene); 9 mesh_base1.position.y=10; 10 mesh_base1.position.x=0; 11 mesh_base1.material=mygame.materials.mat_green; 12 mesh_base1.renderinggroupid=2; 13 //mesh_base1.layermask=2; 14 var mesh_base2=new babylon.meshbuilder.createsphere("mesh_base2",{diameter:1},scene); 15 mesh_base2.position.y=-10; 16 mesh_base2.position.x=0; 17 mesh_base2.material=mygame.materials.mat_green; 18 mesh_base2.renderinggroupid=2; 19 //mesh_base2.layermask=2; 20 for(var i=0;i<5;i++)//建立五个标示组号的标记网格,标记从一(而不是零)开始 21 { 22 var plane=new babylon.meshbuilder.createplane("mesh_groupicon"+(i+1),{size:5},scene); 23 var mat_plane = new babylon.standardmaterial("mat_plane"+(i+1), scene); 24 var texture_plane= new babylon.dynamictexture("texture_plane"+(i+1), {width:100, height:100}, scene); 25 mat_plane.diffusetexture =texture_plane; 26 plane.material=mat_plane; 27 var font = "bold 60px monospace"; 28 texture_plane.drawtext((i+1), 40, 70, font, "white", "green", true, true);//第一个是文字颜色,第二个则是完全填充的背景色 29 plane.position.x=-16; 30 plane.position.z=-2; 31 plane.renderinggroupid=2; 32 //plane.rotation.x=-math.pi/2;//这会导致*相机的视角发生bug??y与z轴混淆? 33 plane.ispickable=false; 34 plane.isvisible=false; 35 arr_mesh_groupicon.push(plane); 36 //plane.parent=mesh_arr_cards; 37 } 38 }
建立了三个小绿球作为场景的参照物,建立了五个小平面作为分组标记,这五个标记暂时不可见(在调试分组标记的过程中babylon.js发生了bug,相机输入的y轴和z轴发生混淆,但没有深入分析原因)。
e、初始化事件
1 function initevent() 2 { 3 initmouse(); 4 window.addeventlistener("keydown", onkeydown, false);//按键按下 5 window.addeventlistener("keyup", onkeyup, false);//按键抬起 6 window.addeventlistener("resize", function () { 7 if (engine) { 8 engine.resize(); 9 } 10 },false); 11 }
initmouse中是对鼠标的四种事件监听,具体代码在control20180312.js文件中,接下来监听了按键按下、按键抬起、窗口尺寸变化。
f、初始化ui
1 function initui() 2 { 3 makefullui(); 4 //var advancedtexture = mygame.fsui; 5 6 }
代码主体在fullui.js文件中
g、初始化对象
1 function initobj() 2 {//添加75个(?)实验对象 3 4 drawcard4(); 5 sortcard(); 6 }
具体代码在drawcard.js中
h、初始化渲染循环(也是逻辑循环)
1 function initloop() 2 { 3 var _this=mygame; 4 scene.registerbeforerender(function() { //比runrenderloop更早 5 }); 6 scene.registerafterrender( 7 function() { 8 if(mygame.flag_startr==1)//如果开始渲染了 9 {//如果正在使用相机网格进行漫游 10 if(mygame.player.prototype=cameramesh&&mygame.flag_view=="first_lock") 11 { 12 host20171018(mygame.player); 13 } 14 } 15 } 16 ); 17 18 engine.runrenderloop(function () //场景逻辑和ai也从这里引入 19 { 20 if (divfps) { 21 // fps 22 divfps.innerhtml = engine.getfps().tofixed() + " fps"; 23 } 24 mygame.handlenohurry();//这里包含了运动使用的计时器 25 if(_this.flag_startr==1||_this.flag_view!="first_pick") 26 { 27 //主相机和小地图相机都随着玩家的位置变化 28 camerasfollowactor(_this.player); 29 } 30 31 _this.scene.render(); 32 33 }); 34 }
其中registerbeforerender是在每一帧渲染之前执行的代码,registerafterrender是在每一帧渲染之后执行的代码,除了scene之外mesh类对象也可以使用这样的方法,这也意味着可以将渲染前后的代码分散写在多个地方,但这里为了方便管理统一写在一处。host20171018是根据按键状态和视角计算player运动的方法,具体代码在moves.js文件中。
runrenderloop里是每一帧渲染时执行的代码,这里首先更新了当前帧数显示,然后通过handlenohurry(代码在game类中)执行一些“需要周期性执行,但没有必要每一帧都执行的代码”,接下来通过camerasfollowactor(moves.js文件中)让“和player关联但不是player子元素”的其他对象跟随player运动,最后调用场景的渲染方法。
关于运动,上述代码的计算流程是这样的:
player的position-》计算出player的_absoluteposition(?)-》registerbeforerender-》根据player的_absoluteposition计算关联对象的新位置-》渲染-》registerafterrender-》host20171018更新player的position。
考虑到player也许是其他元素的子元素,其position(位置)和_absoluteposition(绝对位置)可能不同(相差物体的世界矩阵),需要使用_absoluteposition来定位关联对象;而babylon.js根据position计算_absoluteposition的操作发生在registerbeforerender之前,所以如果我们把host20171018放在registerbeforerender中则会导致position更新而_absoluteposition仍为旧的,表现的效果就是相机运动时对象抖动。所以我们把host20171018放在registerafterrender中,当然如果position和_absoluteposition完全相同,则从理论上讲不存在这种限制,但并未测试过。
3、game类
a、初始化方法:
1 game=function(init_state,flag_view,wsuri,h2uri) 2 {//参数:初始化时的状态代号,初始化时的浏览模式,websocket的服务器地址,h2数据库地址 3 var _this = this; 4 this.scene=scene; 5 this.loader = new babylon.assetsmanager(scene);//资源管理器,用于预先加载资源 6 //控制者数组 7 this.arr_myplayers={}; 8 this.arr_npcs={};//npc数组 9 this.count={};//综合计数器对象 10 this.count.count_name_npcs=0;//npc命名计数器,每产生一个npc则加一,避免npcid重复 11 this.cameras={};//scene里也有?,综合相机对象 12 this.websocket; 13 this.lights={};//综合光源对象 14 this.fsui=babylon.gui.advanceddynamictexture.createfullscreenui("ui1");//全屏gui对象 15 this.hl=new babylon.highlightlayer("hl1", scene);//高光层对象,下面是高光层的一些参数 16 this.hl.blurverticalsize = 1.0;//这个影响的并不是高光的粗细程度,而是将它分成 多条以产生模糊效果,数值表示多条间的间隙尺寸 17 this.hl.blurhorizontalsize =1.0; 18 this.hl.innerglow = false;//取消内部光晕 19 this.hl.alphablendingmode=3; 20 //this.hl.isstroke=true; 21 //this.hl.blurtexturesizeratio=2; 22 //this.hl.maintexturefixedsize=100; 23 //this.hl.renderinggroupid=3; 24 //this.hl._options.maintextureratio=1000; 25 26 this.wsuri=wsuri; 27 this.init_state=init_state;//当前运行状态 28 /*0-startwebgl 29 1-webglstarted 30 2-planetdrawed 31 * */ 32 this.h2uri=h2uri; 33 //我是谁 34 this.whoami=newland.randomstring(8); 35 36 this.materials={};//综合材质对象,下面初始化了几种常用的材质 37 var mat_frame = new babylon.standardmaterial("mat_frame", scene); 38 mat_frame.wireframe = true; 39 this.materials.mat_frame=mat_frame; 40 var mat_red=new babylon.standardmaterial("mat_red", scene); 41 mat_red.diffusecolor = new babylon.color3(1, 0, 0); 42 var mat_green=new babylon.standardmaterial("mat_green", scene); 43 mat_green.diffusecolor = new babylon.color3(0, 1, 0); 44 var mat_blue=new babylon.standardmaterial("mat_blue", scene); 45 mat_blue.diffusecolor = new babylon.color3(0, 0, 1); 46 this.materials.mat_red=mat_red; 47 this.materials.mat_green=mat_green; 48 this.materials.mat_blue=mat_blue; 49 50 this.models={};//综合模型对象 51 this.textures={};//综合纹理对象 52 this.texts={};//综合文本对象 53 54 this.flag_startr=0;//开始渲染并且地形初始化完毕 55 this.flag_starta=0;//开始执行npc的ai逻辑 56 this.list_nohurry=[];//需要周期性进行的工作 57 this.nohurry=0;//一个计时器,让一些计算不要太频繁 58 this.flag_online=false;//是否是在线场景 59 this.flag_view=flag_view;//first/third/input/free 60 this.flag_controlenabled = false; 61 this.arr_keystate=[];//按键状态数组 62 }
这段代码中初始化了一些场景中可能会用到的变量,最整洁的情况是把所有的全局变量都作为mygame的属性加以管理,但很难做到。
b、原型方法:
每个game类的实例都会继承这些方法:
1 game.prototype={ 2 addnohurry:function(name,delay,lastt,todo,count) 3 {//名字,每次执行之间的间隔(最小间隔),上一次执行时间,要执行的函数名,已经执行的次数 4 if(this.list_nohurry[name])//如果已经有叫做这个名字的任务 5 { 6 return; 7 } 8 this.list_nohurry[name]={delay:delay,lastt:lastt,todo:todo 9 ,count:count}; 10 }, 11 removenohurry:function(name) 12 { 13 delete this.list_nohurry[name]; 14 }, 15 handlenohurry:function() 16 { 17 var _this=this; 18 if( _this.flag_startr==0)//开始渲染并且地形初始化完毕!! 19 { 20 engine.hideloadingui();//隐藏载入ui 21 _this.flag_startr=1;//标志开始渲染 22 _this.lastframet=new date().gettime(); 23 _this.firstframet=_this.lastframet; 24 _this.deltatime=0; 25 } 26 else 27 {//如果已经开始渲染 28 _this.currentframet=new date().gettime();//当前帧的时间 29 _this.deltatime=_this.currentframet-_this.lastframet;//取得两帧之间的时间 30 _this.lastframet=_this.currentframet; 31 /*_this.nohurry+=_this.deltatime;//这个代码用于只执行一个定时任务的情况 32 33 if(mygame&&_this.nohurry>1000)//每一秒进行一次 34 { 35 _this.nohurry=0; 36 37 }*/ 38 //var time_start=_this.currentframet-_this.firstframet;//当前时间到最初过了多久 39 for(var i=0;i<_this.list_nohurry.length;i++)//对于每一个定时任务 40 { 41 var obj_nohurry=_this.list_nohurry[i]; 42 if(obj_nohurry.lastt==0)//如果上次执行时间是0,则以当前时间作为上次执行时间 43 { 44 obj_nohurry.lastt=new date().gettime(); 45 } 46 else 47 { 48 var time_start=_this.currentframet-obj_nohurry.lastt;//当前帧到上次执行经过的时间 49 if(time_start>obj_nohurry.delay)//如果经过的时间超过了每次执行周期乘以执行次数加一,则执行一次 50 { 51 obj_nohurry.todo(); 52 obj_nohurry.count++; 53 obj_nohurry.lastt=_this.currentframet; 54 break;//每一帧最多只做一个费时任务,周期更短的任务放在list_nohurry队列前面,获得更多执行机会 55 } 56 } 57 58 } 59 if(_this.flag_starta==1)//除非开始进行ai计算,否则只处理和基本ui有关的内容 60 { 61 62 } 63 } 64 } 65 }
这里的三个方法都是和定时任务有关的,将需要执行的定时任务放在list_nohurry中,在引擎每一次渲染循环时检测是否需要执行队列中的任务,因为要尽量减少每一帧的时间差异,规定每一帧最多只执行一个任务,到时但未执行的任务需要延后到下一帧判断是否执行,队列中越靠前的任务被及时执行的可能性越高。
上述方法并没有实际使用过,一个类似的执行定时任务的例子可以在https://www.cnblogs.com/ljzc002/p/7373046.html查看。
4、object类:
object类是场景中所有受控物体的基类,包含运动控制和姿态控制所需的一些信息,其代码位于newland.js文件中。
a、初始化代码:
1 newland.object=function() 2 { 3 4 } 5 newland.object.prototype.init = function(param) 6 { 7 //启用物理引擎后这一部分可能用不上,但暂时保留 8 this.keys={w:0,s:0,a:0,d:0,space:0,ctrl:0,shift:0};//按键是否保持按下,已经改为由mygame管理 9 this.witha0={forward:0,right:0,up:-9.82};//非键盘控制产生的加速度 10 this.witha={forward:0,right:0,up:-9.82};//环境加速度,包括地面阻力和重力,现在还没有风力 11 this.witha2={forward:0,right:0,up:0};//键盘控制加速度与物体本身加速度和非键盘控制产生的加速度合并后的最终加速度 12 this.v0={forward:0,right:0,up:0};//上一时刻的速度 13 this.vt={forward:0,right:0,up:0};//下一时刻的速度 14 this.vm={forward:15,backwards:5,left:5,right:5,up:100,down:100};//各个方向的最大速度 15 this.fm={forward:2,backwards:1,left:1,right:1,up:10,down:10};//各个方向的最大发力 16 this.ff=0.05;//在地面不做任何发力时的阻力效果 17 //this.flag_runfast=1;//速度系数 18 this.ry0=0;//上一时刻的y轴转角 19 this.ryt=0;//下一时刻的y轴转角 20 this.rychange=0;//y轴转角差 21 this.mchange={forward:0,right:0,up:0};//物体自身坐标系上的位移 22 this.vmove=new babylon.vector3(0,0,0);//世界坐标系中每一时刻的位移和量 23 this.py0=0;//记录上一时刻的y轴位置,和下一时刻比较确定物体有没有继续向下运动!!,用于判断物体是否接触地面 24 25 param = param || {}; 26 this.mesh=param.mesh; 27 this.meshname=this.mesh.name; 28 this.skeletonsplayer=param.skeletonsplayer||[];//如果和某个babylon.js模型关联,则提取模型的骨骼动画 29 this.submeshs=param.submeshs;//提取子网格 30 this.ry0=param.mesh.rotation.y; 31 this.py0=param.mesh.position.y; 32 this.flag_runfast=param.flag_runfast ||1;//速度系数,最终位移要乘以速度系数 33 this.standontheground=0;//一开始在空中,落到地上,是否接触地面 34 //this.flag_objfast=param.flag_objfast ||1; 35 this.countstop=0;//记录物体静止了几次,如果物体一直静止就停止发送运动信息,在联网情况下减少数据传输 36 37 this.playannimation = false;//是否在执行动画 38 this.methodofmove=param.methodofmove||"";//运动算法 39 this.path_goto="sleep";//这个物体接到指令要去哪里,是一个向量数组(路径),在寻路算法中使用 40 41 //window.addeventlistener("keydown", onkeydown, false);//按键按下 42 //window.addeventlistener("keyup", onkeyup, false);//按键抬起 43 }
这里将一些物体可能用到的变量保存在基类中,减化了子类物体的创建代码。
b、其他原型方法
1 //骨骼动画 2 newland.object.prototype.beginsp=function(num_type)//执行骨骼动画列表里的某一个骨骼动画 3 { 4 if(this.skeletonsplayer.length>0) 5 { 6 this.sp = this.skeletonsplayer[num_type]; 7 8 this.totalframe = this.skeletonsplayer[0]._scene._activeskeletons.data.length;//总帧数 9 this.start = 0; 10 this.end = 100; 11 this.vitesseanim = parsefloat(100 / 100);//动画的速度比 12 scene.beginanimation(this.sp, (100 * this.start) / this.totalframe, (100 * this.end) / this.totalframe, true, this.vitesseanim);//启动动画,skeletonsplayer是一个骨骼动画对象 13 this.playannimation = true; 14 } 15 else 16 {//本体不能启动骨骼动画,则直接启动其子元素的骨骼动画 17 var len=this.submeshs.length; 18 for(var i=0;i<len;i++) 19 { 20 var skeleton=this.submeshs[i].skeleton; 21 var totalframe = skeleton._scene._activeskeletons.data.length;//总帧数 22 var start = 0; 23 var end = 100; 24 var vitesseanim = parsefloat(100 / 100);//动画的速度比 25 scene.beginanimation(skeleton, (100 * start) / totalframe, (100 * end) / totalframe, true, vitesseanim); 26 } 27 this.playannimation = true; 28 } 29 } 30 newland.object.prototype.stopsp=function(num_type) 31 { 32 this.playannimation = false; 33 if(this.skeletonsplayer.length>0) 34 { 35 scene.stopanimation(this.skeletonsplayer[0]); 36 } 37 else 38 { 39 var len=this.submeshs.length; 40 for(var i=0;i<len;i++) 41 { 42 var skeleton=this.submeshs[i].skeleton; 43 scene.stopanimation(skeleton); 44 } 45 } 46 }
object类具有两个和骨骼动画相关的原型方法,用来控制骨骼动画的启停(方法编程时间较早,在新版babylon.js中也许会有错误)
5、ballman类:
ballman类是object类的一个子类,主要用来在不载入模型的情况下,用简单的球体网格进行物体移动、视角变化、对象拾取等试验。代码位于character.js文件中。
a、初始化代码:
1 ballman=function()//只用来显示其他玩家?-》自己也要显示 2 { 3 newland.object.call(this);//调用父类的构造方法 4 } 5 ballman.prototype=new newland.object();//继承父类的属性 6 ballman.prototype.init=function(param,scene) 7 { 8 param = param || {}; 9 newland.object.prototype.init.call(this,param);//调用父类的初始化方法 10 this.name=param.name; 11 this.id=param.id; 12 //this.vd={forward:10.0,backwards:10.0,left:10.0,right:10.0,up:10.0,down:10.0};//简单运动时各个方向的默认速度 13 //this.flag_objfast=param.flag_objfast ||1;//使用这种机体移动物体的默认速度 14 15 var mat_head=new babylon.standardmaterial("mat_head", scene);//球体(头部)的材质 16 mat_head.diffusetexture =new babylon.texture(param.image,scene);//将球体材质的漫反射纹理设置为一张图片 17 mat_head.freeze();//冻结材质,减少向显卡传递数据,据说能提升性能 18 var mesh_head=babylon.mesh.createsphere(this.name+"head", 10, 2.0, scene);//建立一个球体 19 mesh_head.renderinggroupid=2;//渲染组设为2,这里规定隐形物体渲染组为0,远处的背景物体渲染组为1,普通物体行为2,特别强调的物体为3 20 mesh_head.layermask=2; 21 //mesh_head.rotation.y=math.pi*0.5; 22 mesh_head.material=mat_head; 23 //mesh_head.parent=this.mesh;//想让head随着ghost一起位移,又不想让它随着ghost滚动!! 24 //this.mesh.setphysicslinkwith(mesh_head,new babylon.vector3(0,0,0),new babylon.vector3(0,0,0));//枢轴链接 25 mesh_head.position=this.mesh.position.clone();//不克隆直接赋值有抖动 26 mesh_head.ispickable=false;//不可被选取 27 this.head=mesh_head; 28 this.mesh.ballman=this; 29 30 //改用gui?显示名字 31 if(this.lab) 32 { 33 this.lab.dispose(); 34 this.lab=null; 35 } 36 var label = new babylon.gui.rectangle(this.name); 37 label.background = "black"; 38 label.height = "30px"; 39 label.alpha = 0.5; 40 label.width = "100px"; 41 label.cornerradius = 20; 42 label.thickness = 1; 43 label.linkoffsety = 30;//位置偏移量?? 44 mygame.fsui.addcontrol(label); 45 label.linkwithmesh(this.head); 46 var text1 = new babylon.gui.textblock(); 47 text1.text = this.name; 48 text1.color = "white"; 49 label.addcontrol(text1); 50 label.isvisible=true; 51 label.layermask=2; 52 this.lab=label; 53 54 //定位第一人称视角的位置 55 var headview=new babylon.mesh(this.name+"headview",scene);//用网格定义一个位置,位于这个位置的物体可以是headview的子元素,这样它将随着ballman一起移动 56 headview.parent=this.head; 57 headview.position=new babylon.vector3(0,0,2.0); 58 this.headview=headview; 59 //定位第三人称视角的位置 60 var backview=new babylon.mesh(this.name+"backview",scene); 61 backview.parent=this.head; 62 backview.position=new babylon.vector3(0,2,-6); 63 this.backview=backview; 64 var backview_right=new babylon.mesh(this.name+"backview_right",scene); 65 backview_right.parent=this.head; 66 backview_right.position=new babylon.vector3(2.6,2,-6); 67 this.backview_right=backview_right; 68 //定位手持物体的位置 69 var handpoint=new babylon.mesh(this.name+"handpoint",scene); 70 handpoint.parent=this.head; 71 handpoint.position=new babylon.vector3(0,0,10); 72 this.handpoint=handpoint; 73 //左手和右手 74 var lefthand=new babylon.mesh(this.name+"lefthand",scene); 75 lefthand.parent=this.head; 76 lefthand.position=new babylon.vector3(-1,0.2,3.0); 77 lefthand.lookat(lefthand.position.negate().add(headview.position)); 78 this.lefthand=lefthand; 79 var righthand=new babylon.mesh(this.name+"righthand",scene); 80 righthand.parent=this.head; 81 righthand.position=new babylon.vector3(1,0.2,3.0); 82 righthand.lookat(righthand.position.negate().add(headview.position)); 83 this.righthand=righthand; 84 85 //暂时不使用抬头显示器 86 console.log("player初始化完毕"); 87 88 }
关于渲染组的材料可以查看babylon.js官网关于网格渲染顺序的文档(transparency and how meshes are rendered),可以在这里下载简单的中英对照http://down.51cto.com/data/2452124
代码的中部用gui绘制了一个显示玩家名字的文本框,并设置文本框跟随ballman(关于2dgui资料可以查看https://www.cnblogs.com/ljzc002/p/7699162.html,前段时间babylon.js官方推出了新的3dgui,但是仍然以2dgui为基础,并没有突破性的进展)。
代码后部为ballman的头部(需要注意ballman的头部是网格,而ballman对象并不是)添加了一系列子元素(这里的_children不叫做“子网格”是为了防止和前面的submesh相区分,前者指子元素使用父网格的局部坐标系,后者则指将父网格分为不同的区块,每个区块使用不同的材质),用来表示ballman身上的各个位置。
6、cameramesh类
cameramesh类也是object的子类,用来给相机绑定一个网格,这样一方面玩家可以在第三人称操作时看到自身,另一方面可以使用网格一些物理引擎方法。cameramesh类的代码在character.js文件中。在这个工程中我将一个ballman网格绑定给了相机。
a、初始化方法:
1 /*20180613现在规定主相机在mygame中对应三种状态: 2 first_lock表示相机和相机网格绑定在一起并使用control控制, 3 4 first_ani表示由动画控制相机相机不可手动控制 5 first_pick表示相机位置不可以移动,但是可以改变视角进行点击(是在没有锁定指针属性时的替代方法??)*/ 6 cameramesh=function() 7 { 8 newland.object.call(this); 9 } 10 cameramesh.prototype=new newland.object(); 11 cameramesh.prototype.init=function(param,scene) 12 { 13 param = param || {}; 14 newland.object.prototype.init.call(this,param);//继承原型的方法 15 this.name=param.name; 16 this.id=param.id; 17 var num_v=0.001; 18 this.vd={forward:num_v*2,backwards:num_v,left:num_v,right:num_v,up:num_v,down:num_v};//简单运动时各个方向的默认速度,最慢的情况下每一毫秒移动多少 19 this.flag_objfast=param.flag_objfast ||1;//使用这种机体移动物体的默认速度 20 this.camera=param.camera; 21 this.mesh=param.mesh;//可以把这个mesh指定为ballman!!!! 22 this.camera.mesh=this.mesh; 23 var _this = this; 24 //中间光标,准星 25 this.centercursor=this.centercursor(); 26 this.centercursor.isvisible=false; 27 this._initpointerlock();//先不要锁定光标,等初始化地形完毕后再锁定? 28 29 console.log("相机网格初始化完毕"); 30 }
cameramesh.prototype.handleusermouse=function(evt, pickinfo)
{
//this.weapon.fire(pickinfo);//fps和tps的武器射击同样由这个类负责
}
在25行附近使用gui在窗口的中心绘制了一个准星:
1 //准心 2 cameramesh.prototype.centercursor=function() 3 { 4 //在屏幕中心绘制一个光标 5 var rect_centor=new babylon.gui.rectangle(); 6 rect_centor.width = "80px"; 7 rect_centor.height = "80px"; 8 rect_centor.alpha=0.5; 9 rect_centor.color="blue"; 10 mygame.fsui.addcontrol(rect_centor); 11 12 var rect_line1=new babylon.gui.rectangle();//gui不能直接绘制线段,所以用一个细长的矩形表示线段 13 rect_line1.width = "2px"; 14 rect_line1.height = "20px"; 15 rect_line1.color = "black"; 16 rect_line1.thickness = 4; 17 rect_line1.alpha = 0.5; 18 rect_line1.verticalalignment=babylon.gui.control.vertical_alignment_top; 19 rect_centor.addcontrol(rect_line1); 20 var rect_line2=new babylon.gui.rectangle(); 21 rect_line2.width = "2px"; 22 rect_line2.height = "20px"; 23 rect_line2.color = "black"; 24 rect_line2.thickness = 4; 25 rect_line2.alpha = 0.5; 26 rect_line2.verticalalignment=babylon.gui.control.vertical_alignment_bottom; 27 rect_centor.addcontrol(rect_line2); 28 var rect_line3=new babylon.gui.rectangle(); 29 rect_line3.width = "20px"; 30 rect_line3.height = "2px"; 31 rect_line3.color = "black"; 32 rect_line3.thickness = 4; 33 rect_line3.alpha = 0.5; 34 rect_line3.horizontalalignment=babylon.gui.control.horizontal_alignment_left; 35 rect_centor.addcontrol(rect_line3); 36 var rect_line4=new babylon.gui.rectangle(); 37 rect_line4.width = "20px"; 38 rect_line4.height = "2px"; 39 rect_line4.color = "black"; 40 rect_line4.thickness = 4; 41 rect_line4.alpha = 0.5; 42 rect_line4.horizontalalignment=babylon.gui.control.horizontal_alignment_right; 43 rect_centor.addcontrol(rect_line4); 44 return rect_centor; 45 }
b、光标锁定
babylon.js的默认相机要求用户一直按下鼠标拖拽,才能改变相机视角,显然这在fps、tps之类场景中是很不方便的,所以要使用浏览器的光标锁定功能将光标锁定在屏幕中心,并一直保持拖拽状态。
首先在相机网格初始化时直接进行光标锁定,并且设置为点击窗口则锁定光标(用于焦点离开浏览器后返回的情况)
1 //锁定光标 2 cameramesh.prototype._initpointerlock =function() { 3 var _this = this; 4 //这个监听只是用来获取焦点的?从降低耦合的角度来讲,全局事件监听并不应该放在角色类里!!!! 5 canvas.addeventlistener("click", function(evt) {//这个监听也会在点击gui按钮时触发!! 6 if(mygame.init_state==1||mygame.init_state==2)//点击canvas则锁定光标,在因为某种原因在first_lock状态脱离焦点后用来恢复焦点 7 {//不锁定指针时,这个监听什么也不做 8 if(mygame.flag_view!="first_pick") 9 {//不同浏览器中canvas锁定光标的方法不同 10 canvas.requestpointerlock = canvas.requestpointerlock || canvas.msrequestpointerlock || canvas.mozrequestpointerlock || canvas.webkitrequestpointerlock; 11 if (canvas.requestpointerlock) { 12 canvas.requestpointerlock(); 13 14 mygame.flag_view="first_lock"; 15 16 _this.centercursor.isvisible=true;//将准星设为可见 17 } 18 } 19 else//在非锁定光标时,click监听似乎不会被相机阻断 20 { 21 if(mygame.flag_view=="first_ani")//由程序控制视角的动画时间 22 { 23 cancelpropagation(evt); 24 cancelevent(evt); 25 return; 26 } 27 //var width = engine.getrenderwidth(); 28 //var height = engine.getrenderheight(); 29 var pickinfo = scene.pick(scene.pointerx, scene.pointery, null, false, mygame.cameras.camera0);//点击信息,取屏幕中心信息而不是鼠标信息!! 30 if(mygame.init_state==1&&mygame.flag_view=="first_pick" 31 &&pickinfo.hit&&pickinfo.pickedmesh.name.substr(0,5)=="card_"&&pickinfo.pickedmesh.card.belongto==mygame.whoami)//在一个卡片上按下鼠标,按下即被选中 32 { 33 cancelpropagation(evt); 34 cancelevent(evt); 35 //releasekeystate(); 36 var mesh=pickinfo.pickedmesh; 37 var card=mesh.card; 38 pickcard(card);//相机会阻断鼠标按下,但不阻断鼠标点击 39 } 40 } 41 } 42 43 }, false); 44 //一开始直接锁定光标 45 canvas.requestpointerlock = canvas.requestpointerlock || canvas.msrequestpointerlock || canvas.mozrequestpointerlock || canvas.webkitrequestpointerlock; 46 if (canvas.requestpointerlock) { 47 canvas.requestpointerlock(); 48 mygame.flag_view = "first_lock"; 49 _this.centercursor.isvisible = true; 50 mesh_arr_cards.parent=this.mesh.ballman.backview;//一开始将所有手牌背在身后 51 } 52 53 // event listener when the pointerlock is updated.当光标锁定状态发生改变时触发这一事件 54 var pointerlockchange = function (event) { 55 //if(myserver.flag_view=="first_lock") 56 //{//不锁定指针时,这个监听什么也不做 57 _this.controlenabled = (document.mozpointerlockelement === canvas || document.webkitpointerlockelement === canvas || document.mspointerlockelement === canvas || document.pointerlockelement === canvas); 58 if (!_this.controlenabled) { 59 //_this.camera.detachcontrol(canvas);//解除控制,在first_pick时还是要保持操纵性 60 } else { 61 _this.camera.attachcontrol(canvas,true);//将canvas的事件交给这个相机处理 62 } 63 //} 64 }; 65 document.addeventlistener("pointerlockchange", pointerlockchange, false); 66 document.addeventlistener("mspointerlockchange", pointerlockchange, false); 67 document.addeventlistener("mozpointerlockchange", pointerlockchange, false); 68 document.addeventlistener("webkitpointerlockchange", pointerlockchange, false); 69 }
这里将全局click监听放在了相机网格类里,事实上这个监听应该放在
control20180312.js文件中更为合理。
另外在实验中发现babylon.js的相机控制会拦截页面的“鼠标按下”事件(用来拖动视角),所以不能用鼠标按下事件来选取卡牌,所以使用click事件来选取卡牌。
另一方面在control20180312.js中设置了按下alt键切换浏览模式,浏览模式改变时光标锁定状态也要变化:
1 //执行时切换锁定状态和锁定状态的监听 2 cameramesh.prototype._changepointerlock =function() { 3 var _this = this; 4 if(mygame.flag_view=="first_lock") 5 { 6 document.exitpointerlock = document.exitpointerlock || 7 document.mozexitpointerlock || 8 document.webkitexitpointerlock; 9 10 if (document.exitpointerlock) { 11 document.exitpointerlock();//重复执行它能改变锁定状态吗?在非调试模式下不行(和焦点的变化有关?)改用专用的退出锁定方法 12 } 13 //stoplistening(canvas,"click",);//这里很难找到eventhandler 14 mygame.flag_view="first_pick"; 15 _this.camera.attachcontrol(canvas,true); 16 _this.centercursor.isvisible=false; 17 var len=mesh_arr_cards._children.length; 18 //mesh_arr_cards.parent=null; 19 20 handcard(0);//用动画方式显示手牌 21 //mesh_arr_cards.parent=this.mesh.ballman.handpoint; 22 23 } 24 else if(mygame.flag_view=="first_pick") 25 { 26 canvas.requestpointerlock = canvas.requestpointerlock || canvas.msrequestpointerlock || canvas.mozrequestpointerlock || canvas.webkitrequestpointerlock; 27 if (canvas.requestpointerlock) { 28 canvas.requestpointerlock();//但是如果这一句是在调试中运行的,就不能起作用了,因为光标在另一个页面中!! 29 } 30 mygame.flag_view="first_lock"; 31 _this.camera.attachcontrol(canvas,true); 32 _this.centercursor.isvisible=true; 33 var len=mesh_arr_cards._children.length; 34 mygame.uipanelr.button1.isvisible=false; 35 mygame.uipanelr.button2.isvisible=false; 36 mesh_arr_cards.position.y=0; 37 handcard(1); 38 //mesh_arr_cards.parent=this.mesh.ballman.backview;//把手牌隐藏起来 39 } 40 41 }
其中handcard是用动画显示手牌的方法,严格来讲这些调用也不应该放在cameramesh类的代码里。
7、control20180312.js文件
control20180312中设置了鼠标和键盘的事件响应(主要是调用其他文件里的方法)
1 //这里是处理键盘鼠标等各种操作,并进行转发的代码 2 function initmouse() 3 { 4 canvas.addeventlistener("mousedown", function(evt) {//发现只有在光标锁定的状态下,这个鼠标按下才会触发,解除光标锁定后被相机阻断了事件传播? 5 var width = engine.getrenderwidth();//这种pick专用于first_lock锁定光标模式!!!! 6 var height = engine.getrenderheight(); 7 var pickinfo = scene.pick(width/2, height/2, null, false, mygame.cameras.camera0);//点击信息,取屏幕中心信息而不是鼠标信息!! 8 9 if(mygame.init_state==1&&mygame.flag_view=="first_lock")//在用host方法移动相机时,部分禁用了原本的相机控制 10 { 11 cancelpropagation(evt);//阻止事件的传播 12 cancelevent(evt);//阻止事件的默认响应 13 } 14 15 }, false); 16 canvas.addeventlistener("mousemove", function(evt){ 17 var width = engine.getrenderwidth(); 18 var height = engine.getrenderheight(); 19 var pickinfo = scene.pick(width/2, height/2, null, false, mygame.cameras.camera0);//点击信息 20 if(mygame.flag_view=="first_ani") 21 { 22 cancelpropagation(evt); 23 cancelevent(evt); 24 return; 25 } 26 if(mygame.init_state==2&&mygame.flag_view=="first_lock")// 27 { 28 } 29 },false); 30 canvas.addeventlistener("blur",function(evt){//监听失去焦点 31 releasekeystate(); 32 }) 33 canvas.addeventlistener("focus",function(evt){//改为监听获得焦点,因为调试失去焦点时事件的先后顺序不好说 34 releasekeystate(); 35 }) 36 37 } 38 function onkeydown(event) 39 {//在播放动画时禁用所有的按键、鼠标效果 40 if(mygame.flag_view=="first_ani") 41 { 42 cancelpropagation(event); 43 cancelevent(event); 44 return; 45 } 46 if(mygame.flag_view=="first_lock"||mygame.flag_view=="first_pick")//||mygame.flag_view=="first_free") 47 { 48 49 cancelevent(event);//覆盖默认按键响应 50 51 var keycode = event.keycode; 52 var ch = string.fromcharcode(keycode);//键码转字符 53 mygame.arr_keystate[keycode]=1; 54 /*按键响应有两种,一种是按下之后立即生效的,一种是保持按下随时间积累的,第一种放在这里调度,第二种放在相应的控制类里*/ 55 if(keycode==88)//切换武器 56 { 57 58 } 59 else if(keycode==18||keycode==27)//alt切换释放锁定->改为切换view 60 { 61 mygame.player._changepointerlock(); 62 arr_pickedcards=[]; 63 card_firstpick=null; 64 65 } 66 else if(keycode>=49&&keycode<=53)//如果按下数字键1-5 67 { 68 if(mygame.flag_view=="first_pick"&&arr_pickedcards.length>0)//如果这时选择了一些手牌 69 { 70 handlegroup(keycode);//对卡牌编组 71 72 } 73 } 74 } 75 } 76 function onkeyup() 77 { 78 if(mygame.flag_view=="first_ani") 79 { 80 cancelpropagation(evt); 81 cancelevent(evt); 82 return; 83 } 84 if(mygame.flag_view=="first_lock"||mygame.flag_view=="first_pick")//||mygame.flag_view=="first_free")//光标锁定情况下的第一人称移动 85 { 86 87 cancelevent(event);//覆盖默认按键响应 88 89 var keycode = event.keycode; 90 var ch = string.fromcharcode(keycode);//键码转字符 91 mygame.arr_keystate[keycode]=0; 92 } 93 } 94 function releasekeystate()//将所有激活的按键状态置为0 95 { 96 for(key in mygame.arr_keystate) 97 { 98 mygame.arr_keystate[key]=0; 99 } 100 }
因为不锁定光标时,babylon.js相机会阻断鼠标按下事件,所以这里对鼠标按下的监听只能在first_lock浏览状态使用,目前还没有给它安排工作。需要注意的是在不锁定光标时,光标可以*移动所以使用光标在窗口中的位置生成pickinfo,而锁定光标时则直接使用窗口的中心点生成pickinfo。
键盘按键的效果分为两种,一是按下则立即生效,比如按下空格角色立即跳起,一种则是按住时一直生效,比如按住空格角色持续向上飞行,前者直接调用相应的方法,后者则是改变按键状态数组的内容,然后由moves.js里的代码对按键状态数组进行检查,以此计算相应的运动效果。
这段代码还监听了鼠标离开和移入浏览器的事件,这时所有按键的状态将被归零。
8、fullui.js文件
在这个文件中使用gui定义一些控制按钮,目前只有“向上两行”和“向下两行”两个按钮,未来应该会添加更多按钮。
1 //在这里详细设定全屏等级的ui效果 2 function makefullui() 3 { 4 var advancedtexture = mygame.fsui; 5 var uipanel = new babylon.gui.stackpanel(); 6 uipanel.width = "220px"; 7 uipanel.fontsize = "14px"; 8 uipanel.horizontalalignment = babylon.gui.control.horizontal_alignment_right; 9 uipanel.verticalalignment = babylon.gui.control.vertical_alignment_center; 10 uipanel.color = "white"; 11 advancedtexture.addcontrol(uipanel); 12 // .. 13 var button1 = babylon.gui.button.createsimplebutton("button1", "向上两行"); 14 button1.paddingtop = "10px"; 15 button1.width = "100px"; 16 button1.height = "50px"; 17 button1.background = "green"; 18 button1.isvisible=false; 19 button1.onpointerdownobservable.add(function(state,info,coordinates) { 20 if(mygame.init_state==1)//如果完成了场景的初始化 21 { 22 scrollupordown(0,1.8,2);//上下滚动 23 } 24 }); 25 uipanel.addcontrol(button1); 26 uipanel.button1=button1; 27 var button2 = babylon.gui.button.createsimplebutton("button2", "向下两行"); 28 button2.paddingtop = "10px"; 29 button2.width = "100px"; 30 button2.height = "50px"; 31 button2.background = "green"; 32 button2.isvisible=false; 33 button2.onpointerdownobservable.add(function(state,info,coordinates) { 34 if(mygame.init_state==1)//如果完成了场景的初始化 35 { 36 scrollupordown(1,1.8,2); 37 } 38 }); 39 uipanel.addcontrol(button2); 40 uipanel.button2=button2; 41 mygame.uipanelr=uipanel; 42 }
需要注意的是babylon.js并不支持父子元素之间isvisible属性的传递,虽然button1和button2都在uipanel内部,但改变uipanel的可见性并不会影响两个按钮。
9、moves.js文件
a、计算运动效果
1 function host20171018(obj)//这里的obj是一个cameramesh对象 2 { 3 //mygame.player.flag_objfast=math.max(1,math.abs(mygame.cameras.camera0.position.y/5)); 4 var arr_state=mygame.arr_keystate;//键盘状态数组 5 var rad_y=parsefloat(obj.mesh.rotation.y); 6 var v_obj={x:0,z:0,y:0};//物理模型在自身坐标系中的线速度 7 //var num_tempx= 0,num_tempz= 0,num_tempy=0;//认为这个是各个方向的分量 8 if((arr_state[68]-arr_state[65])==0)//同时按下了左右键,或者什么也没按 9 { 10 11 } 12 else if(arr_state[65]==1)//向左 13 { 14 v_obj.x=-obj.vd.left;//采用obj的在这个方向上设定的速度,obj.vd.left是一个标量 15 } 16 else if(arr_state[68]==1) 17 { 18 v_obj.x=obj.vd.right; 19 } 20 if((arr_state[87]-arr_state[83])==0)//同时按下了前后键,或者什么也没按 21 { 22 23 } 24 else if(arr_state[87]==1)//向前 25 { 26 v_obj.z=obj.vd.forward; 27 } 28 else if(arr_state[83]==1) 29 { 30 v_obj.z=-obj.vd.backwards; 31 } 32 if((arr_state[32]-arr_state[16])==0)//同时按下了上下键,或者什么也没按 33 { 34 35 } 36 else if(arr_state[32]==1)//空格 37 { 38 v_obj.y=obj.vd.up; 39 } 40 else if(arr_state[16]==1)//shift 41 { 42 v_obj.y=-obj.vd.down; 43 } 44 //var v_obj0=v_obj.clone(); 45 var v_x=math.sin(rad_y)*v_obj.z+math.cos(rad_y)*v_obj.x;//使用高中数学知识进行计算 46 var v_z=math.cos(rad_y)*v_obj.z-math.sin(rad_y)*v_obj.x; 47 var num_temp=mygame.deltatime*obj.flag_objfast;//两帧之间的时间量乘以速度系数 48 var v_add=new babylon.vector3(v_x*num_temp,v_obj.y*num_temp,v_z*num_temp);//这一帧内的位移 49 //console.log(v_add); 50 obj.mesh.position.addinplace(v_add);//修改对象位置 51 52 }
这里是一个简单的按住方向键则不断向某一方向匀速运动的算法,相机的俯仰姿态并不影响运动效果,相机左右旋转对运动效果的影响使用三角函数计算,具体计算过程不再赘述。在以前的文章里还有一些其他的运动计算方法,比如带有加速度的计算、寻路计算、带有物理引擎效果的计算,如果感兴趣可以自己查看。
b、统一cameramesh相关的对象的姿态:
1 function camerasfollowactor(object) 2 { 3 if(object.prototype=cameramesh) 4 { 5 var camera0=mygame.cameras.camera0; 6 if(mygame.flag_view=="first_lock"||mygame.flag_view=="first_ani")//动画时相机也要跟随 7 { 8 object.mesh.rotation.y = 0+camera0.rotation.y;//cameramesh的姿态由相机的姿态决定,因为视角调整方法不好编,所以借用babylon.js的相机控制方法 9 object.mesh.rotation.x=0+camera0.rotation.x;//而相机的位置则由cameramesh的位置决定 10 camera0.position=object.mesh.position.clone()//这里的player没有父元素所以_absoluteposition和position相等 11 object.mesh.ballman.head.position=object.mesh.position.clone();//我没有设置head是ballman的子元素,所以位置和姿态要手动修改 12 object.mesh.ballman.head.rotation=object.mesh.rotation.clone();//因为要保留添加物理外壳的可能性 13 14 if(mygame.init_state==2&&mygame.flag_view=="first_lock") 15 {//在鼠标不动时实现mousemove的功能 16 var width = engine.getrenderwidth(); 17 var height = engine.getrenderheight(); 18 var pickinfo = scene.pick(width/2, height/2, null, false, mygame.cameras.camera0); 19 20 } 21 } 22 else if(mygame.flag_view=="third") 23 { 24 25 } 26 else if(mygame.flag_view=="free") 27 { 28 29 } 30 } 31 }
为了保留对ballman使用物理引擎的可能,object.mesh.ballman.head并不是object.mesh的子元素,所以还要手动设置object.mesh.ballman.head的位置和姿态。
在十四行进行了一次鼠标拾取计算,这在鼠标不动但场景内物体移动,导致准心所指对象发生变化时起到作用。
三、卡牌设计
1、卡牌类cardmesh
cardmesh类是object类的子类,代码位于character.js文件中。
a、卡牌对象的实例化,以下的代码实例化了75个卡牌对象
1 function drawcard4() 2 { 3 for(var i=0;i<75;i++) 4 { 5 var card_test=new cardmesh(); 6 var obj_p={name:"cardname"+count_cardname,point_x:point_x,point_y:point_y 7 ,card:arr_carddata["test"]//从卡牌数据列表里提取名为“test”的卡牌信息 8 ,linecolor:new babylon.color3(0, 1, 0) //边线颜色 9 ,scene:scene 10 ,position:new babylon.vector3(0,0,0) 11 ,rotation:new babylon.vector3(0,0,0) 12 ,scaling:new babylon.vector3(0.1,0.1,0.1) 13 ,belongto:mygame.whoami//属于哪个玩家 14 }; 15 card_test.init(obj_p,scene); 16 card_test.mesh.parent=mesh_arr_cards; 17 count_cardname++;//命名计数器自增 18 } 19 }
其中arr_carddata是一个存储卡牌种类的数组,位于tab_carddata.js文件中,其格式如下:
1 //卡牌数据 2 arr_carddata={ 3 test:{ 4 imageb:"flower"//卡背种类 5 ,imagemain:"../assets/image/play.png"//正面的主要图片 6 ,background:"cu"//卡片正面的背景边框种类 7 ,attack:3,hp:4,cost:2,range:3,speed:5
//下面是卡片的主要文字 8 ,str_comment:"通过canvas排布生成动态纹理,(或者加入html2canvas,将dom排版转为dataurl?)" 9 ,str_title:"测试卡片"//卡片上显示的卡片名称 10 } 11 }
其中卡背种类和卡片边框种类的信息保存在tab_somedata.js文件中:
1 //存放一些通用的数据 2 var arr_icontypes={test1:"../assets/image/cursor/cursor1.png"//显示在卡片上的一些小图标,表示特殊的状态 3 ,test2:"../assets/image/cursor/cursor2.png" 4 ,test3:"../assets/image/cursor/cursor3.png"} 5 6 var arr_fronttypes={cu:"../assets/image/fronttype/cu.png"//要把这里的图片纹理设计成只实例化一次 7 ,ag:"../assets/image/fronttype/ag.png"//正面边框种类,分别是铜、银、金 8 ,au:"../assets/image/fronttype/au.png" 9 ,pt:"" 10 } 11 var arr_backtypes={flower:"../assets/image/flower.png"//卡背图片 12 13 }
初始化时设置的属性的部分代码如下:
1 cardmesh=function() 2 { 3 newland.object.call(this); 4 } 5 cardmesh.prototype=new newland.object(); 6 cardmesh.prototype.init=function(param,scene) 7 { 8 //param = param || {}; 9 if(!param||!param.card)//如果输入的卡牌参数有误 10 { 11 alert("卡牌初始化失败"); 12 return; 13 } 14 this.name = param.name;//名称 15 this.point_x = param.point_x;//x方向有几个点 16 this.point_y = param.point_y;//y方向有几个点 17 this.imagemain=param.card.imagemain; 18 this.background=param.card.background; 19 this.attack=param.card.attack; 20 this.hp=param.card.hp; 21 this.cost=param.card.cost; 22 this.str_comment=param.card.str_comment; 23 this.str_title=param.card.str_title; 24 this.range=param.card.range; 25 this.speed=param.card.speed; 26 //this.imagef = this.make_imagef();//正面纹理图片使用canvas生成——》还是用多层图片吧 27 this.imageb = param.card.imageb;//背面纹理图片 28 this.linecolor = param.linecolor;//未选中时显示边线,选中时用发光边线 29 this.scene = param.scene; 30 this.belongto=param.belongto;//表明该卡牌现在由哪个玩家掌控 31 this.ispicked=false;//这个卡片是否被选中 32 this.num_group=999;//这个卡片的编队数字,编队越靠前显示越靠前,999表示最大,意为没有编队,显示在列表的最后面 33 this.pickindex=0;//在被选中卡片数组中的索引,需要不断刷新?
b、卡牌的网格
为了赋予卡牌扭曲形变的