在WebGL場景中管理多個卡牌對象的實驗

  這篇文章討論如何在基於Babylon.js的WebGL場景中,實現多個簡單卡牌類對象的顯示、選擇、分組、排序,同時創建一套實用的3D場景代碼框架。因爲做者美工能力有限,因此示例場景視覺效果可能欠佳,本文的重點在於對相關技術的探討。css

  由於文章比較長,讀者能夠考慮將網頁導出爲mhtml格式,使用Word瀏覽。Chrome瀏覽器導出mhtml文件的方法見末尾。html

1、顯示效果:html5

一、訪問https://ljzc002.github.io/CardSimulate/HTML/TEST2.html查看「卡牌模擬頁面」:git

  場景中間是三個做爲參照物的小球,視口平面的中間是一個用Babylon.js GUI製做的準星,默認鼠標與準星鎖定在一塊兒,直接移動鼠標便可改變相機視角,使用WASD Shift 空格鍵能夠控制相機前、左、後、右、下、上運動(可能將Ctrl鍵設爲向下更符合傳統,可是沒有找到禁用瀏覽器Ctrl+s快捷鍵的方法,只好用Shift代替)。由於光標被鎖定,將這種瀏覽狀態命名爲「first_lock」。github

二、按下Alt鍵,75張卡片經過動畫移入相機視野,同時相機的位置被固定(但仍能夠經過拖動鼠標改變視角):web

  點擊右側的「向上兩行」和「向下兩行」按鈕能夠上下滾動卡片,再次按下Alt鍵將隱藏卡片,同時恢復相機的移動和光標的鎖定。由於這種瀏覽狀態主要用來點選場景中的物體,將它命名爲「first_pick」。算法

三、鼠標左鍵單擊一張卡片,卡片將處於「選中狀態」(綠色邊緣),再次左鍵單擊處於選中狀態的卡片,卡片將被放大拉近顯示,再左鍵單擊將恢復原位:chrome

  執行動畫時會禁用用戶的控制,徹底由動畫控制視角,因此將這種瀏覽狀態命名爲「first_ani」。數據庫

四、模仿Windows的文件多選編寫了卡片多選功能,按下Ctrl時能夠點選多個卡片,按下Shift時能夠選取首尾之間的全部卡片:編程

五、選中若干張卡片後,按1-5鍵能夠將被選中的卡片編爲1-5隊,被編隊的卡片將按編隊順序顯示在最高處,同時編隊的前面會顯示隊號標記:

六、在first_pick狀態可使用上下左右方向鍵進行場景漫遊,能夠看到場景中的全部對象:

 2、代碼實現:

一、文件結構:

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裏是其餘輔助信息

二、代碼入口與場景初始化:

  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徹底相同,則從理論上講不存在這種限制,但並未測試過。

三、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查看。

四、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中也許會有錯誤)

五、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身上的各個位置。

六、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類的代碼裏。

七、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裏的代碼對按鍵狀態數組進行檢查,以此計算相應的運動效果。

  這段代碼還監聽了鼠標離開和移入瀏覽器的事件,這時全部按鍵的狀態將被歸零。

八、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的可見性並不會影響兩個按鈕。

九、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的位置和姿態。

  在十四行進行了一次鼠標拾取計算,這在鼠標不動但場景內物體移動,致使準心所指對象發生變化時起到做用。

3、卡牌設計

一、卡牌類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、卡牌的網格

  爲了賦予卡牌扭曲形變的可能性,我使用了更多的頂點來構成卡牌網格,而不是使用最簡單的四個頂點組成矩形面。生成卡牌頂點數據的方法以下:

