在WebGL场景中管理多个卡牌对象的实验

时间:2021-10-05 16:54:23

  这篇文章讨论如何在基于Babylon.js的WebGL场景中,实现多个简单卡牌类对象的显示、选择、分组、排序,同时建立一套实用的3D场景代码框架。由于作者美工能力有限,所以示例场景视觉效果可能欠佳,本文的重点在于对相关技术的探讨。

  因为文章比较长,读者可以考虑将网页导出为mhtml格式,使用Word浏览。Chrome浏览器导出mhtml文件的方法见末尾。

一、显示效果:

1、访问https://ljzc002.github.io/CardSimulate/HTML/TEST2.html查看“卡牌模拟页面”:

在WebGL场景中管理多个卡牌对象的实验

  场景中间是三个作为参照物的小球,视口平面的中间是一个用Babylon.js GUI制作的准星,默认鼠标与准星锁定在一起,直接移动鼠标即可改变相机视角,使用WASD Shift 空格键可以控制相机前、左、后、右、下、上运动(可能将Ctrl键设为向下更符合传统,但是没有找到禁用浏览器Ctrl+s快捷键的方法,只好用Shift代替)。因为光标被锁定,将这种浏览状态命名为“first_lock”。

2、按下Alt键,75张卡片通过动画移入相机视野,同时相机的位置被固定(但仍可以通过拖动鼠标改变视角):

在WebGL场景中管理多个卡牌对象的实验

  点击右侧的“向上两行”和“向下两行”按钮可以上下滚动卡片,再次按下Alt键将隐藏卡片,同时恢复相机的移动和光标的锁定。因为这种浏览状态主要用来点选场景中的物体,将它命名为“first_pick”。

3、鼠标左键单击一张卡片,卡片将处于“选中状态”(绿色边缘),再次左键单击处于选中状态的卡片,卡片将被放大拉近显示,再左键单击将恢复原位:

在WebGL场景中管理多个卡牌对象的实验

  执行动画时会禁用用户的控制,完全由动画控制视角,所以将这种浏览状态命名为“first_ani”。

4、模仿Windows的文件多选编写了卡片多选功能,按下Ctrl时可以点选多个卡片,按下Shift时可以选取首尾之间的所有卡片:

在WebGL场景中管理多个卡牌对象的实验

5、选中若干张卡片后,按1-5键可以将被选中的卡片编为1-5队,被编队的卡片将按编队顺序显示在最高处,同时编队的前面会显示队号标记:

在WebGL场景中管理多个卡牌对象的实验

6、在first_pick状态可以使用上下左右方向键进行场景漫游,可以看到场景中的所有对象:

在WebGL场景中管理多个卡牌对象的实验

二、代码实现:

1、文件结构:

CardSimulate工程的文件结构如下图所示:

在WebGL场景中管理多个卡牌对象的实验

其中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开始执行,其中一部分和前面几篇文章用到的相似:

 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>第二个场景测试,手牌的显示、排列、分组排序,显示瓷砖地面</title>
<link href="../CSS/newland.css" rel="stylesheet">
<link href="../CSS/stat.css" rel="stylesheet">
<script src="../JS/LIB/babylon.32.all.maxs.js"></script>
<script src="../JS/LIB/stat.js"></script>
<script src="../JS/MYLIB/Events.js"></script>
<script src="../JS/MYLIB/FileText.js"></script>
<script src="../JS/MYLIB/newland.js"></script>
<script src="../JS/MYLIB/View.js"></script>
<script src="../JS/PAGE/Game.js"></script>
<script src="../JS/PAGE/Character.js"></script>
<script src="../JS/PAGE/Control20180312.js"></script>
<script src="../JS/PAGE/Moves.js"></script>
<script src="../JS/PAGE/DrawCard.js"></script>
<script src="../JS/PAGE/tab_carddata.js"></script>
<script src="../JS/PAGE/tab_somedata.js"></script>
<script src="../JS/PAGE/HandleCard2.js"></script>
<script src="../JS/PAGE/FullUI.js"></script>
</head>
<body>
<div id="div_allbase">
<canvas id="renderCanvas"></canvas>
<div id="fps" style="z-index: 301;"></div>
</div>
</body>
<script>
var VERSION=1.0,AUTHOR="lz_newland@163.com";
var machine,canvas,engine,scene,gl,MyGame={};
canvas = document.getElementById("renderCanvas");
engine = new BABYLON.Engine(canvas, true);
engine.displayLoadingUI();
gl=engine._gl;//决定在这里结合使用原生OpenGL和Babylon.js;
scene = new BABYLON.Scene(engine);
var divFps = document.getElementById("fps"); var MyGame={};
window.onload=beforewebGL;
function beforewebGL()
{
if(engine._webGLVersion==2.0)//输出ES版本
{
console.log("ES3.0");
}
else{
console.log("ES2.0");
}
MyGame=new Game(0,"first_pick","","http://127.0.0.1:8082/");//建立MyGame对象用来进行全局调度
/*0-startWebGL
* */
webGLStart();
}
。。。
</script>
</html>

  但与前面的简单场景将主要代码都写在webGLStart方法中不同,对于较为复杂的流程最好将流程的每个阶段写在单独的方法里,对于较多的对象则最好提取对象的共同点作为一个“类”,将每个对象作为类的实例。这样可以将程序的复杂度分解,每次只关注其中的一小部分,降低编程难度。(设计模式的本质是对变量名进行管理,理论上讲,如果编程者的记忆力足够强、编程者之间的沟通效率足够高,则这些所谓的“设计模式”都可以省略)

  B、在webGLStart方法中对场景初始化流程进行了划分,各个流程如注释所示:

 //对象框架架构
