这篇文章讨论如何在基于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//建立五个标示组号的标记网格,标记从一(而不是零)开始 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
参与评论
手机查看
返回顶部