1 //正反表面頂點
2     this.vertexData = new BABYLON.VertexData();//每一張卡片都要有本身的頂點數組對象,正反兩面複用。這個對象要一直保持不變!!
3     this.make_vertex(this.point_x, this.point_y);//參數是xy方向各有幾個頂點,頂點越多可能的變形越細緻,但性能消耗也越大
 1 //生成通用的頂點數組和紋理映射數組
 2 CardMesh.prototype.make_vertex=function(x,y)
 3 {
 4     var positions=[];//頂點位置數組
 5     var uvs=[];//紋理座標數組
 6     var normals=[];//法線數組
 7     var indices=[];//索引數組
 8     for(var i=0;i<y;i++)//對於每一行頂點
 9     {
10         for(var j=0;j<x;j++)//對於這一行裏的每個頂點
11         {
12             positions.push(j);
13             positions.push(i);
14             positions.push(0);//頂點位置
15 
16             uvs.push((j/(x-1)));
17             uvs.push((i/(y-1)));//紋理映射位置
18 
19         }
20     }
21     for(var i=0;i<y-1;i++)
22     {
23         for(var j=0;j<x-1;j++)
24         {
25             var int_point=j+x*i;//第一個點的數字索引
26             indices.push(int_point);
27             indices.push(int_point+1);
28             indices.push(int_point+x);
29             indices.push(int_point+1);
30             indices.push(int_point+x+1);
31             indices.push(int_point+x);//畫出兩個三角形組成一個矩形
32         }
33     }
34     BABYLON.VertexData.ComputeNormals(positions, indices, normals);//計算法線
35     BABYLON.VertexData._ComputeSides(0, positions, indices, normals, uvs);
36     this.vertexData.indices = indices.concat();//索引
37     this.vertexData.positions = positions.concat();
38     this.vertexData.normals = normals.concat();//position改變法線也要改變!!!!
39     this.vertexData.uvs = uvs.concat();
40 }

  上面遍歷了卡片的每個區域,並生成了對應的三角形,關於頂點位置、紋理座標、法線、索引的關係,能夠查看個人3D編程入門視頻教程,或者其餘的WebGL資料。

  爲了方便的將卡背和卡面設爲不一樣的紋理,我使用這裏生成的頂點數據建立了兩個相同的網格,一個做爲卡背,一個做爲卡面,具體設置方法在接下來的材質部分討論。

  C、卡面材質構成方法

  爲了讓卡牌的層次感更強,卡面須要由多種不一樣的材質構成,好比邊框(將卡牌的外邊緣定義爲「邊線」,將圖片和邊線之間的區域定義爲「邊框」)可能須要金屬閃光,圖片中要有火焰燃燒或者水波盪漾之類的特效,我找到了兩種設計思路:

  方案A是使用自定義着色器對卡牌網格進行渲染,在自定義着色器中用條件判斷語句產生多個分支,每一個分支使用一種顏色算法,而後將判斷條件放在卡面圖片通常用不到的透明度通道里,圖片不一樣像素的透明度值不一樣,則在着色器中導向不一樣的算法,這種算法應該是當前卡牌材質構成的主流算法。

  其優勢在於只須要對一張圖片進行操做(這在專業圖片處理工具中並不難作),而且自定義着色器的功能精確可控,能夠實現很是複雜的特殊效果,同時在卡牌發生彎曲時特效可以伴隨卡牌一同彎曲。但這種算法也存在缺陷,好比卡牌中的內容沒法靈活的動態變化,每次變化都要從新生成整個圖片;同時由於卡面是一個總體,若是操做者想要和卡面上的不一樣區域作不一樣互動,則須要設計一套計算光標在卡牌上精確位置的算法;第三則是Babylon.js內置的光照、陰影等功能不支持自定義着色器。

  方案B則是在卡面的不一樣部分放置多個網格,每一個網格使用不一樣的材質,這樣可以提供更高的靈活度和更好的Babylon.js引擎兼容性,但圖片與網格一同變形的操做將很難作到。

  兩種方案的示意圖以下:

   考慮到B方案更容易實現,決定使用採用B方案生成卡面,之後有機會再向A方案方向迭代。

  D、卡牌網格、紋理的組合:

  1 //正面紋理
  2     var materialf = new BABYLON.StandardMaterial(this.name+"cardf", this.scene);//測試用卡片紋理
  3     if(MyGame.textures[param.card.background])//若是已經初始化過這種紋理,則使用已經初始化完畢的
  4     {
  5         materialf.diffuseTexture=MyGame.textures[param.card.background];
  6     }
  7     else
  8     {
  9         materialf.diffuseTexture = new BABYLON.Texture(arr_fronttypes[param.card.background], this.scene);
 10         materialf.diffuseTexture.hasAlpha = false;
 11         MyGame.textures[param.card.background]=materialf.diffuseTexture;
 12     }
 13     materialf.backFaceCulling = true;
 14     materialf.bumpTexture = new BABYLON.Texture("../ASSETS/IMAGE/grained_uv.png", scene);//磨砂表面
 15     materialf.useLogarithmicDepth=true;
 16     //背面紋理
 17     var materialb = new BABYLON.StandardMaterial(this.name+"cardb", this.scene);//測試用卡片紋理
 18     if(MyGame.textures[param.card.imageb])//若是已經初始化過這種紋理,則使用已經初始化完畢的
 19     {
 20         materialb.diffuseTexture=MyGame.textures[param.card.imageb];
 21     }
 22     else
 23     {
 24         materialb.diffuseTexture = new BABYLON.Texture(arr_backtypes[param.card.imageb], this.scene);
 25         materialb.diffuseTexture.hasAlpha = false;
 26         MyGame.textures[param.card.imageb]=materialb.diffuseTexture;
 27     }
 28     materialb.backFaceCulling = false;
 29     //materialb.sideOrientation=BABYLON.Mesh.BACKSIDE;
 30 
 31 
 32     var x=this.point_x;
 33     var y=this.point_y;
 34 
 35     //仍是將正反兩面做爲不一樣的mesh更直觀?
 36     //背面網格
 37     var cardb = new BABYLON.Mesh(this.name + "b", this.scene);
 38     this.vertexData.applyToMesh(cardb, true);//經過頂點數據生成網格
 39     cardb.material = materialb;
 40     cardb.renderingGroupId = 2;
 41     //cardb.position.x+=(x-1);
 42     //cardb.rotation.y=Math.PI;
 43     cardb.sideOrientation = BABYLON.Mesh.BACKSIDE;
 44     cardb.position.y -= (y - 1) / 2;
 45     cardb.position.x -= (x - 1) / 2;
 46     cardb.isPickable=false;
 47     //正面網格
 48     var cardf = new BABYLON.Mesh(this.name + "f", this.scene);
 49     this.vertexData.applyToMesh(cardf, true);
 50     cardf.material = materialf;
 51     cardf.renderingGroupId = 2;
 52     cardf.sideOrientation = BABYLON.Mesh.FRONTSIDE;
 53     cardf.position.y -= (y - 1) / 2;//定義的頂點把左下角設爲了零點,而默認的網格則是把中心點設爲零點
 54     cardf.position.x -= (x - 1) / 2;
 55     cardf.isPickable=false;
 56     //邊線
 57     var path_line = this.make_line(this.vertexData, x, y);//這裏是四個頂點,可否自動封口?改用細線+高亮輝光??!!用可見性控制
 58     this.path_line=path_line;
 59     //Mesh的Create方法事實上在調用MeshBuilder的對應Create方法,MeshBuilder的Create方法也能夠實現對現有Mesh的變形功能
 60     //var line = new BABYLON.Mesh.CreateLines("line", path_line, this.scene, true);
 61     //Babylon.js不支持調整3D線段的線寬,爲了可以調整寬度,將線改成圓柱體
 62     var line =BABYLON.MeshBuilder.CreateTube("line_"+this.name, {path: path_line, radius: 0.05,updatable:false}, scene);
 63     //邊線紋理
 64     var materialline = new BABYLON.StandardMaterial("mat_line", this.scene);
 65     materialline.diffuseColor = this.linecolor;
 66     line.material = materialline;
 67     //line.color = new BABYLON.Color3(1, 0, 0);//這個顏色表示方式各個份量在0到1之間
 68     line.renderingGroupId = 2;
 69     line.position.y -= (y - 1) / 2;
 70     line.position.x -= (x - 1) / 2;
 71     line.isVisible=false//非選中狀態邊線不可見
 72 
 73     this.mesh=new BABYLON.MeshBuilder.CreateBox(("card_" +this.name),{width:x-1,height:y-1,depth:0.005},this.scene);
 74     this.mesh.renderingGroupId = 0;//創建一個隱形的盒子做爲卡牌正反面網格的父網格
 75     this.mesh.position=param.position;//能夠經過點擊這個盒子來選擇卡片,也能夠爲這個盒子綁定物理引擎
 76     this.mesh.rotation=param.rotation;
 77     this.mesh.scaling=param.scaling;
 78     this.cardf = cardf;
 79     this.cardb = cardb;
 80     this.line = line;
 81     this.path_line=path_line;
 82     this.arr_path_line=line.getVerticesData(BABYLON.VertexBuffer.PositionKind,false);
 83     cardf.parent = this.mesh;
 84     cardb.parent = this.mesh;
 85     line.parent = this.mesh;
 86     this.mesh.card = this;
 87     //this.mesh.parent=mesh_arr_cards;//按照高內聚低耦合的規則,這個設定不該該放在角色類內部
 88     //暫時使用16:9的高寬設計
 89     var mesh_mainpic=new BABYLON.MeshBuilder.CreateGround(this.name+"mesh_mainpic",{width:8.4,height:9},scene);
 90     mesh_mainpic.parent=this.mesh;//承載正面圖片的網格
 91     mesh_mainpic.position=new BABYLON.Vector3(0,2.8,-0.01);
 92     var mat_mainpic = new BABYLON.StandardMaterial(this.name+"mat_mainpic", this.scene);//測試用卡片紋理
 93     mat_mainpic.diffuseTexture = new BABYLON.Texture(this.imagemain, this.scene);//地面的紋理貼圖
 94     mat_mainpic.diffuseTexture.hasAlpha = false;
 95     mat_mainpic.backFaceCulling = true;
 96     mat_mainpic.useLogarithmicDepth=true;//雖然還不徹底理解爲何,可是這種深度測試方式可以避免「Z-fighting」
 97     mat_mainpic.freeze();
 98     mesh_mainpic.material=mat_mainpic;
 99     mesh_mainpic.renderingGroupId=2;