function webGLStart()
{
//initWebSocket();//如何确保上一环结成功才开启下一环节?
initScene();//初始化场景,包括最初入门教程里的那些东西
initArena();//初始化地形,包括天空盒,参照物等
initEvent();//初始化事件
initUI();//初始化场景UI
initObj();//初始化一开始存在的可交互的物体
initLoop();//初始化渲染循环
MyGame.init_state=1;//更新初始化状态
engine.hideLoadingUI();//隐藏载入UI
//MyGame.flag_startr=1;//这个是通过nohurry计时器自动启动的,不需要手动启动
}

  C、初始化场景

 function initScene()
{//光照
var light0 = new BABYLON.HemisphericLight("light0", new BABYLON.Vector3(0, 1, 0), scene);
light0.diffuse = new BABYLON.Color3(1,1,1);//这道“颜色”是从上向下的,底部收到100%,侧方收到50%,顶部没有
light0.specular = new BABYLON.Color3(0,0,0);
light0.groundColor = new BABYLON.Color3(1,1,1);//这个与第一道正相反
MyGame.lights.light0=light0;//将光照变量交给MyGame对象管理
mesh_arr_cards=new BABYLON.Mesh("mesh_arr_cards", scene);
//相机对象
var camera0= new BABYLON.FreeCamera("FreeCamera", new BABYLON.Vector3(0, 0, 0), scene);
//camera0.layerMask = 2;
//camera0.position=new BABYLON.Vector3(0, 0, -20);
camera0.minZ=0.001;
scene.activeCameras.push(camera0); //用BallMan作为CameraMesh的mesh
var player = new BallMan();
var obj_p={};//初始化参数
//计划不使用物理引擎
var mesh_ballman=new BABYLON.Mesh("mesh_ballman",scene);
obj_p.mesh=mesh_ballman;
obj_p.name="本机";//显示的名字
obj_p.id="本机";//WebSocket Sessionid
obj_p.image="../ASSETS/IMAGE/Rainbow.jpg";
player.init(
obj_p,scene
); var cameramesh=new CameraMesh();
var obj_p={};//初始化参数
obj_p.mesh=mesh_ballman;
obj_p.mesh.isVisible=false;
obj_p.mesh.position=new BABYLON.Vector3(0,0,-20);
if(obj_p.mesh.ballman)
{
obj_p.mesh.ballman.head.position=obj_p.mesh.position.clone();
}
obj_p.methodofmove="host20171018";
obj_p.name="FreeCamera";//显示的名字
obj_p.id="FreeCamera";//WebSocket Sessionid
obj_p.camera=camera0;
//obj_p.image="assets/image/play.png";
obj_p.flag_objfast=5;
cameramesh.init(
obj_p,scene
);
MyGame.arr_myplayers[obj_p.name]=cameramesh;
MyGame.player=cameramesh;
MyGame.Cameras.camera0=camera0;
camera0.position=cameramesh.mesh.position.clone();
cameramesh.mesh.rotation=camera0.rotation.clone();
mesh_arr_cards.position=MyGame.player.mesh.ballman.backview._absolutePosition.clone();
}

  其中mesh_arr_cards是所有手牌的父网格,用来对手牌进行定位,事实上这个对象放在initArena或者initObj阶段更加合理,但是因为相机对象的一些事件和这个网格有关,只好放在场景初始化阶段。BallMan的外观是一个球体网格,用来代表场景中的玩家,其用法可以参考https://www.cnblogs.com/ljzc002/p/7274455.html;CameraMesh是一个网格与相机的结合体,在第三人称时用户将能看见自己操纵的单位(关于BallMan和CameraMesh类的参数将在后面详细介绍)。最后把各种对象都交给MyGame统一管理。

  D、初始化环境

 function initArena()
{
var mesh_base=new BABYLON.MeshBuilder.CreateSphere("mesh_base",{diameter:1},scene);
mesh_base.material=MyGame.materials.mat_green;
mesh_base.position.x=0;
mesh_base.renderingGroupId=2;
//mesh_base.layerMask=2;
var mesh_base1=new BABYLON.MeshBuilder.CreateSphere("mesh_base1",{diameter:1},scene);
mesh_base1.position.y=10;
mesh_base1.position.x=0;
mesh_base1.material=MyGame.materials.mat_green;
mesh_base1.renderingGroupId=2;
//mesh_base1.layerMask=2;
var mesh_base2=new BABYLON.MeshBuilder.CreateSphere("mesh_base2",{diameter:1},scene);
mesh_base2.position.y=-10;
mesh_base2.position.x=0;
mesh_base2.material=MyGame.materials.mat_green;
mesh_base2.renderingGroupId=2;
//mesh_base2.layerMask=2;
for(var i=0;i<5;i++)//建立五个标示组号的标记网格,标记从一(而不是零)开始
{
var plane=new BABYLON.MeshBuilder.CreatePlane("mesh_groupicon"+(i+1),{size:5},scene);
var mat_plane = new BABYLON.StandardMaterial("mat_plane"+(i+1), scene);
var texture_plane= new BABYLON.DynamicTexture("texture_plane"+(i+1), {width:100, height:100}, scene);
mat_plane.diffuseTexture =texture_plane;
plane.material=mat_plane;
var font = "bold 60px monospace";
texture_plane.drawText((i+1), 40, 70, font, "white", "green", true, true);//第一个是文字颜色,第二个则是完全填充的背景色
plane.position.x=-16;
plane.position.z=-2;
plane.renderingGroupId=2;
//plane.rotation.x=-Math.PI/2;//这会导致*相机的视角发生bug??Y与Z轴混淆?
plane.isPickable=false;
plane.isVisible=false;
arr_mesh_groupicon.push(plane);
//plane.parent=mesh_arr_cards;
}
}

  建立了三个小绿球作为场景的参照物,建立了五个小平面作为分组标记,这五个标记暂时不可见(在调试分组标记的过程中Babylon.js发生了bug,相机输入的Y轴和Z轴发生混淆,但没有深入分析原因)。

  E、初始化事件

 function initEvent()
{
InitMouse();
window.addEventListener("keydown", onKeyDown, false);//按键按下
window.addEventListener("keyup", onKeyUp, false);//按键抬起
window.addEventListener("resize", function () {
if (engine) {
engine.resize();
}
},false);
}

  InitMouse中是对鼠标的四种事件监听,具体代码在Control20180312.js文件中,接下来监听了按键按下、按键抬起、窗口尺寸变化。

  F、初始化UI

 function initUI()
{
MakeFullUI();
//var advancedTexture = MyGame.fsUI; }

  代码主体在FullUI.js文件中

  G、初始化对象

 function initObj()
{//添加75个(?)实验对象 DrawCard4();
SortCard();
}

  具体代码在DrawCard.js中

  H、初始化渲染循环(也是逻辑循环)

 function initLoop()
{
var _this=MyGame;
scene.registerBeforeRender(function() { //比runRenderLoop更早
});
scene.registerAfterRender(
function() {
if(MyGame.flag_startr==1)//如果开始渲染了
{//如果正在使用相机网格进行漫游
if(MyGame.player.prototype=CameraMesh&&MyGame.flag_view=="first_lock")
{
host20171018(MyGame.player);
}
}
}
); engine.runRenderLoop(function () //场景逻辑和AI也从这里引入
{
if (divFps) {
// Fps
divFps.innerHTML = engine.getFps().toFixed() + " fps";
}
MyGame.HandleNoHurry();//这里包含了运动使用的计时器
if(_this.flag_startr==1||_this.flag_view!="first_pick")
{
//主相机和小地图相机都随着玩家的位置变化
CamerasFollowActor(_this.player);
} _this.scene.render(); });
}

  其中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、初始化方法:

 Game=function(init_state,flag_view,wsUri,h2Uri)
{//参数:初始化时的状态代号,初始化时的浏览模式,webSocket的服务器地址,h2数据库地址
var _this = this;
this.scene=scene;
this.loader = new BABYLON.AssetsManager(scene);//资源管理器,用于预先加载资源
//控制者数组
this.arr_myplayers={};
this.arr_npcs={};//NPC数组
this.count={};//综合计数器对象
this.count.count_name_npcs=0;//NPC命名计数器,每产生一个NPC则加一,避免NPCID重复
this.Cameras={};//scene里也有?,综合相机对象
this.websocket;
this.lights={};//综合光源对象
this.fsUI=BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("ui1");//全屏GUI对象
this.hl=new BABYLON.HighlightLayer("hl1", scene);//高光层对象,下面是高光层的一些参数
this.hl.blurVerticalSize = 1.0;//这个影响的并不是高光的粗细程度,而是将它分成 多条以产生模糊效果,数值表示多条间的间隙尺寸
this.hl.blurHorizontalSize =1.0;
this.hl.innerGlow = false;//取消内部光晕
this.hl.alphaBlendingMode=3;
//this.hl.isStroke=true;
//this.hl.blurTextureSizeRatio=2;
//this.hl.mainTextureFixedSize=100;
//this.hl.renderingGroupId=3;
//this.hl._options.mainTextureRatio=1000; this.wsUri=wsUri;
this.init_state=init_state;//当前运行状态
/*0-startWebGL
1-WebGLStarted
2-PlanetDrawed
* */
this.h2Uri=h2Uri;
//我是谁
this.WhoAmI=newland.randomString(8); this.materials={};//综合材质对象,下面初始化了几种常用的材质
var mat_frame = new BABYLON.StandardMaterial("mat_frame", scene);
mat_frame.wireframe = true;
this.materials.mat_frame=mat_frame;
var mat_red=new BABYLON.StandardMaterial("mat_red", scene);
mat_red.diffuseColor = new BABYLON.Color3(1, 0, 0);
var mat_green=new BABYLON.StandardMaterial("mat_green", scene);
mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0);
var mat_blue=new BABYLON.StandardMaterial("mat_blue", scene);
mat_blue.diffuseColor = new BABYLON.Color3(0, 0, 1);
this.materials.mat_red=mat_red;
this.materials.mat_green=mat_green;
this.materials.mat_blue=mat_blue; this.models={};//综合模型对象
this.textures={};//综合纹理对象
this.texts={};//综合文本对象 this.flag_startr=0;//开始渲染并且地形初始化完毕
this.flag_starta=0;//开始执行NPC的AI逻辑
this.list_nohurry=[];//需要周期性进行的工作
this.nohurry=0;//一个计时器,让一些计算不要太频繁
this.flag_online=false;//是否是在线场景
this.flag_view=flag_view;//first/third/input/free
this.flag_controlEnabled = false;
this.arr_keystate=[];//按键状态数组
}

  这段代码中初始化了一些场景中可能会用到的变量,最整洁的情况是把所有的全局变量都作为MyGame的属性加以管理,但很难做到。

  B、原型方法:

    每个Game类的实例都会继承这些方法:

 Game.prototype={
AddNohurry:function(name,delay,lastt,todo,count)
{//名字,每次执行之间的间隔(最小间隔),上一次执行时间,要执行的函数名,已经执行的次数
if(this.list_nohurry[name])//如果已经有叫做这个名字的任务
{
return;
}
this.list_nohurry[name]={delay:delay,lastt:lastt,todo:todo
,count:count};
},
RemoveNohurry:function(name)
{
delete this.list_nohurry[name];
},
HandleNoHurry:function()
{
var _this=this;
if( _this.flag_startr==0)//开始渲染并且地形初始化完毕!!
{
engine.hideLoadingUI();//隐藏载入UI
_this.flag_startr=1;//标志开始渲染
_this.lastframet=new Date().getTime();
_this.firstframet=_this.lastframet;
_this.DeltaTime=0;
}
else
{//如果已经开始渲染
_this.currentframet=new Date().getTime();//当前帧的时间
_this.DeltaTime=_this.currentframet-_this.lastframet;//取得两帧之间的时间
_this.lastframet=_this.currentframet;
/*_this.nohurry+=_this.DeltaTime;//这个代码用于只执行一个定时任务的情况 if(MyGame&&_this.nohurry>1000)//每一秒进行一次
{
_this.nohurry=0; }*/
//var time_start=_this.currentframet-_this.firstframet;//当前时间到最初过了多久
for(var i=0;i<_this.list_nohurry.length;i++)//对于每一个定时任务
{
var obj_nohurry=_this.list_nohurry[i];
if(obj_nohurry.lastt==0)//如果上次执行时间是0,则以当前时间作为上次执行时间
{
obj_nohurry.lastt=new Date().getTime();
}
else
{
var time_start=_this.currentframet-obj_nohurry.lastt;//当前帧到上次执行经过的时间
if(time_start>obj_nohurry.delay)//如果经过的时间超过了每次执行周期乘以执行次数加一,则执行一次
{
obj_nohurry.todo();
obj_nohurry.count++;
obj_nohurry.lastt=_this.currentframet;
break;//每一帧最多只做一个费时任务,周期更短的任务放在list_nohurry队列前面,获得更多执行机会
}
} }
if(_this.flag_starta==1)//除非开始进行ai计算,否则只处理和基本ui有关的内容
{ }
}
}
}

  这里的三个方法都是和定时任务有关的,将需要执行的定时任务放在list_nohurry中,在引擎每一次渲染循环时检测是否需要执行队列中的任务,因为要尽量减少每一帧的时间差异,规定每一帧最多只执行一个任务,到时但未执行的任务需要延后到下一帧判断是否执行,队列中越靠前的任务被及时执行的可能性越高。

  上述方法并没有实际使用过,一个类似的执行定时任务的例子可以在https://www.cnblogs.com/ljzc002/p/7373046.html查看。

4、object类:

  object类是场景中所有受控物体的基类,包含运动控制和姿态控制所需的一些信息,其代码位于newland.js文件中。

  A、初始化代码:

 newland.object=function()
{ }
newland.object.prototype.init = function(param)
{
//启用物理引擎后这一部分可能用不上,但暂时保留
this.keys={w:0,s:0,a:0,d:0,space:0,ctrl:0,shift:0};//按键是否保持按下,已经改为由MyGame管理
this.witha0={forward:0,right:0,up:-9.82};//非键盘控制产生的加速度
this.witha={forward:0,right:0,up:-9.82};//环境加速度,包括地面阻力和重力,现在还没有风力
this.witha2={forward:0,right:0,up:0};//键盘控制加速度与物体本身加速度和非键盘控制产生的加速度合并后的最终加速度
this.v0={forward:0,right:0,up:0};//上一时刻的速度
this.vt={forward:0,right:0,up:0};//下一时刻的速度
this.vm={forward:15,backwards:5,left:5,right:5,up:100,down:100};//各个方向的最大速度
this.fm={forward:2,backwards:1,left:1,right:1,up:10,down:10};//各个方向的最大发力
this.ff=0.05;//在地面不做任何发力时的阻力效果
//this.flag_runfast=1;//速度系数
this.ry0=0;//上一时刻的y轴转角
this.ryt=0;//下一时刻的y轴转角
this.rychange=0;//y轴转角差
this.mchange={forward:0,right:0,up:0};//物体自身坐标系上的位移
this.vmove=new BABYLON.Vector3(0,0,0);//世界坐标系中每一时刻的位移和量
this.py0=0;//记录上一时刻的y轴位置,和下一时刻比较确定物体有没有继续向下运动!!,用于判断物体是否接触地面 param = param || {};
this.mesh=param.mesh;
this.meshname=this.mesh.name;
this.skeletonsPlayer=param.skeletonsPlayer||[];//如果和某个Babylon.js模型关联,则提取模型的骨骼动画
this.submeshs=param.submeshs;//提取子网格
this.ry0=param.mesh.rotation.y;
this.py0=param.mesh.position.y;
this.flag_runfast=param.flag_runfast ||1;//速度系数,最终位移要乘以速度系数
this.standonTheGround=0;//一开始在空中,落到地上,是否接触地面
//this.flag_objfast=param.flag_objfast ||1;
this.countstop=0;//记录物体静止了几次,如果物体一直静止就停止发送运动信息,在联网情况下减少数据传输 this.PlayAnnimation = false;//是否在执行动画
this.methodofmove=param.methodofmove||"";//运动算法
this.path_goto="sleep";//这个物体接到指令要去哪里,是一个向量数组(路径),在寻路算法中使用 //window.addEventListener("keydown", onKeyDown, false);//按键按下
//window.addEventListener("keyup", onKeyUp, false);//按键抬起
}

  这里将一些物体可能用到的变量保存在基类中,减化了子类物体的创建代码。

  B、其他原型方法

 //骨骼动画
newland.object.prototype.beginSP=function(num_type)//执行骨骼动画列表里的某一个骨骼动画
{
if(this.skeletonsPlayer.length>0)
{
this.sp = this.skeletonsPlayer[num_type]; this.totalFrame = this.skeletonsPlayer[0]._scene._activeSkeletons.data.length;//总帧数
this.start = 0;
this.end = 100;
this.VitesseAnim = parseFloat(100 / 100);//动画的速度比
scene.beginAnimation(this.sp, (100 * this.start) / this.totalFrame, (100 * this.end) / this.totalFrame, true, this.VitesseAnim);//启动动画,skeletonsPlayer是一个骨骼动画对象
this.PlayAnnimation = true;
}
else
{//本体不能启动骨骼动画,则直接启动其子元素的骨骼动画
var len=this.submeshs.length;
for(var i=0;i<len;i++)
{
var skeleton=this.submeshs[i].skeleton;
var totalFrame = skeleton._scene._activeSkeletons.data.length;//总帧数
var start = 0;
var end = 100;
var VitesseAnim = parseFloat(100 / 100);//动画的速度比
scene.beginAnimation(skeleton, (100 * start) / totalFrame, (100 * end) / totalFrame, true, VitesseAnim);
}
this.PlayAnnimation = true;
}
}
newland.object.prototype.stopSP=function(num_type)
{
this.PlayAnnimation = false;
if(this.skeletonsPlayer.length>0)
{
scene.stopAnimation(this.skeletonsPlayer[0]);
}
else
{
var len=this.submeshs.length;
for(var i=0;i<len;i++)
{
var skeleton=this.submeshs[i].skeleton;
scene.stopAnimation(skeleton);
}
}
}

  object类具有两个和骨骼动画相关的原型方法,用来控制骨骼动画的启停(方法编程时间较早,在新版Babylon.js中也许会有错误)

5、BallMan类:

  BallMan类是object类的一个子类,主要用来在不载入模型的情况下,用简单的球体网格进行物体移动、视角变化、对象拾取等试验。代码位于Character.js文件中。

  A、初始化代码:

 BallMan=function()//只用来显示其他玩家?-》自己也要显示
{
newland.object.call(this);//调用父类的构造方法
}
BallMan.prototype=new newland.object();//继承父类的属性
BallMan.prototype.init=function(param,scene)
{
param = param || {};
newland.object.prototype.init.call(this,param);//调用父类的初始化方法
this.name=param.name;
this.id=param.id;
//this.vd={forward:10.0,backwards:10.0,left:10.0,right:10.0,up:10.0,down:10.0};//简单运动时各个方向的默认速度
//this.flag_objfast=param.flag_objfast ||1;//使用这种机体移动物体的默认速度 var mat_head=new BABYLON.StandardMaterial("mat_head", scene);//球体(头部)的材质
mat_head.diffuseTexture =new BABYLON.Texture(param.image,scene);//将球体材质的漫反射纹理设置为一张图片
mat_head.freeze();//冻结材质,减少向显卡传递数据,据说能提升性能
var mesh_head=BABYLON.Mesh.CreateSphere(this.name+"head", 10, 2.0, scene);//建立一个球体
mesh_head.renderingGroupId=2;//渲染组设为2,这里规定隐形物体渲染组为0,远处的背景物体渲染组为1,普通物体行为2,特别强调的物体为3
mesh_head.layerMask=2;
//mesh_head.rotation.y=Math.PI*0.5;
mesh_head.material=mat_head;
//mesh_head.parent=this.mesh;//想让head随着ghost一起位移,又不想让它随着ghost滚动!!
//this.mesh.setPhysicsLinkWith(mesh_head,new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0,0,0));//枢轴链接
mesh_head.position=this.mesh.position.clone();//不克隆直接赋值有抖动
mesh_head.isPickable=false;//不可被选取
this.head=mesh_head;
this.mesh.ballman=this; //改用gui?显示名字
if(this.lab)
{
this.lab.dispose();
this.lab=null;
}
var label = new BABYLON.GUI.Rectangle(this.name);
label.background = "black";
label.height = "30px";
label.alpha = 0.5;
label.width = "100px";
label.cornerRadius = 20;
label.thickness = 1;
label.linkOffsetY = 30;//位置偏移量??
MyGame.fsUI.addControl(label);
label.linkWithMesh(this.head);
var text1 = new BABYLON.GUI.TextBlock();
text1.text = this.name;
text1.color = "white";
label.addControl(text1);
label.isVisible=true;
label.layerMask=2;
this.lab=label; //定位第一人称视角的位置
var headview=new BABYLON.Mesh(this.name+"headview",scene);//用网格定义一个位置,位于这个位置的物体可以是headview的子元素,这样它将随着BallMan一起移动
headview.parent=this.head;
headview.position=new BABYLON.Vector3(0,0,2.0);
this.headview=headview;
//定位第三人称视角的位置
var backview=new BABYLON.Mesh(this.name+"backview",scene);
backview.parent=this.head;
backview.position=new BABYLON.Vector3(0,2,-6);
this.backview=backview;
var backview_right=new BABYLON.Mesh(this.name+"backview_right",scene);
backview_right.parent=this.head;
backview_right.position=new BABYLON.Vector3(2.6,2,-6);
this.backview_right=backview_right;
//定位手持物体的位置
var handpoint=new BABYLON.Mesh(this.name+"handpoint",scene);
handpoint.parent=this.head;
handpoint.position=new BABYLON.Vector3(0,0,10);
this.handpoint=handpoint;
//左手和右手
var lefthand=new BABYLON.Mesh(this.name+"lefthand",scene);
lefthand.parent=this.head;
lefthand.position=new BABYLON.Vector3(-1,0.2,3.0);
lefthand.lookAt(lefthand.position.negate().add(headview.position));
this.lefthand=lefthand;
var righthand=new BABYLON.Mesh(this.name+"righthand",scene);
righthand.parent=this.head;
righthand.position=new BABYLON.Vector3(1,0.2,3.0);
righthand.lookAt(righthand.position.negate().add(headview.position));
this.righthand=righthand; //暂时不使用抬头显示器
console.log("Player初始化完毕"); }

  关于渲染组的材料可以查看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、初始化方法:

 /*20180613现在规定主相机在MyGame中对应三种状态:
first_lock表示相机和相机网格绑定在一起并使用Control控制,
first_ani表示由动画控制相机相机不可手动控制
first_pick表示相机位置不可以移动,但是可以改变视角进行点击(是在没有锁定指针属性时的替代方法??)*/
CameraMesh=function()
{
newland.object.call(this);
}
CameraMesh.prototype=new newland.object();
CameraMesh.prototype.init=function(param,scene)
{
param = param || {};
newland.object.prototype.init.call(this,param);//继承原型的方法
this.name=param.name;
this.id=param.id;
var num_v=0.001;
this.vd={forward:num_v*2,backwards:num_v,left:num_v,right:num_v,up:num_v,down:num_v};//简单运动时各个方向的默认速度,最慢的情况下每一毫秒移动多少
this.flag_objfast=param.flag_objfast ||1;//使用这种机体移动物体的默认速度
this.camera=param.camera;
this.mesh=param.mesh;//可以把这个mesh指定为BallMan!!!!
this.camera.mesh=this.mesh;
var _this = this;
//中间光标,准星
this.centercursor=this.CenterCursor();
this.centercursor.isVisible=false;
this._initPointerLock();//先不要锁定光标,等初始化地形完毕后再锁定? console.log("相机网格初始化完毕");
}
  CameraMesh.prototype.handleUserMouse=function(evt, pickInfo)
  {
  //this.weapon.fire(pickInfo);//FPS和TPS的武器射击同样由这个类负责
  }

  在25行附近使用GUI在窗口的中心绘制了一个准星:

 //准心
CameraMesh.prototype.CenterCursor=function()
{
//在屏幕中心绘制一个光标
var rect_centor=new BABYLON.GUI.Rectangle();
rect_centor.width = "80px";
rect_centor.height = "80px";
rect_centor.alpha=0.5;
rect_centor.color="blue";
MyGame.fsUI.addControl(rect_centor); var rect_line1=new BABYLON.GUI.Rectangle();//GUI不能直接绘制线段,所以用一个细长的矩形表示线段
rect_line1.width = "2px";
rect_line1.height = "20px";
rect_line1.color = "black";
rect_line1.thickness = 4;
rect_line1.alpha = 0.5;
rect_line1.verticalAlignment=BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
rect_centor.addControl(rect_line1);
var rect_line2=new BABYLON.GUI.Rectangle();
rect_line2.width = "2px";
rect_line2.height = "20px";
rect_line2.color = "black";
rect_line2.thickness = 4;
rect_line2.alpha = 0.5;
rect_line2.verticalAlignment=BABYLON.GUI.Control.VERTICAL_ALIGNMENT_BOTTOM;
rect_centor.addControl(rect_line2);
var rect_line3=new BABYLON.GUI.Rectangle();
rect_line3.width = "20px";
rect_line3.height = "2px";
rect_line3.color = "black";
rect_line3.thickness = 4;
rect_line3.alpha = 0.5;
rect_line3.horizontalAlignment=BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
rect_centor.addControl(rect_line3);
var rect_line4=new BABYLON.GUI.Rectangle();
rect_line4.width = "20px";
rect_line4.height = "2px";
rect_line4.color = "black";
rect_line4.thickness = 4;
rect_line4.alpha = 0.5;
rect_line4.horizontalAlignment=BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT;
rect_centor.addControl(rect_line4);
return rect_centor;
}

  B、光标锁定

  Babylon.js的默认相机要求用户一直按下鼠标拖拽,才能改变相机视角,显然这在FPS、TPS之类场景中是很不方便的,所以要使用浏览器的光标锁定功能将光标锁定在屏幕中心,并一直保持拖拽状态。

  首先在相机网格初始化时直接进行光标锁定,并且设置为点击窗口则锁定光标(用于焦点离开浏览器后返回的情况)

 //锁定光标