100     mesh_mainpic.rotation.x=-Math.PI/2;
101     mesh_mainpic.isPickable=false;
102 
103     var mesh_comment=new BABYLON.MeshBuilder.CreateGround(this.name+"mesh_comment",{width:6,height:4.8},scene);
104     mesh_comment.parent=this.mesh;//承載正面文字的網格
105     mesh_comment.position=new BABYLON.Vector3(0,-4.6,-0.01);
106     mesh_comment.renderingGroupId=2;
107     var mat_comment = new BABYLON.StandardMaterial(this.name+"mat_comment", scene);
108     var texture_comment= new BABYLON.DynamicTexture(this.name+"texture_comment", {width:300, height:240}, scene);
109     mat_comment.diffuseTexture =texture_comment;//使用基於canvas的動態紋理顯示文字
110     mat_comment.useLogarithmicDepth=true;
111     mesh_comment.material = mat_comment;
112     mesh_comment.rotation.x=-Math.PI/2;
113     mesh_comment.isPickable=false;
114     var context_comment = texture_comment.getContext();//獲取canvas的上下文
115     context_comment.fillStyle="#0000ff";//使用html5canvas方法
116     context_comment.fillRect(1,1,150,120);
117     context_comment.fillStyle="#ffffff";
118     context_comment.font="bold 32px monospace";
119     newland.canvasTextAutoLine(this.str_comment,context_comment,1,30,35,34);
120     texture_comment.update();//修改canvas後更新動態紋理
121 }

  在繪製卡面時遇到幾個問題:

  a、卡牌的邊線與被選中效果設計

  原計劃使用Babylon.js內置的線段系統繪製卡牌邊線,而後對卡牌的正面使用內置的「邊緣高光」功能表示卡片被選中。但實際測試時發現內置的3D線段沒法修改線寬(鎖定爲1像素),因此改用細長的圓柱體網格(管道網格)表明線段。(邊緣高光和動態紋理的官方文檔中英對照能夠在http://down.51cto.com/data/2450646下載)

  生成邊線路徑的代碼以下:

 1 //邊線軌跡
 2 CardMesh.prototype.make_line=function(vertexData,x,y)
 3 {
 4     var path_line=[];
 5     //找邊線上的全部點
 6     for(var i=0;i<x-1;i++)
 7     {
 8         path_line.push(new BABYLON.Vector3(vertexData.positions[i*3],vertexData.positions[i*3+1],vertexData.positions[i*3+2]));
 9     }
10     for(var i=0;i<y-1;i++)
11     {
12         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]));
13     }
14     for(var i=x-1;i>0;i--)
15     {
16         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]));
17     }
18     for(var i=y-1;i>=0;i--)
19     {
20         path_line.push(new BABYLON.Vector3(vertexData.positions[i*x*3],vertexData.positions[i*x*3+1],vertexData.positions[i*x*3+2]));
21     }
22     return path_line;
23 }

 

  Babylon.js內置的邊緣高光功能也不盡如人意,一是內部圖片網格和文字網格遮擋銅片(邊框)的邊緣也出現了高光,二是高光層與圖片網格和文字網格發生相似深度緩存異常(z-fighting)(後面討論)的現象。

  內部網格的邊緣也出現了高亮:

  z-fighting效果:

 

  因此改成對做爲邊框的管道網格設置邊緣高亮,可是不太清楚如何控制高亮範圍的大小。

  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中自動換行書寫文字的方法:  

 1 //向一個canvas上下文裏自動換行的插入文字(來自網上)
 2 newland.canvasTextAutoLine=function(str,ctx,initX,initX2,initY,lineHeight){
 3     //var ctx = canvas.getContext("2d");
 4     var lineWidth = 0;
 5     var canvasWidth = ctx.canvas.width;
 6     var lastSubStrIndex= 0;
 7     for(let i=0;i<str.length;i++){
 8         lineWidth+=ctx.measureText(str[i]).width;
 9         if(lineWidth>canvasWidth-initX2){//減去initX,防止邊界出現的問題
10             ctx.fillText(str.substring(lastSubStrIndex,i),initX,initY);
11             initY+=lineHeight;
12             lineWidth=0;
13             lastSubStrIndex=i;
14         }
15         if(i==str.length-1){
16             ctx.fillText(str.substring(lastSubStrIndex,i+1),initX,initY);
17         }
18     }
19 }

  不知道是什麼緣由,這個方法運行的效果和網上的例子存在誤差,只好加一些偏移量手動微調。