CameraMesh.prototype._initPointerLock =function() {
var _this = this;
//这个监听只是用来获取焦点的?从降低耦合的角度来讲,全局事件监听并不应该放在角色类里!!!!
canvas.addEventListener("click", function(evt) {//这个监听也会在点击GUI按钮时触发!!
if(MyGame.init_state==1||MyGame.init_state==2)//点击canvas则锁定光标,在因为某种原因在first_lock状态脱离焦点后用来恢复焦点
{//不锁定指针时,这个监听什么也不做
if(MyGame.flag_view!="first_pick")
{//不同浏览器中canvas锁定光标的方法不同
canvas.requestPointerLock = canvas.requestPointerLock || canvas.msRequestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock;
if (canvas.requestPointerLock) {
canvas.requestPointerLock(); MyGame.flag_view="first_lock"; _this.centercursor.isVisible=true;//将准星设为可见
}
}
else//在非锁定光标时,click监听似乎不会被相机阻断
{
if(MyGame.flag_view=="first_ani")//由程序控制视角的动画时间
{
cancelPropagation(evt);
cancelEvent(evt);
return;
}
//var width = engine.getRenderWidth();
//var height = engine.getRenderHeight();
var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, MyGame.Cameras.camera0);//点击信息,取屏幕中心信息而不是鼠标信息!!
if(MyGame.init_state==1&&MyGame.flag_view=="first_pick"
&&pickInfo.hit&&pickInfo.pickedMesh.name.substr(0,5)=="card_"&&pickInfo.pickedMesh.card.belongto==MyGame.WhoAmI)//在一个卡片上按下鼠标,按下即被选中
{
cancelPropagation(evt);
cancelEvent(evt);
//releaseKeyState();
var mesh=pickInfo.pickedMesh;
var card=mesh.card;
PickCard(card);//相机会阻断鼠标按下,但不阻断鼠标点击
}
}
} }, false);
//一开始直接锁定光标
canvas.requestPointerLock = canvas.requestPointerLock || canvas.msRequestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock;
if (canvas.requestPointerLock) {
canvas.requestPointerLock();
MyGame.flag_view = "first_lock";
_this.centercursor.isVisible = true;
mesh_arr_cards.parent=this.mesh.ballman.backview;//一开始将所有手牌背在身后
} // Event listener when the pointerlock is updated.当光标锁定状态发生改变时触发这一事件
var pointerlockchange = function (event) {
//if(MyServer.flag_view=="first_lock")
//{//不锁定指针时,这个监听什么也不做
_this.controlEnabled = (document.mozPointerLockElement === canvas || document.webkitPointerLockElement === canvas || document.msPointerLockElement === canvas || document.pointerLockElement === canvas);
if (!_this.controlEnabled) {
//_this.camera.detachControl(canvas);//解除控制,在first_pick时还是要保持操纵性
} else {
_this.camera.attachControl(canvas,true);//将canvas的事件交给这个相机处理
}
//}
};
document.addEventListener("pointerlockchange", pointerlockchange, false);
document.addEventListener("mspointerlockchange", pointerlockchange, false);
document.addEventListener("mozpointerlockchange", pointerlockchange, false);
document.addEventListener("webkitpointerlockchange", pointerlockchange, false);
}

  这里将全局click监听放在了相机网格类里,事实上这个监听应该放在

Control20180312.js文件中更为合理。

  另外在实验中发现Babylon.js的相机控制会拦截页面的“鼠标按下”事件(用来拖动视角),所以不能用鼠标按下事件来选取卡牌,所以使用click事件来选取卡牌。

  另一方面在Control20180312.js中设置了按下Alt键切换浏览模式,浏览模式改变时光标锁定状态也要变化:

 //执行时切换锁定状态和锁定状态的监听
CameraMesh.prototype._changePointerLock =function() {
var _this = this;
if(MyGame.flag_view=="first_lock")
{
document.exitPointerLock = document.exitPointerLock ||
document.mozExitPointerLock ||
document.webkitExitPointerLock; if (document.exitPointerLock) {
document.exitPointerLock();//重复执行它能改变锁定状态吗?在非调试模式下不行(和焦点的变化有关?)改用专用的退出锁定方法
}
//stopListening(canvas,"click",);//这里很难找到eventHandler
MyGame.flag_view="first_pick";
_this.camera.attachControl(canvas,true);
_this.centercursor.isVisible=false;
var len=mesh_arr_cards._children.length;
//mesh_arr_cards.parent=null; HandCard(0);//用动画方式显示手牌
//mesh_arr_cards.parent=this.mesh.ballman.handpoint; }
else if(MyGame.flag_view=="first_pick")
{
canvas.requestPointerLock = canvas.requestPointerLock || canvas.msRequestPointerLock || canvas.mozRequestPointerLock || canvas.webkitRequestPointerLock;
if (canvas.requestPointerLock) {
canvas.requestPointerLock();//但是如果这一句是在调试中运行的,就不能起作用了,因为光标在另一个页面中!!
}
MyGame.flag_view="first_lock";
_this.camera.attachControl(canvas,true);
_this.centercursor.isVisible=true;
var len=mesh_arr_cards._children.length;
MyGame.UiPanelr.button1.isVisible=false;
MyGame.UiPanelr.button2.isVisible=false;
mesh_arr_cards.position.y=0;
HandCard(1);
//mesh_arr_cards.parent=this.mesh.ballman.backview;//把手牌隐藏起来
} }

  其中HandCard是用动画显示手牌的方法,严格来讲这些调用也不应该放在CameraMesh类的代码里。

7、Control20180312.js文件

  Control20180312中设置了鼠标和键盘的事件响应(主要是调用其他文件里的方法)

 //这里是处理键盘鼠标等各种操作,并进行转发的代码
function InitMouse()
{
canvas.addEventListener("mousedown", function(evt) {//发现只有在光标锁定的状态下,这个鼠标按下才会触发,解除光标锁定后被相机阻断了事件传播?
var width = engine.getRenderWidth();//这种pick专用于first_lock锁定光标模式!!!!
var height = engine.getRenderHeight();
var pickInfo = scene.pick(width/2, height/2, null, false, MyGame.Cameras.camera0);//点击信息,取屏幕中心信息而不是鼠标信息!! if(MyGame.init_state==1&&MyGame.flag_view=="first_lock")//在用host方法移动相机时,部分禁用了原本的相机控制
{
cancelPropagation(evt);//阻止事件的传播
cancelEvent(evt);//阻止事件的默认响应
} }, false);
canvas.addEventListener("mousemove", function(evt){
var width = engine.getRenderWidth();
var height = engine.getRenderHeight();
var pickInfo = scene.pick(width/2, height/2, null, false, MyGame.Cameras.camera0);//点击信息
if(MyGame.flag_view=="first_ani")
{
cancelPropagation(evt);
cancelEvent(evt);
return;
}
if(MyGame.init_state==2&&MyGame.flag_view=="first_lock")//
{
}
},false);
canvas.addEventListener("blur",function(evt){//监听失去焦点
releaseKeyState();
})
canvas.addEventListener("focus",function(evt){//改为监听获得焦点,因为调试失去焦点时事件的先后顺序不好说
releaseKeyState();
}) }
function onKeyDown(event)
{//在播放动画时禁用所有的按键、鼠标效果
if(MyGame.flag_view=="first_ani")
{
cancelPropagation(event);
cancelEvent(event);
return;
}
if(MyGame.flag_view=="first_lock"||MyGame.flag_view=="first_pick")//||MyGame.flag_view=="first_free")
{ cancelEvent(event);//覆盖默认按键响应 var keyCode = event.keyCode;
var ch = String.fromCharCode(keyCode);//键码转字符
MyGame.arr_keystate[keyCode]=1;
/*按键响应有两种,一种是按下之后立即生效的,一种是保持按下随时间积累的,第一种放在这里调度,第二种放在相应的控制类里*/
if(keyCode==88)//切换武器
{ }
else if(keyCode==18||keyCode==27)//alt切换释放锁定->改为切换view
{
MyGame.player._changePointerLock();
arr_pickedCards=[];
card_firstpick=null; }
else if(keyCode>=49&&keyCode<=53)//如果按下数字键1-5
{
if(MyGame.flag_view=="first_pick"&&arr_pickedCards.length>0)//如果这时选择了一些手牌
{
HandleGroup(keyCode);//对卡牌编组 }
}
}
}
function onKeyUp()
{
if(MyGame.flag_view=="first_ani")
{
cancelPropagation(evt);
cancelEvent(evt);
return;
}
if(MyGame.flag_view=="first_lock"||MyGame.flag_view=="first_pick")//||MyGame.flag_view=="first_free")//光标锁定情况下的第一人称移动
{ cancelEvent(event);//覆盖默认按键响应 var keyCode = event.keyCode;
var ch = String.fromCharCode(keyCode);//键码转字符
MyGame.arr_keystate[keyCode]=0;
}
}
function releaseKeyState()//将所有激活的按键状态置为0
{
for(key in MyGame.arr_keystate)
{
MyGame.arr_keystate[key]=0;
}
}

  因为不锁定光标时,Babylon.js相机会阻断鼠标按下事件,所以这里对鼠标按下的监听只能在first_lock浏览状态使用,目前还没有给它安排工作。需要注意的是在不锁定光标时,光标可以*移动所以使用光标在窗口中的位置生成pickInfo,而锁定光标时则直接使用窗口的中心点生成pickInfo。

  键盘按键的效果分为两种,一是按下则立即生效,比如按下空格角色立即跳起,一种则是按住时一直生效,比如按住空格角色持续向上飞行,前者直接调用相应的方法,后者则是改变按键状态数组的内容,然后由Moves.js里的代码对按键状态数组进行检查,以此计算相应的运动效果。

  这段代码还监听了鼠标离开和移入浏览器的事件,这时所有按键的状态将被归零。

8、FullUI.js文件

  在这个文件中使用GUI定义一些控制按钮,目前只有“向上两行”和“向下两行”两个按钮,未来应该会添加更多按钮。

 //在这里详细设定全屏等级的UI效果
function MakeFullUI()
{
var advancedTexture = MyGame.fsUI;
var UiPanel = new BABYLON.GUI.StackPanel();
UiPanel.width = "220px";
UiPanel.fontSize = "14px";
UiPanel.horizontalAlignment = BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_RIGHT;
UiPanel.verticalAlignment = BABYLON.GUI.Control.VERTICAL_ALIGNMENT_CENTER;
UiPanel.color = "white";
advancedTexture.addControl(UiPanel);
// ..
var button1 = BABYLON.GUI.Button.CreateSimpleButton("button1", "向上两行");
button1.paddingTop = "10px";
button1.width = "100px";
button1.height = "50px";
button1.background = "green";
button1.isVisible=false;
button1.onPointerDownObservable.add(function(state,info,coordinates) {
if(MyGame.init_state==1)//如果完成了场景的初始化
{
ScrollUporDown(0,1.8,2);//上下滚动
}
});
UiPanel.addControl(button1);
UiPanel.button1=button1;
var button2 = BABYLON.GUI.Button.CreateSimpleButton("button2", "向下两行");
button2.paddingTop = "10px";
button2.width = "100px";
button2.height = "50px";
button2.background = "green";
button2.isVisible=false;
button2.onPointerDownObservable.add(function(state,info,coordinates) {
if(MyGame.init_state==1)//如果完成了场景的初始化
{
ScrollUporDown(1,1.8,2);
}
});
UiPanel.addControl(button2);
UiPanel.button2=button2;
MyGame.UiPanelr=UiPanel;
}

  需要注意的是Babylon.js并不支持父子元素之间isVisible属性的传递,虽然button1和button2都在UiPanel内部,但改变UiPanel的可见性并不会影响两个按钮。

9、Moves.js文件

  A、计算运动效果

 function host20171018(obj)//这里的obj是一个CameraMesh对象
{
//MyGame.player.flag_objfast=Math.max(1,Math.abs(MyGame.Cameras.camera0.position.y/5));
var arr_state=MyGame.arr_keystate;//键盘状态数组
var rad_y=parseFloat(obj.mesh.rotation.y);
var v_obj={x:0,z:0,y:0};//物理模型在自身坐标系中的线速度
//var num_tempx= 0,num_tempz= 0,num_tempy=0;//认为这个是各个方向的分量
if((arr_state[68]-arr_state[65])==0)//同时按下了左右键,或者什么也没按
{ }
else if(arr_state[65]==1)//向左
{
v_obj.x=-obj.vd.left;//采用obj的在这个方向上设定的速度,obj.vd.left是一个标量
}
else if(arr_state[68]==1)
{
v_obj.x=obj.vd.right;
}
if((arr_state[87]-arr_state[83])==0)//同时按下了前后键,或者什么也没按
{ }
else if(arr_state[87]==1)//向前
{
v_obj.z=obj.vd.forward;
}
else if(arr_state[83]==1)
{
v_obj.z=-obj.vd.backwards;
}
if((arr_state[32]-arr_state[16])==0)//同时按下了上下键,或者什么也没按
{ }
else if(arr_state[32]==1)//空格
{
v_obj.y=obj.vd.up;
}
else if(arr_state[16]==1)//shift
{
v_obj.y=-obj.vd.down;
}
//var v_obj0=v_obj.clone();
var v_x=Math.sin(rad_y)*v_obj.z+Math.cos(rad_y)*v_obj.x;//使用高中数学知识进行计算
var v_z=Math.cos(rad_y)*v_obj.z-Math.sin(rad_y)*v_obj.x;
var num_temp=MyGame.DeltaTime*obj.flag_objfast;//两帧之间的时间量乘以速度系数
var v_add=new BABYLON.Vector3(v_x*num_temp,v_obj.y*num_temp,v_z*num_temp);//这一帧内的位移
//console.log(v_add);
obj.mesh.position.addInPlace(v_add);//修改对象位置 }

  这里是一个简单的按住方向键则不断向某一方向匀速运动的算法,相机的俯仰姿态并不影响运动效果,相机左右旋转对运动效果的影响使用三角函数计算,具体计算过程不再赘述。在以前的文章里还有一些其他的运动计算方法,比如带有加速度的计算、寻路计算、带有物理引擎效果的计算,如果感兴趣可以自己查看。

  B、统一CameraMesh相关的对象的姿态:

 function CamerasFollowActor(object)
{
if(object.prototype=CameraMesh)
{
var camera0=MyGame.Cameras.camera0;
if(MyGame.flag_view=="first_lock"||MyGame.flag_view=="first_ani")//动画时相机也要跟随
{
object.mesh.rotation.y = 0+camera0.rotation.y;//CameraMesh的姿态由相机的姿态决定,因为视角调整方法不好编,所以借用Babylon.js的相机控制方法
object.mesh.rotation.x=0+camera0.rotation.x;//而相机的位置则由CameraMesh的位置决定
camera0.position=object.mesh.position.clone()//这里的player没有父元素所以_absolutePosition和position相等
object.mesh.ballman.head.position=object.mesh.position.clone();//我没有设置head是ballman的子元素,所以位置和姿态要手动修改
object.mesh.ballman.head.rotation=object.mesh.rotation.clone();//因为要保留添加物理外壳的可能性 if(MyGame.init_state==2&&MyGame.flag_view=="first_lock")
{//在鼠标不动时实现MouseMove的功能
var width = engine.getRenderWidth();
var height = engine.getRenderHeight();
var pickInfo = scene.pick(width/2, height/2, null, false, MyGame.Cameras.camera0); }
}
else if(MyGame.flag_view=="third")
{ }
else if(MyGame.flag_view=="free")
{ }
}
}

  为了保留对BallMan使用物理引擎的可能,object.mesh.ballman.head并不是object.mesh的子元素,所以还要手动设置object.mesh.ballman.head的位置和姿态。

  在十四行进行了一次鼠标拾取计算,这在鼠标不动但场景内物体移动,导致准心所指对象发生变化时起到作用。

三、卡牌设计

1、卡牌类CardMesh

  CardMesh类是object类的子类,代码位于Character.js文件中。

  A、卡牌对象的实例化,以下的代码实例化了75个卡牌对象

 function DrawCard4()
{
for(var i=;i<;i++)
{
var card_test=new CardMesh();
var obj_p={name:"cardname"+count_cardname,point_x:point_x,point_y:point_y
,card:arr_carddata["test"]//从卡牌数据列表里提取名为“test”的卡牌信息
,linecolor:new BABYLON.Color3(, , ) //边线颜色
,scene:scene
,position:new BABYLON.Vector3(,,)
,rotation:new BABYLON.Vector3(,,)
,scaling:new BABYLON.Vector3(0.1,0.1,0.1)
,belongto:MyGame.WhoAmI//属于哪个玩家
};
card_test.init(obj_p,scene);
card_test.mesh.parent=mesh_arr_cards;
count_cardname++;//命名计数器自增
}
}

  其中arr_carddata是一个存储卡牌种类的数组,位于tab_carddata.js文件中,其格式如下:

 //卡牌数据
arr_carddata={
test:{
imageb:"flower"//卡背种类
,imagemain:"../ASSETS/IMAGE/play.png"//正面的主要图片
,background:"Cu"//卡片正面的背景边框种类
,attack:3,hp:4,cost:2,range:3,speed:5
        //下面是卡片的主要文字
,str_comment:"通过canvas排布生成动态纹理,(或者加入html2canvas,将dom排版转为dataurl?)"
,str_title:"测试卡片"//卡片上显示的卡片名称
}
}

  其中卡背种类和卡片边框种类的信息保存在tab_somedata.js文件中:

 //存放一些通用的数据
var arr_icontypes={test1:"../ASSETS/IMAGE/CURSOR/cursor1.png"//显示在卡片上的一些小图标,表示特殊的状态
,test2:"../ASSETS/IMAGE/CURSOR/cursor2.png"
,test3:"../ASSETS/IMAGE/CURSOR/cursor3.png"} var arr_fronttypes={Cu:"../ASSETS/IMAGE/FRONTTYPE/cu.png"//要把这里的图片纹理设计成只实例化一次
,Ag:"../ASSETS/IMAGE/FRONTTYPE/ag.png"//正面边框种类,分别是铜、银、金
,Au:"../ASSETS/IMAGE/FRONTTYPE/au.png"
,pt:""
}
var arr_backtypes={flower:"../ASSETS/IMAGE/flower.png"//卡背图片 }

  初始化时设置的属性的部分代码如下:

 CardMesh=function()
{
newland.object.call(this);
}
CardMesh.prototype=new newland.object();
CardMesh.prototype.init=function(param,scene)
{
//param = param || {};
if(!param||!param.card)//如果输入的卡牌参数有误
{
alert("卡牌初始化失败");
return;
}
this.name = param.name;//名称
this.point_x = param.point_x;//x方向有几个点
this.point_y = param.point_y;//y方向有几个点
this.imagemain=param.card.imagemain;
this.background=param.card.background;
this.attack=param.card.attack;
this.hp=param.card.hp;
this.cost=param.card.cost;
this.str_comment=param.card.str_comment;
this.str_title=param.card.str_title;
this.range=param.card.range;
this.speed=param.card.speed;
//this.imagef = this.make_imagef();//正面纹理图片使用canvas生成——》还是用多层图片吧
this.imageb = param.card.imageb;//背面纹理图片
this.linecolor = param.linecolor;//未选中时显示边线,选中时用发光边线
this.scene = param.scene;
this.belongto=param.belongto;//表明该卡牌现在由哪个玩家掌控
this.isPicked=false;//这个卡片是否被选中
this.num_group=999;//这个卡片的编队数字,编队越靠前显示越靠前,999表示最大,意为没有编队,显示在列表的最后面
this.pickindex=0;//在被选中卡片数组中的索引,需要不断刷新?

  B、卡牌的网格

  为了赋予卡牌扭曲形变的可能性,我使用了更多的顶点来构成卡牌网格,而不是使用最简单的四个顶点组成矩形面。生成卡牌顶点数据的方法如下:

 //正反表面顶点
this.vertexData = new BABYLON.VertexData();//每一张卡片都要有自己的顶点数组对象,正反两面复用。这个对象要一直保持不变!!
this.make_vertex(this.point_x, this.point_y);//参数是xy方向各有几个顶点,顶点越多可能的变形越细致,但性能消耗也越大
 //生成通用的顶点数组和纹理映射数组
CardMesh.prototype.make_vertex=function(x,y)
{
var positions=[];//顶点位置数组
var uvs=[];//纹理坐标数组
var normals=[];//法线数组
var indices=[];//索引数组
for(var i=0;i<y;i++)//对于每一行顶点
{
for(var j=0;j<x;j++)//对于这一行里的每一个顶点
{
positions.push(j);
positions.push(i);
positions.push(0);//顶点位置 uvs.push((j/(x-1)));
uvs.push((i/(y-1)));//纹理映射位置 }
}
for(var i=0;i<y-1;i++)
{
for(var j=0;j<x-1;j++)
{
var int_point=j+x*i;//第一个点的数字索引
indices.push(int_point);
indices.push(int_point+1);
indices.push(int_point+x);
indices.push(int_point+1);
indices.push(int_point+x+1);
indices.push(int_point+x);//画出两个三角形组成一个矩形
}
}
BABYLON.VertexData.ComputeNormals(positions, indices, normals);//计算法线
BABYLON.VertexData._ComputeSides(0, positions, indices, normals, uvs);
this.vertexData.indices = indices.concat();//索引
this.vertexData.positions = positions.concat();
this.vertexData.normals = normals.concat();//position改变法线也要改变!!!!
this.vertexData.uvs = uvs.concat();
}

  上面遍历了卡片的每一个区域,并生成了对应的三角形,关于顶点位置、纹理坐标、法线、索引的关系,可以查看我的3D编程入门视频教程,或者其他的WebGL资料。

  为了方便的将卡背和卡面设为不同的纹理,我使用这里生成的顶点数据创建了两个相同的网格,一个作为卡背,一个作为卡面,具体设置方法在接下来的材质部分讨论。

  C、卡面材质构成方法

  为了让卡牌的层次感更强,卡面需要由多种不同的材质构成,比如边框(将卡牌的外边缘定义为“边线”,将图片和边线之间的区域定义为“边框”)可能需要金属闪光,图片中要有火焰燃烧或者水波荡漾之类的特效,我找到了两种设计思路:

  方案A是使用自定义着色器对卡牌网格进行渲染,在自定义着色器中用条件判断语句产生多个分支,每个分支使用一种颜色算法,然后将判断条件放在卡面图片一般用不到的透明度通道里,图片不同像素的透明度值不同,则在着色器中导向不同的算法,这种算法应该是当前卡牌材质构成的主流算法。

  其优点在于只需要对一张图片进行操作(这在专业图片处理工具中并不难做),并且自定义着色器的功能精确可控,可以实现非常复杂的特殊效果,同时在卡牌发生弯曲时特效能够伴随卡牌一同弯曲。但这种算法也存在缺陷,比如卡牌中的内容无法灵活的动态变化,每次变化都要重新生成整个图片;同时因为卡面是一个整体,如果操作者想要和卡面上的不同区域做不同互动,则需要设计一套计算光标在卡牌上精确位置的算法;第三则是Babylon.js内置的光照、阴影等功能不支持自定义着色器。

  方案B则是在卡面的不同部分放置多个网格,每个网格使用不同的材质,这样能够提供更高的灵活度和更好的Babylon.js引擎兼容性,但图片与网格一同变形的操作将很难做到。

  两种方案的示意图如下:

在WebGL场景中管理多个卡牌对象的实验

   考虑到B方案更容易实现,决定使用采用B方案生成卡面,以后有机会再向A方案方向迭代。

  D、卡牌网格、纹理的组合:

 //正面纹理
var materialf = new BABYLON.StandardMaterial(this.name+"cardf", this.scene);//测试用卡片纹理
if(MyGame.textures[param.card.background])//如果已经初始化过这种纹理,则使用已经初始化完毕的
{
materialf.diffuseTexture=MyGame.textures[param.card.background];
}
else
{
materialf.diffuseTexture = new BABYLON.Texture(arr_fronttypes[param.card.background], this.scene);
materialf.diffuseTexture.hasAlpha = false;
MyGame.textures[param.card.background]=materialf.diffuseTexture;
}
materialf.backFaceCulling = true;
materialf.bumpTexture = new BABYLON.Texture("../ASSETS/IMAGE/grained_uv.png", scene);//磨砂表面
materialf.useLogarithmicDepth=true;
//背面纹理
var materialb = new BABYLON.StandardMaterial(this.name+"cardb", this.scene);//测试用卡片纹理
if(MyGame.textures[param.card.imageb])//如果已经初始化过这种纹理,则使用已经初始化完毕的
{
materialb.diffuseTexture=MyGame.textures[param.card.imageb];
}
else
{
materialb.diffuseTexture = new BABYLON.Texture(arr_backtypes[param.card.imageb], this.scene);
materialb.diffuseTexture.hasAlpha = false;
MyGame.textures[param.card.imageb]=materialb.diffuseTexture;
}
materialb.backFaceCulling = false;
//materialb.sideOrientation=BABYLON.Mesh.BACKSIDE; var x=this.point_x;
var y=this.point_y; //还是将正反两面作为不同的mesh更直观?
//背面网格
var cardb = new BABYLON.Mesh(this.name + "b", this.scene);
this.vertexData.applyToMesh(cardb, true);//通过顶点数据生成网格
cardb.material = materialb;
cardb.renderingGroupId = 2;
//cardb.position.x+=(x-1);
//cardb.rotation.y=Math.PI;
cardb.sideOrientation = BABYLON.Mesh.BACKSIDE;
cardb.position.y -= (y - 1) / 2;
cardb.position.x -= (x - 1) / 2;
cardb.isPickable=false;
//正面网格
var cardf = new BABYLON.Mesh(this.name + "f", this.scene);
this.vertexData.applyToMesh(cardf, true);
cardf.material = materialf;
cardf.renderingGroupId = 2;
cardf.sideOrientation = BABYLON.Mesh.FRONTSIDE;
cardf.position.y -= (y - 1) / 2;//定义的顶点把左下角设为了零点,而默认的网格则是把中心点设为零点
cardf.position.x -= (x - 1) / 2;
cardf.isPickable=false;
//边线
var path_line = this.make_line(this.vertexData, x, y);//这里是四个顶点,能否自动封口?改用细线+高亮辉光??!!用可见性控制
this.path_line=path_line;
//Mesh的Create方法事实上在调用MeshBuilder的对应Create方法,MeshBuilder的Create方法也可以实现对现有Mesh的变形功能
//var line = new BABYLON.Mesh.CreateLines("line", path_line, this.scene, true);
//Babylon.js不支持调整3D线段的线宽,为了能够调整宽度,将线改为圆柱体
var line =BABYLON.MeshBuilder.CreateTube("line_"+this.name, {path: path_line, radius: 0.05,updatable:false}, scene);
//边线纹理
var materialline = new BABYLON.StandardMaterial("mat_line", this.scene);
materialline.diffuseColor = this.linecolor;
line.material = materialline;
//line.color = new BABYLON.Color3(1, 0, 0);//这个颜色表示方式各个分量在0到1之间
line.renderingGroupId = 2;
line.position.y -= (y - 1) / 2;
line.position.x -= (x - 1) / 2;
line.isVisible=false//非选中状态边线不可见 this.mesh=new BABYLON.MeshBuilder.CreateBox(("card_" +this.name),{width:x-1,height:y-1,depth:0.005},this.scene);
this.mesh.renderingGroupId = 0;//建立一个隐形的盒子作为卡牌正反面网格的父网格
this.mesh.position=param.position;//可以通过点击这个盒子来选择卡片,也可以为这个盒子绑定物理引擎
this.mesh.rotation=param.rotation;
this.mesh.scaling=param.scaling;
this.cardf = cardf;
this.cardb = cardb;
this.line = line;
this.path_line=path_line;
this.arr_path_line=line.getVerticesData(BABYLON.VertexBuffer.PositionKind,false);
cardf.parent = this.mesh;
cardb.parent = this.mesh;
line.parent = this.mesh;
this.mesh.card = this;
//this.mesh.parent=mesh_arr_cards;//按照高内聚低耦合的规则,这个设定不应该放在角色类内部
//暂时使用16:9的高宽设计
var mesh_mainpic=new BABYLON.MeshBuilder.CreateGround(this.name+"mesh_mainpic",{width:8.4,height:9},scene);
mesh_mainpic.parent=this.mesh;//承载正面图片的网格
mesh_mainpic.position=new BABYLON.Vector3(0,2.8,-0.01);
var mat_mainpic = new BABYLON.StandardMaterial(this.name+"mat_mainpic", this.scene);//测试用卡片纹理
mat_mainpic.diffuseTexture = new BABYLON.Texture(this.imagemain, this.scene);//地面的纹理贴图
mat_mainpic.diffuseTexture.hasAlpha = false;
mat_mainpic.backFaceCulling = true;
mat_mainpic.useLogarithmicDepth=true;//虽然还不完全理解为什么,但是这种深度测试方式能够避免“Z-fighting”
mat_mainpic.freeze();
mesh_mainpic.material=mat_mainpic;
mesh_mainpic.renderingGroupId=2;
mesh_mainpic.rotation.x=-Math.PI/2;
mesh_mainpic.isPickable=false; var mesh_comment=new BABYLON.MeshBuilder.CreateGround(this.name+"mesh_comment",{width:6,height:4.8},scene);
mesh_comment.parent=this.mesh;//承载正面文字的网格
mesh_comment.position=new BABYLON.Vector3(0,-4.6,-0.01);
mesh_comment.renderingGroupId=2;
var mat_comment = new BABYLON.StandardMaterial(this.name+"mat_comment", scene);
var texture_comment= new BABYLON.DynamicTexture(this.name+"texture_comment", {width:300, height:240}, scene);
mat_comment.diffuseTexture =texture_comment;//使用基于canvas的动态纹理显示文字
mat_comment.useLogarithmicDepth=true;
mesh_comment.material = mat_comment;
mesh_comment.rotation.x=-Math.PI/2;
mesh_comment.isPickable=false;
var context_comment = texture_comment.getContext();//获取canvas的上下文
context_comment.fillStyle="#0000ff";//使用html5canvas方法
context_comment.fillRect(1,1,150,120);
context_comment.fillStyle="#ffffff";
context_comment.font="bold 32px monospace";
newland.canvasTextAutoLine(this.str_comment,context_comment,1,30,35,34);
texture_comment.update();//修改canvas后更新动态纹理
}

  在绘制卡面时遇到几个问题:

  a、卡牌的边线与被选中效果设计

  原计划使用Babylon.js内置的线段系统绘制卡牌边线,然后对卡牌的正面使用内置的“边缘高光”功能表示卡片被选中。但实际测试时发现内置的3D线段无法修改线宽(锁定为1像素),所以改用细长的圆柱体网格(管道网格)代表线段。(边缘高光和动态纹理的官方文档中英对照可以在http://down.51cto.com/data/2450646下载)

  生成边线路径的代码如下:

 //边线轨迹
CardMesh.prototype.make_line=function(vertexData,x,y)
{
var path_line=[];
//找边线上的所有点
for(var i=0;i<x-1;i++)
{
path_line.push(new BABYLON.Vector3(vertexData.positions[i*3],vertexData.positions[i*3+1],vertexData.positions[i*3+2]));
}
for(var i=0;i<y-1;i++)
{
path_line.push(new BABYLON.Vector3(vertexData.positions[3*(x-1)+i*x*3],vertexData.positions[3*(x-1)+i*x*3+1],vertexData.positions[3*(x-1)+i*x*3+2]));
}
for(var i=x-1;i>0;i--)
{
path_line.push(new BABYLON.Vector3(vertexData.positions[(y-1)*x*3+i*3],vertexData.positions[(y-1)*x*3+i*3+1],vertexData.positions[(y-1)*x*3+i*3+2]));
}
for(var i=y-1;i>=0;i--)
{
path_line.push(new BABYLON.Vector3(vertexData.positions[i*x*3],vertexData.positions[i*x*3+1],vertexData.positions[i*x*3+2]));
}
return path_line;
}

  Babylon.js内置的边缘高光功能也不尽如人意,一是内部图片网格和文字网格遮挡铜片(边框)的边缘也出现了高光,二是高光层与图片网格和文字网格发生类似深度缓存异常(z-fighting)(后面讨论)的现象。

  内部网格的边缘也出现了高亮:在WebGL场景中管理多个卡牌对象的实验

  z-fighting效果:

在WebGL场景中管理多个卡牌对象的实验

  所以改为对作为边框的管道网格设置边缘高亮,但是不太清楚如何控制高亮范围的大小。

  b、深度缓存异常

  OpenGL使用深度缓存保存屏幕上的每个像素距视点的距离,如果一个像素的位置有多个图元(三角形)存在,则会用三角形距视点的距离与深度缓存比较,如果图元的距离更近则使用这个图元的颜色,并修改深度缓存为这个图元的距离,通过这种方式OpenGL可以实现“前面的物体遮挡后面的物体”这种效果。

  然而这种方法存在问题,一方面因为浮点数精度限制,距离过近的图元会被认为处于同一深度层次甚至相反,这时前后两个表面会交替闪烁显示,这种异常称为“z-fighting”,比如上面的正面网格和图片网格、文字网格之间就可能发生z-fighting;另一方面因为浮点数大小限制,过于遥远的图元所在的深度层次可能超过浮点数上限,产生“深度缓存溢出”,比如同时存在近景物体和宇宙星空的情况。

  为了避免这两种深度缓存异常,对正面和图片网格、文字网格的材质设置了“materialf.useLogarithmicDepth=true;”,这种“对数深度缓存”,将原来图元距离和深度线性对应的计算方式,改为在对数计算方式,这样就在较近的地方建立了很多距离相差很小的深度分层,而在很远的地方则是数量较少的距离相差很大的深度分层。需要注意的是,并不是所有浏览器都支持对数深度缓存,设置了这个属性后Babylon.js会尝试使用对数深度缓存,如果失败则仍使用默认的线性深度缓存。

  c、动态纹理

  上述代码使用Babylon.js的动态纹理功能为文字网格设置了可以动态变化的纹理,这种动态纹理是基于canvas实现的,你可以获取这个canvas的上下文并使用各种html5 canvas方法生成图像。另外Babylon.js也提供了一些封装的操作动态纹理的方法,比如initArena中的数字标签,这种封装的方法不需要调用texture_comment.update()来更新动态纹理,但存在文字定位不准的问题,需要反复调试

  newland.canvasTextAutoLine是根据网上的代码修改而成的,在canvas中自动换行书写文字的方法:  

 //向一个canvas上下文里自动换行的插入文字(来自网上)
newland.canvasTextAutoLine=function(str,ctx,initX,initX2,initY,lineHeight){
//var ctx = canvas.getContext("2d");
var lineWidth = 0;
var canvasWidth = ctx.canvas.width;
var lastSubStrIndex= 0;
for(let i=0;i<str.length;i++){
lineWidth+=ctx.measureText(str[i]).width;
if(lineWidth>canvasWidth-initX2){//减去initX,防止边界出现的问题
ctx.fillText(str.substring(lastSubStrIndex,i),initX,initY);
initY+=lineHeight;
lineWidth=0;
lastSubStrIndex=i;
}
if(i==str.length-1){
ctx.fillText(str.substring(lastSubStrIndex,i+1),initX,initY);
}
}
}

  不知道是什么原因,这个方法运行的效果和网上的例子存在偏差,只好加一些偏移量手动微调。

2、卡牌的处理

  A、按下Alt键时卡牌的移入和移除动画(代码位于HandleCard2.js文件中)

  这里的设计是以mesh_arr_cards作为所有CardMesh对象的父网格,在first_lock状态下mesh_arr_cards的父网格为BallMan的backview且position为0,这意味着卡牌将一直在操作者背后跟随操作者移动,所以操作者看不到卡牌。

  在按下Alt键时,浏览模式变为first_ani,卡牌的位置跟随动画从后向前移动,移动到BallMan的handpoint位置时卡牌的position置为0,父元素改为handpoint,同时浏览模式变为first_pick。

  从first_pick变为first_lock的过程与此相反。

 function HandCard(flag)//用动画方式表现手牌的“展开和收拢”
{
var pos1,pos2;
MyGame.flag_view="first_pick";
if(flag==0)//将手牌从后面推到前面
{
pos1=new BABYLON.Vector3(0,0,0);
pos2=new BABYLON.Vector3(0,-2,16);
var animation3=new BABYLON.Animation("ani_HandCard0","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys1=[{frame:0,value:pos1},{frame:15,value:pos2}];
animation3.setKeys(keys1);
mesh_arr_cards.animations.push(animation3);
scene.beginAnimation(mesh_arr_cards, 0, 15, false,1,function(){
mesh_arr_cards.position=new BABYLON.Vector3(0,0,0);//动画执行结束时执行的函数
mesh_arr_cards.parent=MyGame.player.mesh.ballman.handpoint;
MyGame.flag_view="first_pick";
MyGame.UiPanelr.button1.isVisible=true;
MyGame.UiPanelr.button2.isVisible=true;
});
}
else if(flag==1)//将手牌从前面拉到后面
{
pos1=new BABYLON.Vector3(0,0,0);
pos2=new BABYLON.Vector3(0,2,-16);
var animation3=new BABYLON.Animation("ani_HandCard1","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys1=[{frame:0,value:pos1},{frame:15,value:pos2}];
animation3.setKeys(keys1);
mesh_arr_cards.animations.push(animation3);
scene.beginAnimation(mesh_arr_cards, 0, 15, false,1,function(){
mesh_arr_cards.position=new BABYLON.Vector3(0,0,0);
mesh_arr_cards.parent=MyGame.player.mesh.ballman.backview;
MyGame.flag_view="first_lock";
});
}
}

  B、卡牌的分组和排序

  可以将选取的一些卡牌编成一队,被编队的卡牌将显示在所有卡牌之前。

  编队:

function HandleGroup(keyCode)//按1到5时处理手牌分组
{
var len =arr_pickedCards.length;//所有选中的卡片
var group=arr_cardgroup[keyCode-49];//取按下的按键对应的这一组
for(var key in group)
{//如果这一组里已经有成员
group[key].num_group=999;//将这些成员设为未分组状态
}
arr_cardgroup[keyCode-49]={};//清空原来的分组
for(var i=0;i<len;i++)//对于每一张选中的卡片
{
var card=arr_pickedCards[i];
if(card.num_group!=999)//解除原来的绑定
{
delete arr_cardgroup[card.num_group][card.mesh.name];//将卡牌移出原来的分组
}
//arr_cardgroup[card.num_group].delete(card.mesh.name);
card.num_group=keyCode-49;//双向绑定,第一队要对应索引0!!
arr_cardgroup[keyCode-49][card.mesh.name]=card;
noPicked(card);//分入小队后,取消这张卡牌的选中效果
}
//重绘前要清空已选中手牌
arr_pickedCards=[];
SortCard()//根据分组情况将手牌重新排序
}

  在初始化卡牌(DrawCard4())和卡牌分组之后执行SortCard()为每张卡牌安排新的位置:

 var arr_cardgroup=[{},{},{},{},{}];//五个分组的成员情况
var arr_mesh_groupicon=[];//在每一组分组元素前显示组号
//根据mesh_arr_cards._children和arr_cardgroup进行排序
function SortCard()
{
var arr_mycards=mesh_arr_cards._children;
var len=arr_mycards.length;
var lenx = 10;//每一行的元素个数
var leny = 4;//一页显示的最多有2行
var count=0;//记录元素位置占用了多少个
var widthp = 0.9;//每个卡片经过缩放后的实际宽度
var heightp = 1.6;//每一张卡牌的实际高度
var marginx = 0.2;//x方向间隙大小
var marginy = 0.2;//y方向间隙大小
var len2=arr_cardgroup.length;
for(var i=0;i<len2;i++)//先绘制分组的元素
{
var obj=arr_cardgroup[i];
var flag_icon=0;//是否已经放置了标记
var x=0,y=0;
for(key in obj)
{
x = count % lenx;//从左往右数的索引
y = Math.floor(count / lenx);//从上往下数的索引
var posx = (x - lenx / 2) * (widthp + marginx);//根据索引算出位置
var posy = -(y - leny / 2) * (heightp + marginy) - 0.2;
if(flag_icon==0)//还未放置标记
{//则将这个小组的标记设为这个小组的第一张卡牌的子元素
//arr_mesh_groupicon[i].position=new BABYLON.Vector3(posx-1.5,posy,0);
arr_mesh_groupicon[i].parent=obj[key].mesh;
arr_mesh_groupicon[i].isVisible=true;
flag_icon=1;
}
obj[key].mesh.position=new BABYLON.Vector3(posx,posy,0);
count++;//表示又占用了一个空位
}
if(flag_icon==0)//如果最后也每放置标记,说明这个分组没有元素,将分组标记撤除
{
arr_mesh_groupicon[i].isVisible=false;
}
else
{//如果用到了这个分组
count=(y+1)* lenx;//空位补齐,每个分组都另起一行
}
}
for(var i=0;i<len;i++)//处理小队以外的其他卡牌
{
var mesh=arr_mycards[i];
if(mesh.card.num_group==999)//处理没有分组的元素
{
var x = count % lenx;//从左往右数的索引
var y = Math.floor(count / lenx);//从上往下数的索引
var posx = (x - lenx / 2) * (widthp + marginx);
var posy = -(y - leny / 2) * (heightp + marginy) - 0.2;
mesh.position=new BABYLON.Vector3(posx,posy,0);
count++;
} }
}

  C、卡牌的选取效果

  卡牌被选中时需要高亮边缘,取消选中时解除高亮:

 function getPicked(card)
{//将卡片标识为选中状态,设置高亮边框,并且将它作为第一个选中点
card.line.isVisible=true;
MyGame.hl.addMesh(card.line,card.linecolor);
MyGame.hl.addMesh(card.mesh,card.linecolor);//mesh不可见则不会生成对应高光层
//card.line.width=1000;
//card.line=BABYLON.MeshBuilder.CreateTube(card.line.name, {path: card.path_line, radius:0.2,updatable:true,instance:card.line}, scene);
card.isPicked=true;
//card.pickindex=arr_pickedCards.length;//
}
function noPicked(card)
{
card.line.isVisible=false;
MyGame.hl.removeMesh(card.line);
MyGame.hl.removeMesh(card.mesh);
//card.line.width=100;
//card.line=BABYLON.MeshBuilder.CreateTube(card.line.name, {path: card.path_line, radius:0.05,updatable:true,instance:card.line}, scene);
card.isPicked=false;
}

  点击被选中的卡牌则把卡牌放大,其原理与前面的移入移出手牌相同:

 var card_Closed=null;
function GetCardClose(card)//将卡牌拉近
{
MyGame.flag_view="first_ani";
if(card_Closed)//如果已经有一个拉近的卡片
{
var animation1=new BABYLON.Animation("animation1","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys1=[{frame:0,value:card_Closed.mesh.position.clone()},{frame:15,value:card_Closed.oldpositon}];
animation1.setKeys(keys1);
card_Closed.mesh.animations.push(animation1);
var animation2=new BABYLON.Animation("animation2","scaling",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys2=[{frame:0,value:new BABYLON.Vector3(0.5,0.5,0.5)},{frame:15,value:new BABYLON.Vector3(0.1,0.1,0.1)}];
animation2.setKeys(keys2);
card_Closed.mesh.animations.push(animation2);
scene.beginAnimation(card_Closed.mesh, 0, 15, false,1,function(){//可以在一个动画结束时再启动另一个动画
card_Closed=card;
card.oldpositon=card.mesh.position.clone();
var animation3=new BABYLON.Animation("animation3","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys1=[{frame:0,value:card_Closed.mesh.position.clone()},{frame:15,value:new BABYLON.Vector3(0,0,-0.5)}];
animation3.setKeys(keys1);
card_Closed.mesh.animations.push(animation3);
var animation4=new BABYLON.Animation("animation4","scaling",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys2=[{frame:0,value:new BABYLON.Vector3(0.1,0.1,0.1)},{frame:15,value:new BABYLON.Vector3(0.5,0.5,0.5)}];
animation4.setKeys(keys2);
card_Closed.mesh.animations.push(animation4);
scene.beginAnimation(card_Closed.mesh, 0, 15, false,1,function(){ MyGame.flag_view="first_pick";
});
}); }
else
{
card_Closed=card;
card.oldpositon=card.mesh.position.clone();
var animation3=new BABYLON.Animation("animation3","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys1=[{frame:0,value:card_Closed.mesh.position.clone()},{frame:15,value:new BABYLON.Vector3(0,0,-0.5)}];
animation3.setKeys(keys1);
card_Closed.mesh.animations.push(animation3);
var animation4=new BABYLON.Animation("animation4","scaling",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys2=[{frame:0,value:new BABYLON.Vector3(0.1,0.1,0.1)},{frame:15,value:new BABYLON.Vector3(0.5,0.5,0.5)}];
animation4.setKeys(keys2);
card_Closed.mesh.animations.push(animation4);
scene.beginAnimation(card_Closed.mesh, 0, 15, false,1,function(){
MyGame.flag_view="first_pick";
});
}
}

  D、卡牌的多选

  参考Windows系统的文件多选编写了卡牌多选功能:

 //对已经建立的卡片的各种处理方法放在这里
var arr_pickedCards=[];
function PickCard(card)
{
if(card_Closed)//如果有拉近显示的卡片,则先把他恢复原样
{
MyGame.flag_view="first_ani";
var animation1=new BABYLON.Animation("animation1","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys1=[{frame:0,value:card_Closed.mesh.position.clone()},{frame:15,value:card_Closed.oldpositon}];
animation1.setKeys(keys1);
card_Closed.mesh.animations.push(animation1);
var animation2=new BABYLON.Animation("animation2","scaling",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
var keys2=[{frame:0,value:new BABYLON.Vector3(0.5,0.5,0.5)},{frame:15,value:new BABYLON.Vector3(0.1,0.1,0.1)}];
animation2.setKeys(keys2);
card_Closed.mesh.animations.push(animation2);
scene.beginAnimation(card_Closed.mesh, 0, 15, false,1,function(){
card_Closed=null;
MyGame.flag_view="first_pick";
});
return;
} var len=arr_pickedCards.length;
var arr_state=MyGame.arr_keystate;
var arr_mycards=mesh_arr_cards._children;//这个数组里的元素都是网格
var len2=arr_mycards.length;
var count=0;//加入分组特性后排序也要修改一下
var len3=arr_cardgroup;
for(var i=0;i<len3;i++)//在开始选择之前根据排序结果,为每张卡牌分配一个索引
{
for(key in arr_cardgroup[i])
{
arr_cardgroup[i][key].index=count;
count++;
}
}
for(var i=0;i<len2;i++)
{
arr_mycards[i].card.index=count;
count++;
} if(card.isPicked)
//如果目前已经选中这个卡片,
// 如果有多个选中卡片,如果当前按住了Ctrl,则取消它的选中(alt留着用来切换视角)
//如果按住了shift,则将shift选择的区域截断到这里(允许同时按下?)
// ,如果没有按住space或shift,则取消除它以外的所有选中并拉近
// 如果只有这一个选中卡片,则放大它
{
if(len>1)
{
if(arr_state[16]==1)//按着shift
{
if(card_firstpick)//如果已经选定过一个卡片,将首选卡片和这个卡片之间的所有卡片选定
{//card_firstpick是“第一个被选中的卡片”也称为“首选卡片”
if(card_firstpick.index>card.index)
{
for(var i=0;i<len2;i++)
{
var card0=arr_mycards[i].card;
if(i<=card_firstpick.index&&i>=card.index)
{
if(i!=card_firstpick.index)//首选元素就不向选取数组里放了
{
//选中选取范围内的所有元素
getPicked(card0);
//card_firstpick=card;
arr_pickedCards.push(card0);
}
}
else
{//删除选取范围外的所有已选中元素
if(card0.isPicked)
{
noPicked(card0);//
var len3=arr_pickedCards.length;
for(var j=0;j<len3;j++){//从选取数组中找到这个元素,并删除它
if(arr_pickedCards[j].mesh.name==card0.mesh.name)
{
arr_pickedCards.splice(j,1);
break;
}
}
}
}
}
}
else if(card_firstpick.index<card.index)//
{
for(var i=0;i<len2;i++)
{
var card0=arr_mycards[i].card;
if(i>=card_firstpick.index&&i<=card.index)
{
if(i!=card_firstpick.index)//首选元素就不向选取数组里放了
{
//选中选取范围内的所有元素
getPicked(card0);
//card_firstpick=card;
arr_pickedCards.push(card0);
}
}
else
{//删除选取范围外的所有已选中元素
if(card0.isPicked)
{
noPicked(card0);//
var len3=arr_pickedCards.length;
for(var j=0;j<len3;j++){//从选取数组中找到这个元素,并删除它
if(arr_pickedCards[j].mesh.name==card0.mesh.name)
{
arr_pickedCards.splice(j,1);
break;
}
}
}
}
}
}
else if(card_firstpick.index==card.index)
{
GetCardClose(card);//将这张卡片拿近
//同时释放掉所有被选中的卡片
for(var i=0;i<len;i++)
{
var card0=arr_pickedCards[i];
noPicked(card0);
}
arr_pickedCards=[];
card_firstpick=null;
}
}
}
if(arr_state[17]==1)//ctrl
{//取消这张卡片的选中
noPicked(card);
//var len3=arr_pickedCards.length;
for(var j=0;j<len;j++){//从选取数组中找到这个元素,并删除它
if(arr_pickedCards[j].mesh.name==card.mesh.name)
{
arr_pickedCards.splice(j,1);
break;
}
}
card_firstpick=arr_pickedCards[arr_pickedCards.length-1];
}
if(arr_state[17]!=1&&arr_state[16]!=1)
{
GetCardClose(card);//将这张卡片拿近
//同时释放掉所有被选中的卡片
for(var i=0;i<len;i++)
{
var card0=arr_pickedCards[i];
noPicked(card0);
}
arr_pickedCards=[];
card_firstpick=null;
}
}
else//目前只有这一张卡片被选中,然后点击了他
{
GetCardClose(card);
noPicked(card);
arr_pickedCards=[];
card_firstpick=null;
}
}
else //这张卡片还没有被选中
{
if(len>0)//还有其他被选中的卡片
{
if(arr_state[16]==1)//按着shift
{
if(card_firstpick)//如果已经选定过一个卡片,将首选卡片和这个卡片之间的所有卡片选定
{//如果选取数组不空则一定有首选元素?? if(card_firstpick.index>card.index)
{
for(var i=0;i<len2;i++)
{
var card0=arr_mycards[i].card;
if(i<=card_firstpick.index&&i>=card.index)
{
if(i!=card_firstpick.index)//首选元素就不向选取数组里放了
{
//选中选取范围内的所有元素
getPicked(card0);
//card_firstpick=card;
arr_pickedCards.push(card0);
}
}
else
{//删除选取范围外的所有已选中元素
if(card0.isPicked)
{
noPicked(card0);//
var len3=arr_pickedCards.length;
for(var j=0;j<len3;j++){//从选取数组中找到这个元素,并删除它
if(arr_pickedCards[j].mesh.name==card0.mesh.name)
{
arr_pickedCards.splice(j,1);
break;
}
}
}
}
}
}
else if(card_firstpick.index<card.index)//因为card是未选中的所以card_firstpick.index与card.index不会相等
{
for(var i=0;i<len2;i++)
{
var card0=arr_mycards[i].card;
if(i>=card_firstpick.index&&i<=card.index)
{
if(i!=card_firstpick.index)//首选元素就不向选取数组里放了
{
//选中选取范围内的所有元素
getPicked(card0);
//card_firstpick=card;
arr_pickedCards.push(card0);
}
}
else
{//删除选取范围外的所有已选中元素
if(card0.isPicked)
{
noPicked(card0);//
var len3=arr_pickedCards.length;
for(var j=0;j<len3;j++){//从选取数组中找到这个元素,并删除它
if(arr_pickedCards[j].mesh.name==card0.mesh.name)
{
arr_pickedCards.splice(j,1);
break;
}
}
}
}
}
}
}
else
{//理论上讲,这里不会进入
getPicked(card);
card_firstpick=card;
arr_pickedCards.push(card);
}
}
if(arr_state[17]==1)//Ctrl
{
getPicked(card);
card_firstpick=card;
arr_pickedCards.push(card);
}
if(arr_state[17]!=1&&arr_state[16]!=1)
{//在没有按下shift或者Ctrl时,点击一个未选中的卡片,则释放以前选中的所有卡片,然后选中这个
for(var i=0;i<len;i++)
{
var card0=arr_pickedCards[i];
noPicked(card0);
}
arr_pickedCards=[];
card_firstpick=null;
getPicked(card);
card_firstpick=card;
arr_pickedCards.push(card);
}
}
else//没有其他被选中的卡片,这应该是最简单的情况?
{
//card.getPicked();
getPicked(card);
card_firstpick=card;
arr_pickedCards.push(card);
}
}
}

  E、卡牌的上下滚动

  只是给FullUI.js中的两个按钮添加了响应方法:

 function ScrollUporDown(flag,heightp,row)//flag0表示向上,1表示向下,heightp表示每行滚动距离,row表示滚动行数
{
//var poshand=MyGame.player.mesh.ballman.handpoint._absolutePosition;//此时手的位置
if(flag==0)
{
var arr_mycards=mesh_arr_cards._children;
var posy=arr_mycards[arr_mycards.length-1].position.y;//找到位置最低的卡牌
if(mesh_arr_cards.position.y<(0-posy-row*heightp))//不能向上滚动太多,不完全精确的限定,但也够了
{
mesh_arr_cards.position.y+=row*heightp;
} }
else if(flag==1)
{
if(mesh_arr_cards.position.y>=row*heightp)
{
mesh_arr_cards.position.y-=row*heightp
} }
}

  于是完成了最初演示的功能。

四、总结

  文中的框架代码部分参考了Tony Parisi(Three.js入门书《WebGL入门指南》的作者)的sim.js库(https://github.com/tparisi/WebGLBook/tree/master/sim)和 Julian Chenard(a French 30 years-old engineer currently working as WebGL developer in Rouen (not so far from Paris))的FPS例程(http://www.pixelcodr.com/projects.html),再加上一些WebGL书籍和网络资料综合而来。整个框架命名为newland,意为探索新的领域,基于MIT协议发布。

  卡牌设计部分因为作者美工才能有限而并不好看,但提供了一定的可扩展性,欢迎使用者自行扩展。接下来计划在场景中加入Babylon.js的瓷砖(棋盘)网格,并添加卡牌的移动和影响范围计算,以及技能效果计算。

  因为精力有限,这篇文章没有经过充分的校对和修改,如果您发现有错误或者没说清楚的地方,请留言提醒,谢谢。

附用谷歌浏览器将网页保存为mhtml格式的方法(修改自百度经验):

  1、打开chrome浏览器,地址栏输入:chrome://flags/ 后回车

  2、Ctrl+F 搜索 MHTML,并找到 将网页另存为MHTML,点击 启动

  3、启用后,重启chrome,Ctrl+S即可选择保存为mhtml格式