二、卡牌的處理

  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的過程與此相反。

 1 function HandCard(flag)//用動畫方式表現手牌的「展開和收攏」
 2 {
 3     var pos1,pos2;
 4     MyGame.flag_view="first_pick";
 5     if(flag==0)//將手牌從後面推到前面
 6     {
 7         pos1=new BABYLON.Vector3(0,0,0);
 8         pos2=new BABYLON.Vector3(0,-2,16);
 9         var animation3=new BABYLON.Animation("ani_HandCard0","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
10         var keys1=[{frame:0,value:pos1},{frame:15,value:pos2}];
11         animation3.setKeys(keys1);
12         mesh_arr_cards.animations.push(animation3);
13         scene.beginAnimation(mesh_arr_cards, 0, 15, false,1,function(){
14             mesh_arr_cards.position=new BABYLON.Vector3(0,0,0);//動畫執行結束時執行的函數
15             mesh_arr_cards.parent=MyGame.player.mesh.ballman.handpoint;
16             MyGame.flag_view="first_pick";
17             MyGame.UiPanelr.button1.isVisible=true;
18             MyGame.UiPanelr.button2.isVisible=true;
19         });
20     }
21     else if(flag==1)//將手牌從前面拉到後面
22     {
23         pos1=new BABYLON.Vector3(0,0,0);
24         pos2=new BABYLON.Vector3(0,2,-16);
25         var animation3=new BABYLON.Animation("ani_HandCard1","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
26         var keys1=[{frame:0,value:pos1},{frame:15,value:pos2}];
27         animation3.setKeys(keys1);
28         mesh_arr_cards.animations.push(animation3);
29         scene.beginAnimation(mesh_arr_cards, 0, 15, false,1,function(){
30             mesh_arr_cards.position=new BABYLON.Vector3(0,0,0);
31             mesh_arr_cards.parent=MyGame.player.mesh.ballman.backview;
32             MyGame.flag_view="first_lock";
33         });
34     }
35 }

  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()爲每張卡牌安排新的位置:

 1 var arr_cardgroup=[{},{},{},{},{}];//五個分組的成員狀況
 2 var arr_mesh_groupicon=[];//在每一組分組元素前顯示組號
 3 //根據mesh_arr_cards._children和arr_cardgroup進行排序
 4 function SortCard()
 5 {
 6     var arr_mycards=mesh_arr_cards._children;
 7     var len=arr_mycards.length;
 8     var lenx = 10;//每一行的元素個數
 9     var leny = 4;//一頁顯示的最多有2行
10     var count=0;//記錄元素位置佔用了多少個
11     var widthp = 0.9;//每一個卡片通過縮放後的實際寬度
12     var heightp = 1.6;//每一張卡牌的實際高度
13     var marginx = 0.2;//x方向間隙大小
14     var marginy = 0.2;//y方向間隙大小
15     var len2=arr_cardgroup.length;
16     for(var i=0;i<len2;i++)//先繪製分組的元素
17     {
18         var obj=arr_cardgroup[i];
19         var flag_icon=0;//是否已經放置了標記
20         var x=0,y=0;
21         for(key in obj)
22         {
23             x = count % lenx;//從左往右數的索引
24             y = Math.floor(count / lenx);//從上往下數的索引
25             var posx = (x - lenx / 2) * (widthp + marginx);//根據索引算出位置
26             var posy = -(y - leny / 2) * (heightp + marginy) - 0.2;
27             if(flag_icon==0)//還未放置標記
28             {//則將這個小組的標記設爲這個小組的第一張卡牌的子元素
29                 //arr_mesh_groupicon[i].position=new BABYLON.Vector3(posx-1.5,posy,0);
30                 arr_mesh_groupicon[i].parent=obj[key].mesh;
31                 arr_mesh_groupicon[i].isVisible=true;
32                 flag_icon=1;
33             }
34             obj[key].mesh.position=new BABYLON.Vector3(posx,posy,0);
35             count++;//表示又佔用了一個空位
36         }
37         if(flag_icon==0)//若是最後也每放置標記,說明這個分組沒有元素,將分組標記撤除
38         {
39             arr_mesh_groupicon[i].isVisible=false;
40         }
41         else
42         {//若是用到了這個分組
43             count=(y+1)* lenx;//空位補齊,每一個分組都另起一行
44         }
45     }
46     for(var i=0;i<len;i++)//處理小隊之外的其餘卡牌
47     {
48         var mesh=arr_mycards[i];
49         if(mesh.card.num_group==999)//處理沒有分組的元素
50         {
51             var x = count % lenx;//從左往右數的索引
52             var y = Math.floor(count / lenx);//從上往下數的索引
53             var posx = (x - lenx / 2) * (widthp + marginx);
54             var posy = -(y - leny / 2) * (heightp + marginy) - 0.2;
55             mesh.position=new BABYLON.Vector3(posx,posy,0);
56             count++;
57         }
58 
59     }
60 }

  C、卡牌的選取效果

  卡牌被選中時須要高亮邊緣,取消選中時解除高亮:

 1 function getPicked(card)
 2 {//將卡片標識爲選中狀態,設置高亮邊框,而且將它做爲第一個選中點
 3     card.line.isVisible=true;
 4     MyGame.hl.addMesh(card.line,card.linecolor);
 5     MyGame.hl.addMesh(card.mesh,card.linecolor);//mesh不可見則不會生成對應高光層
 6     //card.line.width=1000;
 7     //card.line=BABYLON.MeshBuilder.CreateTube(card.line.name, {path: card.path_line, radius:0.2,updatable:true,instance:card.line}, scene);
 8     card.isPicked=true;
 9     //card.pickindex=arr_pickedCards.length;//
10 }
11 function noPicked(card)
12 {
13     card.line.isVisible=false;
14     MyGame.hl.removeMesh(card.line);
15     MyGame.hl.removeMesh(card.mesh);
16     //card.line.width=100;
17     //card.line=BABYLON.MeshBuilder.CreateTube(card.line.name, {path: card.path_line, radius:0.05,updatable:true,instance:card.line}, scene);
18     card.isPicked=false;
19 }

  點擊被選中的卡牌則把卡牌放大,其原理與前面的移入移出手牌相同:

 1 var card_Closed=null;
 2 function GetCardClose(card)//將卡牌拉近
 3 {
 4     MyGame.flag_view="first_ani";
 5     if(card_Closed)//若是已經有一個拉近的卡片
 6     {
 7         var animation1=new BABYLON.Animation("animation1","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
 8         var keys1=[{frame:0,value:card_Closed.mesh.position.clone()},{frame:15,value:card_Closed.oldpositon}];
 9         animation1.setKeys(keys1);
10         card_Closed.mesh.animations.push(animation1);
11         var animation2=new BABYLON.Animation("animation2","scaling",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
12         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)}];
13         animation2.setKeys(keys2);
14         card_Closed.mesh.animations.push(animation2);
15         scene.beginAnimation(card_Closed.mesh, 0, 15, false,1,function(){//能夠在一個動畫結束時再啓動另外一個動畫
16             card_Closed=card;
17             card.oldpositon=card.mesh.position.clone();
18             var animation3=new BABYLON.Animation("animation3","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
19             var keys1=[{frame:0,value:card_Closed.mesh.position.clone()},{frame:15,value:new BABYLON.Vector3(0,0,-0.5)}];
20             animation3.setKeys(keys1);
21             card_Closed.mesh.animations.push(animation3);
22             var animation4=new BABYLON.Animation("animation4","scaling",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
23             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)}];
24             animation4.setKeys(keys2);
25             card_Closed.mesh.animations.push(animation4);
26             scene.beginAnimation(card_Closed.mesh, 0, 15, false,1,function(){
27 
28                 MyGame.flag_view="first_pick";
29             });
30         });
31 
32     }
33     else
34     {
35         card_Closed=card;
36         card.oldpositon=card.mesh.position.clone();
37         var animation3=new BABYLON.Animation("animation3","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
38         var keys1=[{frame:0,value:card_Closed.mesh.position.clone()},{frame:15,value:new BABYLON.Vector3(0,0,-0.5)}];
39         animation3.setKeys(keys1);
40         card_Closed.mesh.animations.push(animation3);
41         var animation4=new BABYLON.Animation("animation4","scaling",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
42         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)}];
43         animation4.setKeys(keys2);
44         card_Closed.mesh.animations.push(animation4);
45         scene.beginAnimation(card_Closed.mesh, 0, 15, false,1,function(){
46             MyGame.flag_view="first_pick";
47         });
48     }
49 }

  D、卡牌的多選

  參考Windows系統的文件多選編寫了卡牌多選功能:

  1 //對已經創建的卡片的各類處理方法放在這裏
  2 var arr_pickedCards=[];
  3 function PickCard(card)
  4 {
  5     if(card_Closed)//若是有拉近顯示的卡片,則先把他恢復原樣
  6     {
  7         MyGame.flag_view="first_ani";
  8         var animation1=new BABYLON.Animation("animation1","position",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
  9         var keys1=[{frame:0,value:card_Closed.mesh.position.clone()},{frame:15,value:card_Closed.oldpositon}];
 10         animation1.setKeys(keys1);
 11         card_Closed.mesh.animations.push(animation1);
 12         var animation2=new BABYLON.Animation("animation2","scaling",30,BABYLON.Animation.ANIMATIONTYPE_VECTOR3,BABYLON.Animation.ANIMATIONLOOPMODE_CONSTANT);
 13         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)}];
 14         animation2.setKeys(keys2);
 15         card_Closed.mesh.animations.push(animation2);
 16         scene.beginAnimation(card_Closed.mesh, 0, 15, false,1,function(){
 17             card_Closed=null;
 18             MyGame.flag_view="first_pick";
 19         });
 20         return;
 21     }
 22 
 23     var len=arr_pickedCards.length;
 24     var arr_state=MyGame.arr_keystate;
 25     var arr_mycards=mesh_arr_cards._children;//這個數組裏的元素都是網格
 26     var len2=arr_mycards.length;
 27     var count=0;//加入分組特性後排序也要修改一下
 28     var len3=arr_cardgroup;
 29     for(var i=0;i<len3;i++)//在開始選擇以前根據排序結果,爲每張卡牌分配一個索引
 30     {
 31         for(key in arr_cardgroup[i])
 32         {
 33             arr_cardgroup[i][key].index=count;
 34             count++;
 35         }
 36     }
 37     for(var i=0;i<len2;i++)
 38     {
 39         arr_mycards[i].card.index=count;
 40         count++;
 41     }
 42 
 43     if(card.isPicked)
 44     //若是目前已經選中這個卡片,
 45         // 若是有多個選中卡片,若是當前按住了Ctrl,則取消它的選中(alt留着用來切換視角)
 46             //若是按住了shift,則將shift選擇的區域截斷到這裏(容許同時按下?)
 47             // ,若是沒有按住space或shift,則取消除它之外的全部選中並拉近
 48     // 若是隻有這一個選中卡片,則放大它
 49     {
 50         if(len>1)
 51         {
 52             if(arr_state[16]==1)//按着shift
 53             {
 54                 if(card_firstpick)//若是已經選定過一個卡片,將首選卡片和這個卡片之間的全部卡片選定
 55                 {//card_firstpick是「第一個被選中的卡片」也稱爲「首選卡片」
 56                     if(card_firstpick.index>card.index)
 57                     {
 58                         for(var i=0;i<len2;i++)
 59                         {
 60                             var card0=arr_mycards[i].card;
 61                             if(i<=card_firstpick.index&&i>=card.index)
 62                             {
 63                                 if(i!=card_firstpick.index)//首選元素就不向選取數組裏放了
 64                                 {
 65                                     //選中選取範圍內的全部元素
 66                                     getPicked(card0);
 67                                     //card_firstpick=card;
 68                                     arr_pickedCards.push(card0);
 69                                 }
 70                             }
 71                             else
 72                             {//刪除選取範圍外的全部已選中元素
 73                                 if(card0.isPicked)
 74                                 {
 75                                     noPicked(card0);//
 76                                     var len3=arr_pickedCards.length;
 77                                     for(var j=0;j<len3;j++){//從選取數組中找到這個元素,並刪除它
 78                                         if(arr_pickedCards[j].mesh.name==card0.mesh.name)
 79                                         {
 80                                             arr_pickedCards.splice(j,1);
 81                                             break;
 82                                         }
 83                                     }
 84                                 }
 85                             }
 86                         }
 87                     }
 88                     else if(card_firstpick.index<card.index)//
 89                     {
 90                         for(var i=0;i<len2;i++)
 91                         {
 92                             var card0=arr_mycards[i].card;
 93                             if(i>=card_firstpick.index&&i<=card.index)
 94                             {
 95                                 if(i!=card_firstpick.index)//首選元素就不向選取數組裏放了
 96                                 {
 97                                     //選中選取範圍內的全部元素
 98                                     getPicked(card0);
 99                                     //card_firstpick=card;
100                                     arr_pickedCards.push(card0);
101                                 }
102                             }
103                             else
104                             {//刪除選取範圍外的全部已選中元素
105                                 if(card0.isPicked)
106                                 {
107                                     noPicked(card0);//
108                                     var len3=arr_pickedCards.length;
109                                     for(var j=0;j<len3;j++){//從選取數組中找到這個元素,並刪除它
110                                         if(arr_pickedCards[j].mesh.name==card0.mesh.name)
111                                         {
112                                             arr_pickedCards.splice(j,1);
113                                             break;
114                                         }
115                                     }
116                                 }
117                             }
118                         }
119                     }
120                     else if(card_firstpick.index==card.index)
121                     {
122                         GetCardClose(card);//將這張卡片拿近
123                         //同時釋放掉全部被選中的卡片
124                         for(var i=0;i<len;i++)
125                         {
126                             var card0=arr_pickedCards[i];
127                             noPicked(card0);
128                         }
129                         arr_pickedCards=[];
130                         card_firstpick=null;
131                     }
132                 }
133             }
134             if(arr_state[17]==1)//ctrl
135             {//取消這張卡片的選中
136                 noPicked(card);
137                 //var len3=arr_pickedCards.length;
138                 for(var j=0;j<len;j++){//從選取數組中找到這個元素,並刪除它
139                     if(arr_pickedCards[j].mesh.name==card.mesh.name)
140                     {
141                         arr_pickedCards.splice(j,1);
142                         break;
143                     }
144                 }
145                 card_firstpick=arr_pickedCards[arr_pickedCards.length-1];
146             }
147             if(arr_state[17]!=1&&arr_state[16]!=1)
148             {
149                 GetCardClose(card);//將這張卡片拿近
150                 //同時釋放掉全部被選中的卡片
151                 for(var i=0;i<len;i++)
152                 {
153                     var card0=arr_pickedCards[i];
154                     noPicked(card0);
155                 }
156                 arr_pickedCards=[];
157                 card_firstpick=null;
158             }
159         }
160         else//目前只有這一張卡片被選中,而後點擊了他
161         {
162             GetCardClose(card);
163             noPicked(card);
164             arr_pickedCards=[];
165             card_firstpick=null;
166         }
167     }
168     else    //這張卡片尚未被選中
169     {
170         if(len>0)//還有其餘被選中的卡片
171         {
172             if(arr_state[16]==1)//按着shift
173             {
174                 if(card_firstpick)//若是已經選定過一個卡片,將首選卡片和這個卡片之間的全部卡片選定
175                 {//若是選取數組不空則必定有首選元素??
176 
177                     if(card_firstpick.index>card.index)
178                     {
179                         for(var i=0;i<len2;i++)
180                         {
181                             var card0=arr_mycards[i].card;
182                             if(i<=card_firstpick.index&&i>=card.index)
183                             {
184                                 if(i!=card_firstpick.index)//首選元素就不向選取數組裏放了
185                                 {
186                                     //選中選取範圍內的全部元素
187                                     getPicked(card0);
188                                     //card_firstpick=card;
189                                     arr_pickedCards.push(card0);
190                                 }
191                             }
192                             else
193                             {//刪除選取範圍外的全部已選中元素
194                                 if(card0.isPicked)
195                                 {
196                                     noPicked(card0);//
197                                     var len3=arr_pickedCards.length;
198                                     for(var j=0;j<len3;j++){//從選取數組中找到這個元素,並刪除它
199                                         if(arr_pickedCards[j].mesh.name==card0.mesh.name)
200                                         {
201                                             arr_pickedCards.splice(j,1);
202                                             break;
203                                         }
204                                     }
205                                 }
206                             }
207                         }
208                     }
209                     else if(card_firstpick.index<card.index)//由於card是未選中的因此card_firstpick.index與card.index不會相等
210                     {
211                         for(var i=0;i<len2;i++)
212                         {
213                             var card0=arr_mycards[i].card;
214                             if(i>=card_firstpick.index&&i<=card.index)
215                             {
216                                 if(i!=card_firstpick.index)//首選元素就不向選取數組裏放了
217                                 {
218                                     //選中選取範圍內的全部元素
219                                     getPicked(card0);
220                                     //card_firstpick=card;
221                                     arr_pickedCards.push(card0);
222                                 }
223                             }
224                             else
225                             {//刪除選取範圍外的全部已選中元素
226                                 if(card0.isPicked)
227                                 {
228                                     noPicked(card0);//
229                                     var len3=arr_pickedCards.length;
230                                     for(var j=0;j<len3;j++){//從選取數組中找到這個元素,並刪除它
231                                         if(arr_pickedCards[j].mesh.name==card0.mesh.name)
232                                         {
233                                             arr_pickedCards.splice(j,1);
234                                             break;
235                                         }
236                                     }
237                                 }
238                             }
239                         }
240                     }
241                 }
242                 else
243                 {//理論上講,這裏不會進入
244                     getPicked(card);
245                     card_firstpick=card;
246                     arr_pickedCards.push(card);
247                 }
248             }
249             if(arr_state[17]==1)//Ctrl
250             {
251                 getPicked(card);
252                 card_firstpick=card;
253                 arr_pickedCards.push(card);
254             }
255             if(arr_state[17]!=1&&arr_state[16]!=1)
256             {//在沒有按下shift或者Ctrl時,點擊一個未選中的卡片,則釋放之前選中的全部卡片,而後選中這個
257                 for(var i=0;i<len;i++)
258                 {
259                     var card0=arr_pickedCards[i];
260                     noPicked(card0);
261                 }
262                 arr_pickedCards=[];
263                 card_firstpick=null;
264                 getPicked(card);
265                 card_firstpick=card;
266                 arr_pickedCards.push(card);
267             }
268         }
269         else//沒有其餘被選中的卡片,這應該是最簡單的狀況?
270         {
271             //card.getPicked();
272             getPicked(card);
273             card_firstpick=card;
274             arr_pickedCards.push(card);
275         }
276     }
277 }

  E、卡牌的上下滾動

  只是給FullUI.js中的兩個按鈕添加了響應方法:

 1 function ScrollUporDown(flag,heightp,row)//flag0表示向上,1表示向下,heightp表示每行滾動距離,row表示滾動行數
 2 {
 3     //var poshand=MyGame.player.mesh.ballman.handpoint._absolutePosition;//此時手的位置
 4     if(flag==0)
 5     {
 6         var arr_mycards=mesh_arr_cards._children;
 7         var posy=arr_mycards[arr_mycards.length-1].position.y;//找到位置最低的卡牌
 8         if(mesh_arr_cards.position.y<(0-posy-row*heightp))//不能向上滾動太多,不徹底精確的限定,但也夠了
 9         {
10             mesh_arr_cards.position.y+=row*heightp;
11         }
12 
13     }
14     else if(flag==1)
15     {
16         if(mesh_arr_cards.position.y>=row*heightp)
17         {
18             mesh_arr_cards.position.y-=row*heightp
19         }
20 
21     }
22 }

   因而完成了最初演示的功能。

4、總結

  文中的框架代碼部分參考了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格式的方法(修改自百度經驗):

  一、打開chrome瀏覽器,地址欄輸入:chrome://flags/ 後回車

  二、Ctrl+F 搜索 MHTML,並找到 將網頁另存爲MHTML,點擊 啓動

  三、啓用後,重啓chrome,Ctrl+S便可選擇保存爲mhtml格式

相關文章
相關標籤/搜索