在WebGL場景中導入多個Babylon骨骼模型,在局域網用WebSocket實現多用戶交互控制。html
首先是場景截圖:java
上圖在場景中導入一個Babylon骨骼模型,使用asdw、空格、鼠標控制加速度移動,在移動時播放骨骼動畫。jquery
上圖在場景中加入更多的骨骼模型(兔子),兔子感知到人類接近後會加速遠離人類。git
上圖,一個局域網中的新玩家進入場景,(他們頭上的數字是WebSocket分配的session id),兔子們受到0和1的疊加影響。github
具體實現:web
1、工程結構:編程
前臺WebStorm工程:canvas
其中map.jpg是地形高度圖,tree.jpg不是樹而是地面泥土的紋理。。。數組
LIB文件夾裏是引用的第三方庫(babylon.max.js是2.4版),MYLIB文件夾裏是我本身編寫或整理修改的庫,PAGE裏是專用於此網頁的腳本文件瀏覽器
其中FileText.js是js前臺文件處理庫(這裏只用到了其中的產生日期字符串函數)
MoveWeb.js是加速度計算庫
Sdyq.js裏是對物體對象的定義和操做監聽
Player.js裏是繼承了物體對象的玩家對象和動物對象的定義
utils是一些其餘工具
View是頁面控制庫
MODEL文件夾裏是人物和兔子的骨骼模型文件。
後臺MyEclipse工程:
使用JDK1.7,由於Tomcat v8.0裏包含了WebSocket所用的庫,因此不須要引入額外jar包,只寫了一個類。
2、基本場景構建和骨骼模型導入:
html頁面文件:
1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="UTF-8"> 5 <title>使用websocket聯網進行數據傳遞,這個節點應該既能夠作主機也能夠加入他人的主機</title> 6 </head> 7 <body> 8 <div id="all_base" style="position:fixed;top:0px;left: 0px;"> 9 <div id="div_canvas" style="float: left;width: 75%;border: 1px solid"> 10 <canvas id="renderCanvas" style="width: 100%;height: 100%"></canvas> 11 </div> 12 <div id="div_log" style="float: left;border: 1px solid;overflow-y: scroll"> 13 </div> 14 <div id="div_bottom" style="float: left;width: 100%;height: 100px;padding-top: 10px;padding-left: 10px"> 15 <input style="width: 200px" id="str_ip" value="localhost"> 16 <input id="str_name"> 17 <button id="btn_create" onclick="createScene()" disabled=true>啓動場景</button> 18 <button id="btn_connect" onclick="Connect()" >websocket鏈接</button> 19 <button id="btn_close" onclick="Close()" disabled=true>關閉鏈接</button> 20 <span id="str_id" style="display: inline-block"></span><br><br> 21 <input style="width: 400px" id="str_message"> 22 <button id="btn_send" onclick="Send()">發送</button> 23 </div> 24 </div> 25 <script src="../JS/LIB/babylon.max.js"></script> 26 <script src="../JS/MYLIB/View.js"></script> 27 <script src="../JS/LIB/jquery-1.11.3.min.js"></script> 28 <script src="../JS/MYLIB/FileText.js"></script> 29 <script src="../JS/MYLIB/Sdyq.js"></script> 30 <script src="../JS/MYLIB/player.js"></script> 31 <script src="../JS/MYLIB/MoveWeb.js"></script> 32 <script src="../JS/MYLIB/utils.js"></script> 33 <script src="../JS/PAGE/scene_link.js"></script> 34 <script src="../JS/PAGE/WebSocket.js"></script> 35 </body> 36 <script> 37 var username=""; 38 window.onload=BeforeLog; 39 window.onresize=Resize_Pllsselect; 40 function BeforeLog() 41 { 42 Resize_Pllsselect(); 43 //DrawYzm(); 44 //createScene(); 45 } 46 var str_log=document.getElementById("div_log"); 47 function Resize_Pllsselect() 48 { 49 var size=window_size(); 50 document.getElementById("all_base").style.height=(size.height+"px"); 51 document.getElementById("all_base").style.width=(size.width+"px"); 52 document.getElementById("div_canvas").style.height=(size.height-100+"px"); 53 str_log.style.height=(size.height-100+"px"); 54 str_log.style.width=((size.width/4)-4+"px"); 55 if(engine!=undefined) 56 { 57 engine.resize(); 58 } 59 } 60 61 var state="offline"; 62 63 var arr_myplayers=[]; 64 var arr_webplayers=[]; 65 var arr_animals=[]; 66 var arr_tempobj=[];//暫存對象初始化信息 67 var tempobj; 68 69 var canvas = document.getElementById("renderCanvas"); 70 var ms0=0;//上一時刻毫秒數 71 var mst=0;//下一時刻毫秒數 72 var schange=0;//秒差 73 74 var skybox, 75 scene, 76 sceneCharger = false, 77 meshOctree, 78 cameraArcRotative = [],//弧形旋轉相機列表 79 octree; 80 var engine; 81 var shadowGenerator ; 82 83 </script> 84 </html>
其中包含對頁面尺寸大小變化的響應和一些全局變量的定義
scene_link.js文件中包含場景的構建和模型導入:
一、在createScene()方法的開頭部分創建了一個基本的PlayGround場景:
1 engine = new BABYLON.Engine(canvas, true); 2 engine.displayLoadingUI(); 3 scene = new BABYLON.Scene(engine); 4 5 //在場景中啓用碰撞檢測 6 scene.collisionsEnabled = true; 7 //scene.workerCollisions = true;//啓動webworker進程處理碰撞,確實能夠有效使用多核運算,加大幀數!! 8 //可是worker是異步運算的,其數據傳輸策略會致使movewithcollition執行順序與指望的順序不符 9 10 //定向光照 11 var LightDirectional = new BABYLON.DirectionalLight("dir01", new BABYLON.Vector3(-2, -4, 2), scene); 12 LightDirectional.diffuse = new BABYLON.Color3(1, 1, 1);//散射顏色 13 LightDirectional.specular = new BABYLON.Color3(0, 0, 0);//鏡面反射顏色 14 LightDirectional.position = new BABYLON.Vector3(250, 400, 0); 15 LightDirectional.intensity = 1.8;//強度 16 shadowGenerator = new BABYLON.ShadowGenerator(1024, LightDirectional);//爲該光源創建陰影生成器,用在submesh上時一直在報錯,不知道爲了什麼 17 18 //弧形旋轉相機 19 cameraArcRotative[0] = new BABYLON.ArcRotateCamera("CameraBaseRotate", -Math.PI/2, Math.PI/2.2, 12, new BABYLON.Vector3(0, 5.0, 0), scene); 20 cameraArcRotative[0].wheelPrecision = 15;//鼠標滾輪? 21 cameraArcRotative[0].lowerRadiusLimit = 2; 22 cameraArcRotative[0].upperRadiusLimit = 22; 23 cameraArcRotative[0].minZ = 0; 24 cameraArcRotative[0].minX = 4096; 25 scene.activeCamera = cameraArcRotative[0]; 26 cameraArcRotative[0].attachControl(canvas);//控制關聯 27 28 //地面 29 //name,url,width,height,subdivisions,minheight,maxheight,updateble,onready,scene 30 ground = BABYLON.Mesh.CreateGroundFromHeightMap("ground", "../IMAGE/map.jpg", 1000, 1000, 100, 0, 60, scene, true);//地面類型的網格 31 var groundMaterial = new BABYLON.StandardMaterial("groundMat", scene);//泥土材質 32 groundMaterial.diffuseTexture = new BABYLON.Texture("../IMAGE/tree.png", scene);//地面的紋理貼圖 33 groundMaterial.diffuseTexture.uScale = 50.0;//紋理重複效果 34 groundMaterial.diffuseTexture.vScale = 50.0; 35 ground.material = groundMaterial; 36 ground.checkCollisions = true;//檢測碰撞 37 ground.receiveShadows = true;//接收影子 38 39 //牆 40 var Mur = BABYLON.Mesh.CreateBox("Mur", 1, scene); 41 Mur.scaling = new BABYLON.Vector3(15, 6, 1); 42 Mur.position.y = 20; 43 Mur.position.z = 20; 44 Mur.checkCollisions = true;
其中各個方法的具體用法能夠參考官方的基礎教程
二、接下來是在場景中導入第一我的物的骨骼模型:
1 //角色導入,加載哪一個mesh、文件目錄、文件名、加入場景、回調函數 2 BABYLON.SceneLoader.ImportMesh("", "../MODEL/him/", "him.babylon", scene, function (newMeshes, particleSystems, skeletons) 3 {//載入完成的回調函數 4 var Tom=new Player; 5 var obj_p={};//初始化參數對象 6 obj_p.mesh=newMeshes[0];//網格數據 7 obj_p.scaling=new BABYLON.Vector3(0.05, 0.05, 0.05);//縮放 8 obj_p.position=new BABYLON.Vector3(-5.168, 30.392, -7.463);//位置 9 obj_p.rotation=new BABYLON.Vector3(0, 3.9, 0);// 旋轉 10 obj_p.checkCollisions=true;//使用默認的碰撞檢測 11 obj_p.ellipsoid=new BABYLON.Vector3(0.5, 1, 0.5);//碰撞檢測橢球 12 obj_p.ellipsoidOffset=new BABYLON.Vector3(0, 2, 0);//碰撞檢測橢球位移 13 obj_p.skeletonsPlayer=skeletons; 14 obj_p.methodofmove="controlwitha"; 15 obj_p.name=username; 16 obj_p.id=id; 17 obj_p.p1=""; 18 obj_p.p2="../MODEL/him/"; 19 obj_p.p3="him.babylon"; 20 var len=newMeshes.length;//對於複雜的模型來講newMeshes的其餘部分也必須保存下來 21 var arr=[]; 22 for(var i=1;i<len;i++) 23 { 24 arr.push(newMeshes[i]); 25 } 26 obj_p.submeshs=arr; 27 28 Tom.init( 29 obj_p 30 ); 31 arr_myplayers[username]=Tom; 32 33 if(state=="online") 34 { 35 var arr=[]; 36 arr.push("addnewplayer"); 37 arr.push(Tom.mesh.scaling.x); 38 arr.push(Tom.mesh.scaling.y); 39 arr.push(Tom.mesh.scaling.z); 40 arr.push(Tom.mesh.position.x); 41 arr.push(Tom.mesh.position.y); 42 arr.push(Tom.mesh.position.z); 43 arr.push(Tom.mesh.rotation.x); 44 arr.push(Tom.mesh.rotation.y); 45 arr.push(Tom.mesh.rotation.z); 46 arr.push(Tom.p1); 47 arr.push(Tom.p2); 48 arr.push(Tom.p3); 49 arr.push(Tom.meshname); 50 var dt=new Date(); 51 console.log(dt.getTime()+"send addnewplayer"+id); 52 doSend(arr.join("@")); 53 } 54 55 cameraArcRotative[0].alpha = -parseFloat(arr_myplayers[username].mesh.rotation.y) - 4.69;//初始化相機角度 56 57 });
其中BABYLON.SceneLoader.ImportMesh是一個異步的把服務器端場景文件導入本地內存的方法,第一個參數表示導入場景文件中的哪個Mesh,爲空表示都導入(一個場景文件裏可能包含多個模型,但該示例中的場景文件裏只有一個模型,因此也叫作模型文件),第二個參數是文件所在的相對路徑,第三個參數是文件名,第四個參數是文件加入的場景,第五個參數是導入完成後的回調函數。
回調函數的newMeshes參數是全部導入的Mesh組成的數組,skeletons參數是全部導入的骨骼動畫數組。事實上一個模型可能由多個mesh組合而成,好比示例中的him模型的newMeshes[0]只是一個空殼,newMeshes[1]到newMeshes[5]纔是模型各個部分的實際Mesh,後五個Mesh是newMeshes[0]的「submesh」,newMeshes[0]是後五個Mesh的parent,在理想狀況下這些Mesh之間的關係和Mesh與骨骼動畫(skeleton)之間的關係由Babylon引擎自動管理。
在回調函數中,定義Tom爲一個Player「類」對象,第五行定義的obj_p對象是Player對象的初始化參數對象,Player.init()方法定義在player.js文件中:
1 //玩家對象 2 Player=function() 3 { 4 sdyq.object.call(this); 5 } 6 Player.prototype=new sdyq.object(); 7 Player.prototype.init=function(param) 8 { 9 param = param || {}; 10 sdyq.object.prototype.init.call(this,param);//繼承原型的方法 11 this.flag_standonground=0;//是否接觸地面 12 this.keys={w:0,s:0,a:0,d:0,space:0,ctrl:0,shift:0};//按鍵是否保持按下,考慮到多客戶端並行,那麼勢必每一個player都有本身的keys!! 13 this.flag_runfast=1;//加快速度 14 this.name=param.name; 15 this.id=param.id; 16 this.p1=param.p1; 17 this.p2=param.p2; 18 this.p3=param.p3; 19 。。。
能夠看到Player對象繼承自sdyq.object對象,Player對象的原型是sdyq.object對象,在Player對象的init方法中,先初始化屬於原型的屬性,再初始化本身這個「類」新添加的屬性。
sdyq.object對象的定義在Sdyq.js文件中:
1 //物體自己的屬性和初始化 2 sdyq={};//3D引擎 3 sdyq.object=function() 4 {//在地面上加速度運動的物體 5 6 } 7 sdyq.object.prototype.init = function(param) 8 { 9 this.keys={w:0,s:0,a:0,d:0,space:0,ctrl:0,shift:0};//按鍵是否保持按下 10 this.witha0={forward:0,left:0,up:-9.82};//非鍵盤控制產生的加速度 11 this.witha={forward:0,left:0,up:-9.82};//環境加速度,包括地面阻力和重力,如今尚未風力 12 this.witha2={forward:0,left:0,up:0};//鍵盤控制加速度與物體自己加速度和非鍵盤控制產生的加速度合併後的最終加速度 13 this.v0={forward:0,left:0,up:0};//上一時刻的速度 14 this.vt={forward:0,left:0,up:0};//下一時刻的速度 15 this.vm={forward:15,backwards:5,left:5,right:5,up:100,down:100};//各個方向的最大速度 16 //this.flag_song=0;//是否接觸地面 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,left: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.mesh.scaling=param.scaling; 28 this.mesh.position=param.position; 29 this.mesh.rotation=param.rotation; 30 this.mesh.checkCollisions=param.checkCollisions; 31 this.mesh.ellipsoid=param.ellipsoid; 32 this.mesh.ellipsoidOffset=param.ellipsoidOffset; 33 this.meshname=this.mesh.name; 34 this.skeletonsPlayer=param.skeletonsPlayer||[]; 35 this.submeshs=param.submeshs; 36 this.ry0=param.mesh.rotation.y; 37 this.py0=param.mesh.position.y; 38 this.countstop=0;//記錄物體靜止了幾回,若是物體一直靜止就中止發送運動信息 39 40 this.PlayAnnimation = false; 41 42 this.methodofmove=param.methodofmove||""; 43 switch(this.methodofmove) 44 { 45 case "controlwitha": 46 { 47 window.addEventListener("keydown", onKeyDown, false);//按鍵按下 48 window.addEventListener("keyup", onKeyUp, false);//按鍵擡起 49 break; 50 } 51 default : 52 { 53 break; 54 } 55 } 56 }
sdyq.object對象的初始化方法中包含了對mesh姿態的詳細設定、對鍵盤操做的監聽設定和適用於加速度運動的各項參數設定,各類加速度運動的物體均可以用sdyq.object對象來擴展產生。
在Player對象的初始化方法中還爲每一個玩家添加了id顯示(頭上的那個數字):
1 //在玩家頭上顯示名字,clone時這個也會被clone過去,要處理一下!!!! 2 var lab_texture=new BABYLON.Texture.CreateFromBase64String(texttoimg2(this.id),"datatexture"+this.id,scene);//使用canvas紋理!! 3 var materialSphere1 = new BABYLON.StandardMaterial("texture1"+this.id, scene); 4 materialSphere1.diffuseTexture = lab_texture; 5 var plane = BABYLON.Mesh.CreatePlane("plane"+this.id, 2.0, scene, false, BABYLON.Mesh.FRONTSIDE); 6 //You can also set the mesh side orientation with the values : BABYLON.Mesh.FRONTSIDE (default), BABYLON.Mesh.BACKSIDE or BABYLON.Mesh.DOUBLESIDE 7 materialSphere1.diffuseTexture.hasAlpha = true;//應用紋理的透明度 8 9 plane.position=new BABYLON.Vector3(0,75,0);//其父元素應用過0.05之縮放,故而這裏位移量要*20 10 plane.rotation.y = Math.PI; 11 plane.scaling.x=20; 12 plane.scaling.y=4; 13 plane.parent=this.mesh; 14 15 plane.material=materialSphere1; 16 this.lab=plane;
在這裏使用了canvas現場產生紋理(術語叫「程序貼圖」),其中texttoimg2()方法的定義在utils.js文件中:
1 //把文字轉變爲圖片jpeg 2 function texttoimg(str) 3 { 4 var c=document.createElement("canvas"); 5 c.height=20; 6 c.width=100; 7 var context = c.getContext('2d'); 8 context.font="normal 15px sans-serif"; 9 context.clearRect(0, 0, canvas.width, canvas.height); 10 context.fillStyle="rgb(255,255,255)"; 11 context.fillRect(0,0,canvas.width,canvas.height); 12 context.fillStyle = "rgb(0,0,0)"; 13 context.textBaseline = 'top'; 14 context.fillText(str,(c.width-str.length*15)/2,0, c.width*0.9); 15 var str_src=c.toDataURL("image/jpeg"); 16 return str_src; 17 //return c; 18 } 19 //把文字轉變爲圖片PNG 20 function texttoimg2(str) 21 { 22 var c=document.createElement("canvas"); 23 c.height=20; 24 c.width=100; 25 var context = c.getContext('2d'); 26 context.font="normal 20px sans-serif"; 27 context.clearRect(0, 0, canvas.width, canvas.height); 28 //context.fillStyle="rgb(255,255,255)"; 29 //context.fillRect(0,0,canvas.width,canvas.height); 30 context.fillStyle = "rgb(255,255,255)"; 31 context.textBaseline = 'middle';// 32 context.fillText(str,(c.width-str.length*20)/2,10, c.width*0.9); 33 var str_src=c.toDataURL("image/png"); 34 return str_src; 35 //return c; 36 }
該代碼綜合網上多個教程修改而來,其中生成jpeg的難點在於canvas默認生成四通道圖像,而jpeg在去除透明度通道時會自動將透明度通道變成黑色,因而jpeg一片漆黑,解決方法是先畫一個不透明的白色矩形背景,擋住全部透明通道,再在白色背景上畫圖。
在模型導入完畢後把Tom設爲玩家列表對象arr_myplayers的一個屬性,若是當前玩家處於在線狀態,則還要把其加載狀態同步給其餘玩家,具體同步方式稍後介紹。
最後把玩家的相機定位到玩家模型的身後,作第三方跟隨視角狀。
3、加速度運動控制
在scene_link.js文件的中部能夠看到scene.registerBeforeRender()方法,這個方法的做用是在每次渲染前調用做爲它的參數的方法,咱們經過這個方法在每次渲染前對物體的下一步運動狀況進行計算:
1 scene.registerBeforeRender(function() 2 {//每次渲染前 3 if(scene.isReady() && arr_myplayers) 4 {//場景加載完畢 5 if(sceneCharger == false) { 6 engine.hideLoadingUI();//隱藏載入ui 7 sceneCharger = true; 8 } 9 if(ms0==0) 10 {//最開始,等一幀 11 ms0=new Date();//設置初始時間 12 schange=0;//初始化時間差 13 } 14 else 15 { 16 mst = new Date();//下一時刻 17 schange = (mst - ms0) / 1000; 18 ms0=mst;//時間越過 19 //對於這段時間內的每個物體 20 for (var key in arr_myplayers)//該客戶端所控制的物體 21 { 22 var obj = arr_myplayers[key]; 23 switch(obj.methodofmove) 24 { 25 case "controlwitha": 26 { 27 movewitha(obj); 28 //這裏加上dosend!!!!,原地不動也發送嗎? 29 if (state == "online") 30 { 31 if(obj.vmove.x==0&&obj.vmove.y==0&&obj.vmove.z==0&&obj.rychange==0) 32 {//若是位置和姿態不變 33 if(obj.countstop>0) 34 {//一直靜止則不發送運動信息 35 36 } 37 else 38 { 39 obj.countstop+=1; 40 //當前位置,當前角度,當前移動,當前姿態變化 41 var arr = []; 42 arr.push("updatemesh"); 43 arr.push(obj.mesh.position.x); 44 arr.push(obj.mesh.position.y); 45 arr.push(obj.mesh.position.z); 46 arr.push(obj.mesh.rotation.x); 47 arr.push(obj.mesh.rotation.y); 48 arr.push(obj.mesh.rotation.z); 49 arr.push(obj.vmove.x); 50 arr.push(obj.vmove.y); 51 arr.push(obj.vmove.z); 52 arr.push(obj.rychange); 53 doSend(arr.join("@")); 54 } 55 } 56 else 57 { 58 obj.countstop=0; 59 //當前位置,當前角度,當前移動,當前姿態變化 60 var arr = []; 61 arr.push("updatemesh"); 62 arr.push(obj.mesh.position.x); 63 arr.push(obj.mesh.position.y); 64 arr.push(obj.mesh.position.z); 65 arr.push(obj.mesh.rotation.x); 66 arr.push(obj.mesh.rotation.y); 67 arr.push(obj.mesh.rotation.z); 68 arr.push(obj.vmove.x); 69 arr.push(obj.vmove.y); 70 arr.push(obj.vmove.z); 71 arr.push(obj.rychange); 72 doSend(arr.join("@")); 73 } 74 } 75 76 if((obj.vmove.x!=0||obj.vmove.y!=0||obj.vmove.z!=0||obj.rychange!=0)&&obj.PlayAnnimation==false) 77 {//若是開始運動,啓動骨骼動畫 78 obj.PlayAnnimation=true; 79 obj.beginSP(0); 80 } 81 else if(obj.vmove.x==0&&obj.vmove.y==0&&obj.vmove.z==0&&obj.rychange==0&&obj.PlayAnnimation==true) 82 {//若是運動結束,關閉骨骼動畫 83 obj.PlayAnnimation=false; 84 scene.stopAnimation(obj.skeletonsPlayer[0]); 85 } 86 break; 87 } 88 default : 89 { 90 break; 91 } 92 } 93 } 94 。。。
這裏的意思是說若是玩家列表裏的玩家的運動方式(methodofmove)是"controlwitha",則使用movewitha(obj)方法計算其在這一時間段中的運動,固然,若是編寫出了其餘的運動方法也能夠相似的擴展進來。
movewitha(obj)方法定義在MoveWeb.js文件中:
一、初速度投影
1 function movewitha(obj)//地面上帶有加速度的運動,必須站在地上才能加速,與宇宙空間中的噴氣式加速度相比較 2 { 3 obj.ryt=obj.mesh.rotation.y; 4 obj.rychange=parseFloat(obj.ryt - obj.ry0); 5 obj.ry0=obj.ryt; 6 //將上一時刻的速度投影到下一時刻的座標裏 7 var v0t = {forward: 0, left: 0, up: 0}; 8 v0t.forward = obj.v0.forward * parseFloat(Math.cos(obj.rychange)) + (-obj.v0.left * parseFloat(Math.sin(obj.rychange))); 9 v0t.left = (obj.v0.forward * parseFloat(Math.sin(obj.rychange))) + (obj.v0.left * parseFloat(Math.cos(obj.rychange))); 10 v0t.up = obj.v0.up; 11 obj.v0 = v0t;
物體在這一小段時間內可能繞y軸轉過了必定角度,因此要把物體在上一時刻的自身座標系速度投影到通過變化以後的自身座標系中。
二、計算水平加速度,與水平位移
1 //計算水平加速度 2 if(obj.flag_standonground==1)//在地面上才能使用水平加速度 3 { 4 //移動速度產生的阻力,只考慮地面阻力,不考慮空氣阻力 5 if (obj.v0.forward == 0) { 6 obj.witha.forward = 0; 7 } 8 else if (obj.v0.forward > 0) { 9 obj.witha.forward = -0.5; 10 } 11 else { 12 obj.witha.forward = 0.5; 13 } 14 if (obj.v0.left == 0) { 15 obj.witha.left = 0; 16 } 17 else if (obj.v0.left > 0) { 18 obj.witha.left = -0.5; 19 } 20 else { 21 obj.witha.left = 0.5; 22 } 23 //最終加速度由環境加速度和物體自身加速度疊加而成 24 obj.witha2.forward = obj.witha.forward+obj.witha0.forward; 25 obj.witha2.left = obj.witha.left+obj.witha0.left; 26 //根據鍵盤操做設置加速度 27 //處理先後 28 if (obj.keys.w != 0) { 29 obj.witha2.forward += 5; 30 } 31 else if (obj.keys.s != 0) { 32 obj.witha2.forward -= 2; 33 } 34 //處理左右 35 if (obj.keys.a != 0 && obj.keys.d != 0) {//同時按下左右鍵則什麼也不作 36 37 } 38 else if (obj.keys.a != 0) { 39 obj.witha2.left += 2; 40 } 41 else if (obj.keys.d != 0) { 42 obj.witha2.left -= 2; 43 } 44 } 45 else 46 { 47 obj.witha2.forward=0; 48 obj.witha2.left=0; 49 } 50 //根據水平加速度計算水平運動 51 if(obj.witha2.forward!=0) 52 { 53 obj.vt.forward = obj.v0.forward + obj.witha2.forward * schange;//速度變化 54 if((0 < obj.vt.forward && obj.vt.forward < obj.vm.forward) || (0 > obj.vt.forward && obj.vt.forward > -obj.vm.backwards)) 55 {//在最大速度範圍內 56 obj.mchange.forward = obj.witha2.forward * schange * schange + obj.v0.forward * schange;//加速度產生的距離變化 57 } 58 else if (obj.vm.forward <= obj.vt.forward) {//超出最大速度則按最大速度算 59 obj.vt.forward = obj.vm.forward; 60 obj.mchange.forward = obj.vt.forward * schange; 61 } 62 else if (-obj.vm.backwards >= obj.vt.forward) { 63 obj.vt.forward = -obj.vm.backwards; 64 obj.mchange.forward = obj.vt.forward * schange; 65 } 66 } 67 else {//無加速度時勻速運動 68 obj.mchange.forward = obj.v0.forward * schange; 69 } 70 if(obj.witha2.left!=0) 71 { 72 obj.vt.left = obj.v0.left + obj.witha2.left * schange;//速度變化 73 if((0 < obj.vt.left && obj.vt.left < obj.vm.left) || (0 > obj.vt.left && obj.vt.left > -obj.vm.right)) 74 {//在最大速度範圍內 75 obj.mchange.left = obj.witha2.left * schange * schange + obj.v0.left * schange;//加速度產生的距離變化 76 } 77 else if (obj.vm.left <= obj.vt.left) { 78 obj.vt.left = obj.vm.left; 79 obj.mchange.left = obj.vt.left * schange; 80 } 81 else if (-obj.vm.right >= obj.vt.left) { 82 obj.vt.left = -obj.vm.right; 83 obj.mchange.left = obj.vt.left * schange; 84 } 85 } 86 else { 87 obj.mchange.left = obj.v0.left * schange; 88 }
三、計算垂直加速度、垂直位移:
1 //垂直加速度單獨計算 2 3 //正在下落,但沒有下落應有的距離 4 if(obj.v0.up<0&&obj.flag_standonground==0&&((obj.py0-obj.mesh.position.y)<(-obj.mchange.up)/5)) 5 { 6 obj.v0.up=0; 7 obj.flag_standonground=1;//表示接觸地面 8 obj.witha.up=-0.5;//考慮到下坡的存在,還要有一點向下的份量,使其可以沿地面向下但又不至於抖動過於劇烈 9 obj.vm.up=5; 10 obj.vm.down=5; 11 } 12 else if(obj.flag_standonground==1&&((obj.py0-obj.mesh.position.y)>(-obj.mchange.up)/5))//遇到了一個坑 13 { 14 obj.flag_standonground=0; 15 obj.witha.up=-9.82; 16 obj.vm.up=100; 17 obj.vm.down=100; 18 } 19 obj.witha2.up = obj.witha.up; 20 if (obj.witha2.up != 0&&(obj.flag_standonground==0||(obj.flag_standonground==1&&(obj.mchange.left!=0||obj.mchange.forward!=0)))) {//不在地面或者有水平位移才考慮上下加速移動 21 22 obj.vt.up = obj.v0.up + obj.witha2.up * schange;//速度變化 23 if ((0 < obj.vt.up && obj.vt.up < obj.vm.up) || (0 > obj.vt.up && obj.vt.up > -obj.vm.down)) { 24 obj.mchange.up = obj.witha2.up * schange * schange + obj.v0.up * schange;//加速度產生的距離變化 25 } 26 else if (obj.vm.up <= obj.vt.up) { 27 obj.vt.up = obj.vm.up; 28 obj.mchange.up = obj.vt.up * schange; 29 } 30 else if (-obj.vm.down >= obj.vt.up) { 31 obj.vt.up = -obj.vm.down; 32 obj.mchange.up = obj.vt.up * schange; 33 } 34 } 35 else { 36 obj.mchange.up = obj.v0.up * schange; 37 }
Babylon初級教程中提供了兩種現成的碰撞檢測方法,其中一種可以較精確的檢測到物體掉落在地面上,但不支持事件響應或者回調函數;另外一種支持事件響應,但物體的碰撞檢測邊界太過粗糙,沒法精確檢測碰撞。因此我只好用「有沒有在該方向上移動應有的距離」來暫時代替碰撞檢測。
四、應用位移:
1 //舊的當前速度沒用了,更新當前速度 2 obj.v0.forward = obj.vt.forward; 3 obj.v0.left = obj.vt.left; 4 obj.v0.up = obj.vt.up; 5 //取消過於微小的速度和位移 6 if (obj.v0.forward < 0.002 && obj.v0.forward > -0.002) { 7 obj.v0.forward = 0; 8 obj.mchange.forward=0; 9 } 10 if (obj.v0.left < 0.002 && obj.v0.left > -0.002) { 11 obj.v0.left = 0; 12 obj.mchange.left=0; 13 } 14 if (obj.v0.up < 0.002 && obj.v0.up > -0.002) { 15 obj.v0.up = 0; 16 obj.mchange.up=0; 17 } 18 if(obj.mchange.forward<0.002&& obj.mchange.forward > -0.002) 19 { 20 obj.mchange.forward=0; 21 } 22 if(obj.mchange.left<0.002&& obj.mchange.left > -0.002) 23 { 24 obj.mchange.left=0; 25 } 26 if(obj.mchange.up<0.002&& obj.mchange.up > -0.002) 27 { 28 obj.mchange.up=0; 29 } 30 //實施移動,將來要考慮把這個實施移動傳遞給遠方客戶端 31 obj.py0=obj.mesh.position.y; 32 var vectir1=(new BABYLON.Vector3(parseFloat(Math.sin(parseFloat(obj.mesh.rotation.y))) * obj.mchange.forward * obj.flag_runfast, 33 0, parseFloat(Math.cos(parseFloat(obj.mesh.rotation.y))) * obj.mchange.forward * obj.flag_runfast)).negate(); 34 var vectir2=new BABYLON.Vector3(-parseFloat(Math.cos(parseFloat(obj.mesh.rotation.y))) * obj.mchange.left * obj.flag_runfast, 35 0, parseFloat(Math.sin(parseFloat(obj.mesh.rotation.y))) * obj.mchange.left * obj.flag_runfast).negate(); 36 var vectir3=new BABYLON.Vector3(0, obj.mchange.up * obj.flag_runfast, 0); 37 obj.vmove = vectir1.add(vectir2).add(vectir3); 38 39 if((obj.vmove.x!=0||obj.vmove.y!=0||obj.vmove.z!=0)) 40 { 41 obj.mesh.moveWithCollisions(obj.vmove);//彷佛同一時刻只有一個物體可以使用這個方法!! 42 43 }
這裏把物體座標系位移向世界座標系位移投影的方法參考了Babylon教程示例。這裏有一個思惟上的難點:對於一個物體來講「模型的正向」、「mesh的正向」和「骨骼動畫的正向」可能不是一個方向!這是模型繪製者使用3D模型繪製工具時的習慣形成的,若是有條件的話能夠在使用3D模型前用繪製工具把模型調整一下。
4、數據發送:
一、Java後臺的Websocket代碼:
1 import java.io.IOException; 2 import java.util.Date; 3 import java.util.concurrent.CopyOnWriteArraySet; 4 5 import javax.websocket.OnClose; 6 import javax.websocket.OnError; 7 import javax.websocket.OnMessage; 8 import javax.websocket.OnOpen; 9 import javax.websocket.Session; 10 import javax.websocket.server.ServerEndpoint; 11 12 @ServerEndpoint("/websocket3") 13 public class Practice { 14 private static int onlineCount = 0; 15 private static CopyOnWriteArraySet<Practice> webSocketSet = new CopyOnWriteArraySet<Practice>(); 16 private static String admin=""; 17 private Session session; 18 private String name=""; 19 private String id=""; 20 @OnOpen 21 public void onOpen(Session session) 22 { 23 this.session = session; 24 webSocketSet.add(this); //加入set中 25 addOnlineCount(); //在線數加1 26 //System.out.println("有新鏈接加入!當前在線人數爲" + getOnlineCount()); 27 try 28 { 29 this.sendMessage("@id:"+this.session.getId());//這個id是按總鏈接數來算的,能夠避免重複 30 this.id=this.session.getId(); 31 } catch (IOException e) { 32 e.printStackTrace(); 33 } 34 for(Practice item: webSocketSet) 35 { 36 if(!item.id.equals(this.id)) 37 { 38 try { 39 item.sendMessage("[getonl]"+this.id); 40 } catch (IOException e) { 41 e.printStackTrace(); 42 continue; 43 } 44 } 45 } 46 } 47 @OnClose 48 public void onClose() 49 { 50 for(Practice item: webSocketSet) 51 { 52 if(!item.id.equals(this.id)) 53 { 54 try { 55 item.sendMessage("[getoff]"+this.id); 56 } catch (IOException e) { 57 e.printStackTrace(); 58 continue; 59 } 60 } 61 } 62 if(this.id.equals(Practice.admin))//若是是admin下線了 63 { 64 webSocketSet.remove(this); //從set中刪除 65 subOnlineCount(); //在線數減1 66 if(webSocketSet.size()>0) 67 { 68 int i=0; 69 for(Practice item: webSocketSet) 70 { //挑選剩餘隊列中的下一個玩家做爲admin 71 if(i==0) 72 { 73 i++; 74 item.name="admin"; 75 Practice.admin=item.id; 76 try { 77 item.sendMessage("@name:admin");//任命 78 } catch (IOException e) { 79 e.printStackTrace(); 80 } 81 } 82 83 } 84 } 85 else 86 { 87 Practice.admin="";//可能全部用戶都下線了,但這個服務還在 88 } 89 } 90 else 91 { 92 webSocketSet.remove(this); //從set中刪除 93 subOnlineCount(); //在線數減1 94 } 95 96 //System.out.println("有一鏈接關閉!當前在線人數爲" + getOnlineCount()); 97 } 98 @OnMessage 99 public void onMessage(String message, Session session) 100 { 101 //System.out.println("來自客戶端的消息:" + message); 102 if((message.length()>6)&&(message.substring(0,6).equals("@name:")))//這個是命名信息//若是message不足6居然會報錯!! 103 { 104 String str_name=message.split(":")[1]; 105 if(str_name.equals("admin"))//若是這個玩家的角色是admin 106 { 107 if(Practice.admin.equals("")) 108 {//若是尚未admin 109 this.name=str_name; 110 Practice.admin=this.id; 111 try { 112 this.sendMessage("@name:admin");//任命 113 } catch (IOException e) { 114 e.printStackTrace(); 115 } 116 } 117 else 118 {//若是已經有了admin 119 this.name=this.id; 120 try { 121 this.sendMessage("@name:"+this.session.getId()); 122 } catch (IOException e) { 123 e.printStackTrace(); 124 } 125 } 126 } 127 } 128 else if((message.length()>6)&&(message.substring(0,7).equals("privat:"))) 129 {//私聊信息 130 for(Practice item: webSocketSet) 131 { 132 if(item.id.equals(message.split("#")[0].split(":")[1])) 133 { 134 try { 135 item.sendMessage(this.id+"@"+message.split("#")[1]); 136 } catch (IOException e) { 137 e.printStackTrace(); 138 continue; 139 } 140 break; 141 } 142 } 143 } 144 else if((message.length()>6)&&(message.substring(0,8).equals("[admins]"))&&this.name.equals("admin")) 145 {//由adminserver向其餘server廣播的信息 146 for(Practice item: webSocketSet) 147 { 148 if(!item.id.equals(this.id)) 149 { 150 try { 151 item.sendMessage(message); 152 } catch (IOException e) { 153 e.printStackTrace(); 154 continue; 155 } 156 } 157 } 158 } 159 else 160 { 161 //廣播信息,不發給本身 162 for(Practice item: webSocketSet) 163 { 164 if(!item.id.equals(this.id)) 165 { 166 try { 167 item.sendMessage(this.id+"@"+message); 168 } catch (IOException e) { 169 e.printStackTrace(); 170 continue; 171 } 172 } 173 } 174 } 175 } 176 @OnError 177 public void onError(Session session, Throwable error){ 178 System.out.println("發生錯誤,關閉鏈接"); 179 for(Practice item: webSocketSet) 180 { 181 if(!item.id.equals(this.id)) 182 { 183 try { 184 item.sendMessage("[geterr]"+this.id); 185 } catch (IOException e) { 186 e.printStackTrace(); 187 continue; 188 } 189 } 190 } 191 if(this.id.equals(Practice.admin))//若是是admin下線了 192 { 193 webSocketSet.remove(this); //從set中刪除 194 subOnlineCount(); //在線數減1 195 if(webSocketSet.size()>0) 196 { 197 int i=0; 198 for(Practice item: webSocketSet) 199 { //挑選剩餘隊列中的下一個玩家做爲admin 200 if(i==0) 201 { 202 i++; 203 item.name="admin"; 204 Practice.admin=item.id; 205 } 206 try { 207 item.sendMessage("@name:admin");//任命 208 } catch (IOException e) { 209 e.printStackTrace(); 210 } 211 } 212 } 213 else 214 { 215 Practice.admin="";//可能全部用戶都下線了,但這個服務還在 216 } 217 } 218 else 219 { 220 webSocketSet.remove(this); //從set中刪除 221 subOnlineCount(); //在線數減1 222 } 223 //webSocketSet.remove(this); 224 //subOnlineCount(); 225 error.printStackTrace(); 226 } 227 public synchronized void sendMessage(String message) throws IOException{//此爲同步阻塞的發送方式(單發) 228 this.session.getBasicRemote().sendText(message); 229 Date dt=new Date(); 230 //System.out.println(dt.getTime()+"==>>"+message); 231 //this.session.getAsyncRemote().sendText(message); 232 } 233 public void sendMessage2(String message) throws IOException{//此爲異步非阻塞的發送方式(單發) 234 this.session.getAsyncRemote ().sendText(message); 235 Date dt=new Date(); 236 //System.out.println(dt.getTime()+"==>>"+message); 237 //this.session.getAsyncRemote().sendText(message); 238 } 239 240 public static synchronized int getOnlineCount() { 241 return onlineCount; 242 } 243 public static synchronized void addOnlineCount() { 244 Practice.onlineCount++; 245 } 246 public static synchronized void subOnlineCount() { 247 Practice.onlineCount--; 248 } 249 }
這個方法參考網上的一篇WebSocket教程編寫而成,其大意是爲每一個上線的用戶分配id,並把第一個自稱是admin的用戶設爲主機,在主機用戶下線後再任命另外一個用戶爲主機。在數據同步方面提供「私聊」、「admin廣播」、「普通廣播」三種方式。在傳輸數據時遇到多個異步傳輸需求對this.session.getAsyncRemote ()爭搶致使報錯的問題,通過試驗使用同步模式的sendMessage方法能夠避免這一錯誤,至於用戶量提高後同步方法可否提供足夠的傳輸效率還要進一步研究。
二、前臺的WebSocket代碼位於WebSocket.js中:
1 var wsUri=""; 2 var websocket; 3 var id="";//這個是sessionid!! 4 5 //創建鏈接 6 function Connect() 7 {// 8 var location = (window.location+'').split('/'); 9 var IP=location[2]; 10 //wsUri="ws://"+IP+"/JUMP/websocket3"; 11 wsUri="ws://"+$("#str_ip")[0].value+":8081/PRACTICE/websocket3"; 12 try 13 { 14 websocket = new WebSocket(wsUri);//創建ws鏈接 15 $("#str_ip")[0].disabled=true; 16 $("#str_name")[0].disabled=true; 17 username=$("#str_name")[0].value; 18 $("#btn_create")[0].disabled=false; 19 20 websocket.onopen = function(evt) //鏈接創建完畢 21 { 22 onOpen(evt) 23 }; 24 websocket.onmessage = function(evt) {//收到服務器發來的信息 25 onMessage(evt) 26 }; 27 websocket.onclose = function(evt) { 28 onClose(evt) 29 }; 30 websocket.onerror = function(evt) { 31 onError(evt) 32 }; 33 } 34 catch(e) 35 { 36 alert(e); 37 $("#str_ip")[0].disabled=false; 38 $("#str_name")[0].disabled=false; 39 } 40 } 41 //鏈接創建完成的回調函數 42 function onOpen(evt) { 43 state="online"; 44 doSend("@name:"+$("#str_name")[0].value);//鏈接創建後把瀏覽器端的用戶信息傳過去 45 } 46 //關閉鏈接 47 function Close() 48 { 49 websocket.close();//瀏覽器端關閉鏈接 50 51 } 52 function onClose(evt) { 53 writeToScreen('<span style="color: red;">本機鏈接關閉</span>'); 54 $("#str_ip")[0].disabled=false; 55 $("#str_name")[0].disabled=false; 56 state="offline"; 57 } 58 //收到服務器端發來的消息 59 function onMessage(evt) { 60 var str_data=evt.data; 61 if(str_data.substr(0,4)=="@id:")//從服務端返回了sessionid 62 { 63 id=str_data.split(":")[1]; 64 $("#str_id")[0].innerHTML=id; 65 } 66 else if(str_data.substr(0,6)=="@name:")//從服務端返回了任命信息 67 { 68 username=str_data.split(":")[1]; 69 if(username=="admin") 70 { 71 $("#str_name")[0].value=username; 72 writeToScreen('<span style="color: blue;">本機被任命爲admin</span>'); 73 } 74 else 75 { 76 $("#str_name")[0].value=username; 77 writeToScreen('<span style="color: blue;">已存在admin,本機被重命名爲'+username+'</span>'); 78 } 79 } 80 。。。 81 82 //發生錯誤 83 function onError(evt) { 84 writeToScreen('<span style="color: red;">ERROR:</span> '+ evt.data); 85 $("#str_ip")[0].disabled=false; 86 $("#str_name")[0].disabled=false; 87 state="offline"; 88 } 89 //發送命令行信息 90 function Send() 91 { 92 doSend($("#str_message")[0].value); 93 } 94 //向服務端發送信息 95 function doSend(message) 96 { 97 websocket.send(message); 98 } 99 //寫入操做日誌 100 function writeToScreen(message) 101 { 102 var pre = document.createElement("p"); 103 pre.style.wordWrap = "break-word"; 104 pre.innerHTML = MakeDateStr()+"->"+message; 105 str_log.appendChild(pre); 106 }
參考網上教程編寫的常規WebSocket通訊代碼
三、創建一些「NPC物體」,也要對他們的狀態進行同步
NPC物體的創建代碼在scene_link.js文件的110行:
1 //一次引入十個物體 2 BABYLON.SceneLoader.ImportMesh("Rabbit", "../MODEL/Rabbit/", "Rabbit.babylon", scene, function (newMeshes, particleSystems, skeletons) 3 { 4 5 var rabbitmesh = newMeshes[1]; 6 //shadowGenerator.getShadowMap().renderList.push(rabbitmesh);//加入陰影渲染隊列 7 var rabbit=new Animal; 8 var obj_p={ 9 mesh:rabbitmesh, 10 scaling:new BABYLON.Vector3(0.04, 0.04, 0.04),//縮放 11 position:new BABYLON.Vector3(Math.random()*100, 30, Math.random()*100),//位置 12 rotation:new BABYLON.Vector3(0, Math.random()*6.28, 0),// 旋轉 13 //rotation:new BABYLON.Vector3(0, 0, 0), 14 checkCollisions:true,//使用默認的碰撞檢測 15 ellipsoid:new BABYLON.Vector3(1, 1, 1),//碰撞檢測橢球 16 ellipsoidOffset:new BABYLON.Vector3(0, 0, 0),//碰撞檢測橢球位移 17 fieldofvision:50,//視野 18 powerofmove:1,//移動力量 19 methodofmove:"controlwitha", 20 state:"eat", 21 id:"rabbit" 22 }; 23 rabbit.init(obj_p); 24 arr_animals["rabbit"]=rabbit; 25 scene.beginAnimation(rabbitmesh.skeleton, 0, 72, true, 0.8); 26 console.log("rabbit"); 27 28 for(i=0;i<9;i++) 29 { 30 var rabbitmesh2 = rabbitmesh.clone("rabbit2"+(i+2)); 31 rabbitmesh2.skeleton = rabbitmesh.skeleton.clone("clonedSkeleton"); 32 var rabbit2=new Animal; 33 var obj_p2={ 34 mesh:rabbitmesh2, 35 scaling:new BABYLON.Vector3(0.04, 0.04, 0.04),//縮放 36 position:new BABYLON.Vector3(Math.random()*100, 30, Math.random()*100),//位置 37 rotation:new BABYLON.Vector3(0, Math.random()*6.28, 0),// 旋轉 38 //rotation:new BABYLON.Vector3(0, 0, 0),// 旋轉 39 checkCollisions:true,//使用默認的碰撞檢測 40 ellipsoid:new BABYLON.Vector3(1, 1, 1),//碰撞檢測橢球 41 ellipsoidOffset:new BABYLON.Vector3(0, 0, 0),//碰撞檢測橢球位移 42 fieldofvision:50,//視野 43 powerofmove:1,//移動力量 44 methodofmove:"controlwitha", 45 state:"eat", 46 id:"rabbit"+(i+2) 47 }; 48 rabbit2.init(obj_p2); 49 arr_animals["rabbit"+(i+2)]=rabbit2; 50 scene.beginAnimation(rabbitmesh2.skeleton, 0, 72, true, 0.8); 51 console.log("rabbit"+(i+2)); 52 //shadowGenerator.getShadowMap().renderList.push(rabbitmesh2);//報錯 53 } 54 55 });
這裏創建了十個物體,其中只有第一個物體的骨骼模型是從模型文件中導入內存的,其餘的物體都在內存中從第一個物體「克隆」而來。注意,在Babylon看來骨骼也是一種特殊的網格(Mesh),因此對網格和骨骼的克隆是分別進行的,再把骨骼克隆的結果做爲網格克隆結果的骨骼屬性。
十個物體被初始化爲Animal對象,Animal對象與Player對象相似,都是從sdyq.object對象派生而來。
NPC物體的運動控制和運動同步代碼在317行:
1 if(username=="admin")//由主機對全部NPC物體的相互做用進行計算,再把做用結果同步到各個分機 2 { 3 //計算每一個動物和全部玩家的交互效果 4 var arr_rabbitmove=[]; 5 for(var key in arr_animals) 6 { 7 var rabbit=arr_animals[key]; 8 var v_face=new BABYLON.Vector3(0,0,0); 9 var newstate="eat"; 10 for(var key2 in arr_myplayers) 11 { 12 var obj=arr_myplayers[key2]; 13 var v_sub=rabbit.mesh.position.subtract(obj.mesh.position); 14 var distans=v_sub.length();//兔子與人類之間的距離 15 if(distans<rabbit.fieldofvision)//在視野內發現了人類 16 { 17 newstate="run"; 18 v_face.addInPlace(v_sub.normalize().scaleInPlace(1/distans));//越近則影響越大 19 } 20 } 21 for(var key2 in arr_webplayers) 22 { 23 var obj=arr_webplayers[key2]; 24 var v_sub=rabbit.mesh.position.subtract(obj.mesh.position); 25 var distans=v_sub.length(); 26 if(distans<rabbit.fieldofvision)//在視野內發現了人類 27 { 28 newstate="run"; 29 v_face.addInPlace(v_sub.normalize().scaleInPlace(1/distans)); 30 } 31 } 32 if(newstate=="run"&&rabbit.state=="eat") 33 {//從eat狀態變爲run狀態 34 rabbit.state="run"; 35 rabbit.powerofmove=3; 36 scene.beginAnimation(rabbit.mesh.skeleton, 0, 72, true, 2.4); 37 } 38 else if(newstate=="eat"&&rabbit.state=="run") 39 {//從run狀態變爲eat狀態 40 rabbit.state="eat"; 41 rabbit.powerofmove=1; 42 scene.beginAnimation(rabbit.mesh.skeleton, 0, 72, true, 0.8); 43 } 44 45 var num_pi=Math.PI; 46 if(rabbit.state=="eat")//一直沒有見到人類 47 { 48 rabbit.waitforturn+=schange; 49 if(rabbit.waitforturn>3) 50 {//每3秒隨機決定一個運動方向 51 rabbit.waitforturn=0; 52 rabbit.witha0={forward:(Math.random()-0.5)*2*rabbit.powerofmove,up:0,left:(Math.random()-0.5)*2*rabbit.powerofmove}; 53 rabbit.mesh.rotation.y=Math.random()*6.28; 54 } 55 movewitha(rabbit); 56 //這些兔子的數據彙總起來一塊兒傳 57 arr_rabbitmove.push([key,rabbit.mesh.position,rabbit.mesh.rotation,rabbit.vmove,rabbit.rychange,rabbit.state]); 58 } 59 else if(rabbit.state=="run") 60 {//奔跑遠離人類 61 rabbit.witha0={forward:-rabbit.powerofmove,up:0,left:0};//這個是兔子的「自主加速度」!!不是世界加速度,也不是鍵盤控制產生的加速度 62 rabbit.mesh.rotation.y=(Math.atan(v_face.z/v_face.x)+num_pi*1/2); 63 movewitha(rabbit); 64 arr_rabbitmove.push([key,rabbit.mesh.position,rabbit.mesh.rotation,rabbit.vmove,rabbit.rychange,rabbit.state]); 65 } 66 } 67 var str_data="[admins]"+JSON.stringify(arr_rabbitmove); 68 doSend(str_data); 69 }
在這個模式中,由主機承擔全部的NPC物體運動計算工做,再把全部計算結果同步到分機,
起初對於不太複雜的玩家信息數據,我簡單的用分隔符「@」將各個字段拼接成一個字符串向其餘客戶端傳遞,後來隨着數據結構的複雜化,我改用JSON傳遞結構化的數據。
四、客戶端對服務器端傳來的信息進行處理:
a、添加新玩家,代碼位於WebSocket.js184行:
1 case "addnewplayer": 2 {//感知到加入了一個新的玩家,把新玩家加入到本身的場景裏,先查詢場景中是否已經有同名的mesh,若是有則使用clone方法同步加載,若是沒有再使用import異步導入,這樣作的根本緣由在於import方法導入模型的返回函數裏沒法自定義參數 3 var dt=new Date(); 4 console.log(dt.getTime()+"get addnewplayer"+arr[0]); 5 var flag=0;//加載完成標誌 6 for(var key in arr_myplayers)//先在本機的玩家列表裏找 7 { 8 if(arr_myplayers[key].meshname==arr[14])//若是與主控物體的meshname相同 9 { 10 11 var obj_key=arr_myplayers[key]; 12 arr_webplayers[arr[0]] = MyCloneplayer(obj_key,arr); 13 shadowGenerator.getShadowMap().renderList.push(arr_webplayers[arr[0]].mesh);//陰影生成器彷佛對含有submesh的Mesh不起做用 14 writeToScreen('<span style="color: blue;">addnewplayer: ' + arr[0] + '</span>'); 15 flag=1; 16 17 //異步加入新玩家以後,還要把本身的信息發給新玩家,讓新玩家添加本身(私聊) 18 addoldplayer(arr[0]); 19 break; 20 } 21 } 22 if(flag==0)//再在網絡玩家列表裏查找 23 { 24 for(var key in arr_webplayers) 25 { 26 if(arr_webplayers[key].meshname==arr[14])//若是與主控物體的meshname相同 27 { 28 var obj_key=arr_webplayers[key]; 29 arr_webplayers[arr[0]] = MyCloneplayer(obj_key,arr); 30 shadowGenerator.getShadowMap().renderList.push(arr_webplayers[arr[0]].mesh); 31 writeToScreen('<span style="color: blue;">addnewplayer: ' + arr[0] + '</span>'); 32 flag=1; 33 //異步加入新玩家以後,還要把本身的信息發給新玩家,讓新玩家添加本身(私聊) 34 addoldplayer(arr[0]); 35 break; 36 } 37 } 38 } 39 if(flag==0)//都沒找着,就新建 40 { 41 //arr[14]保存着meshname能夠做爲異步方法間的紐帶,若是發生同時加載兩個同樣的不存在的mesh時,讓後來的那個經過websocket延時重發 42 if(tempobj[arr[14]]&&tempobj[arr[14]]!="OK")//這個暫存位正在被佔用 43 { 44 doSend("privat:" + id + "#" + str_data);//請求websocket服務器再次把這個指令發給本身,以等待佔用者完成操做 45 } 46 else 47 { 48 tempobj[arr[14]] = arr;//用tempobj暫存該物體的初始化參數 49 BABYLON.SceneLoader.ImportMesh(arr[11], arr[12], arr[13], scene, function (newMeshes, particleSystems, skeletons) {//載入完成的回調函數 50 var Tom = new Player; 51 var obj_p = {}; 52 obj_p.mesh = newMeshes[0];//網格數據 53 var arr = tempobj[obj_p.mesh.name]; 54 obj_p.scaling = new BABYLON.Vector3(parseFloat(arr[2]), parseFloat(arr[3]), parseFloat(arr[4]));//縮放 55 obj_p.position = new BABYLON.Vector3(parseFloat(arr[5]), parseFloat(arr[6]), parseFloat(arr[7]));//位置 56 obj_p.rotation = new BABYLON.Vector3(parseFloat(arr[8]), parseFloat(arr[9]), parseFloat(arr[10]));// 旋轉 57 obj_p.checkCollisions = true;//使用默認的碰撞檢測 58 obj_p.ellipsoid = new BABYLON.Vector3(0.5, 1, 0.5);//碰撞檢測橢球 59 obj_p.ellipsoidOffset = new BABYLON.Vector3(0, 2, 0);//碰撞檢測橢球位移 60 obj_p.skeletonsPlayer = skeletons; 61 obj_p.methodofmove = "controlwitha"; 62 obj_p.id = arr[0]; 63 obj_p.name = arr[0]; 64 obj_p.p1 = arr[11]; 65 obj_p.p2 = arr[12]; 66 obj_p.p3 = arr[13]; 67 var len=newMeshes.length;//對於複雜的模型來講newMeshes的其餘部分也必須保存下來 68 var arr=[]; 69 for(var i=1;i<len;i++) 70 { 71 arr.push(newMeshes[i]); 72 } 73 obj_p.submeshs=arr; 74 Tom.init( 75 obj_p 76 ); 77 tempobj[obj_p.mesh.name] = "OK"; 78 arr_webplayers[arr[0]] = Tom; 79 shadowGenerator.getShadowMap().renderList.push(arr_webplayers[arr[0]].mesh); 80 81 writeToScreen('<span style="color: blue;">addnewplayer: ' + arr[0] + '</span>'); 82 flag=1; 83 //異步加入新玩家以後,還要把本身的信息發給新玩家,讓新玩家添加本身(私聊) 84 addoldplayer(arr[0]); 85 86 }); 87 } 88 } 89 break; 90 } 91 92 case "addoldplayer": 93 {//添加一個前輩玩家,此時默認前輩的網絡玩家列表裏已經有了本元素,因此不須要再通知前輩玩家添加本玩家,多個前輩玩家同時返回如何處理?用出入棧方式?能保證先進先出? 94 var dt=new Date(); 95 console.log(dt.getTime()+"get addoldplayer"+arr[0]); 96 var flag=0; 97 for(var key in arr_myplayers) 98 { 99 if(arr_myplayers[key].meshname==arr[14])//若是與主控物體的meshname相同 100 { 101 var obj_key=arr_myplayers[key]; 102 arr_webplayers[arr[0]] =MyCloneplayer(obj_key,arr); 103 shadowGenerator.getShadowMap().renderList.push(arr_webplayers[arr[0]].mesh); 104 writeToScreen('<span style="color: blue;">addoldplayer: ' + arr[0] + '</span>'); 105 flag=1; 106 107 break; 108 } 109 } 110 if(flag==0)//再在網絡元素裏查找 111 { 112 for(var key in arr_webplayers) 113 { 114 if(arr_webplayers[key].meshname==arr[14])//若是與主控物體的meshname相同 115 { 116 var obj_key=arr_webplayers[key]; 117 arr_webplayers[arr[0]] = MyCloneplayer(obj_key,arr); 118 shadowGenerator.getShadowMap().renderList.push(arr_webplayers[arr[0]].mesh); 119 writeToScreen('<span style="color: blue;">addoldplayer: ' + arr[0] + '</span>'); 120 flag=1; 121 break; 122 } 123 } 124 } 125 if(flag==0)//都沒找着,就新建 126 { 127 //arr[14]保存着meshname能夠做爲異步方法間的紐帶,若是發生同時加載兩個同樣的不存在的mesh時,讓後來的那個經過websocket延時重發 128 if(tempobj[arr[14]]&&tempobj[arr[14]]!="OK")//這個暫存位正在被佔用 129 { 130 doSend("privat:" + id + "#" + str_data);//請求websocket服務器再次把這個指令發給本身,以等待佔用者完成操做 131 } 132 else 133 { 134 tempobj[arr[14]] = arr; 135 BABYLON.SceneLoader.ImportMesh(arr[11], arr[12], arr[13], scene, function (newMeshes, particleSystems, skeletons) {//載入完成的回調函數 136 var Tom = new Player; 137 var obj_p = {}; 138 obj_p.mesh = newMeshes[0];//網格數據 139 var arr = tempobj[obj_p.mesh.name]; 140 obj_p.scaling = new BABYLON.Vector3(parseFloat(arr[2]), parseFloat(arr[3]), parseFloat(arr[4]));//縮放 141 obj_p.position = new BABYLON.Vector3(parseFloat(arr[5]), parseFloat(arr[6]), parseFloat(arr[7]));//位置 142 obj_p.rotation = new BABYLON.Vector3(parseFloat(arr[8]), parseFloat(arr[9]), parseFloat(arr[10]));// 旋轉 143 obj_p.checkCollisions = true;//使用默認的碰撞檢測 144 obj_p.ellipsoid = new BABYLON.Vector3(0.5, 1, 0.5);//碰撞檢測橢球 145 obj_p.ellipsoidOffset = new BABYLON.Vector3(0, 2, 0);//碰撞檢測橢球位移 146 obj_p.skeletonsPlayer = skeletons; 147 obj_p.methodofmove = "controlwitha"; 148 obj_p.id = arr[0]; 149 obj_p.name = arr[0]; 150 obj_p.p1 = arr[11]; 151 obj_p.p2 = arr[12]; 152 obj_p.p3 = arr[13]; 153 var len=newMeshes.length;//對於複雜的模型來講newMeshes的其餘部分也必須保存下來 154 var arr=[]; 155 for(var i=1;i<len;i++) 156 { 157 arr.push(newMeshes[i]); 158 } 159 obj_p.submeshs=arr; 160 Tom.init( 161 obj_p 162 ); 163 tempobj[obj_p.mesh.name] = "OK"; 164 arr_webplayers[arr[0]] = Tom; 165 shadowGenerator.getShadowMap().renderList.push(arr_webplayers[arr[0]].mesh); 166 writeToScreen('<span style="color: blue;">addoldplayer: ' + arr[0] + '</span>'); 167 flag=1; 168 169 }); 170 } 171 } 172 break; 173 }
這裏的主要難點是如何處理多個異步的添加玩家請求,通過思考和實驗部分的解決了問題。
b、多個客戶端之間同步玩家的狀態:
1 case "updatemesh": 2 { 3 var dt=new Date(); 4 console.log(dt.getTime()+"get updatemesh"+arr[0]); 5 var obj = arr_webplayers[arr[0]];//從網絡玩家列表裏找到這個玩家 6 if(obj) 7 { 8 var mesh = obj.mesh; 9 mesh.position.x = parseFloat(arr[2]);//這裏已經產生了位移效果!! 10 mesh.position.y = parseFloat(arr[3]); 11 mesh.position.z = parseFloat(arr[4]); 12 mesh.rotation.x = parseFloat(arr[5]); 13 mesh.rotation.y = parseFloat(arr[6]); 14 mesh.rotation.z = parseFloat(arr[7]); 15 16 obj.vmove.x=parseFloat(arr[8]); 17 obj.vmove.y=parseFloat(arr[9]); 18 obj.vmove.z=parseFloat(arr[10]); 19 20 obj.rychange= parseFloat(arr[11]); 21 obj.countstop=0;//喚醒該物體的運動 22 if(obj.PlayAnnimation == false&&(obj.vmove.x != 0 || obj.vmove.y != 0 || obj.vmove.z != 0 || obj.rychange != 0)) 23 { 24 obj.PlayAnnimation = true; 25 obj.beginSP(0); 26 } 27 } 28 break; 29 }
另外一部分控制網絡玩家的代碼在scene.registerBeforeRender()中:
1 for (var key2 in arr_webplayers)//對於由其餘客戶端控制的物體 2 { 3 var obj = arr_webplayers[key2]; 4 switch(obj.methodofmove) 5 { 6 case "controlwitha": 7 { 8 obj.lab.rotation.y=(-1.55 - cameraArcRotative[0].alpha)-obj.mesh.rotation.y; 9 if(obj.countstop<=4) 10 { 11 if ((obj.vmove.x != 0 || obj.vmove.y != 0 || obj.vmove.z != 0 || obj.rychange != 0)&& obj.PlayAnnimation == false) { 12 obj.PlayAnnimation = true; 13 obj.beginSP(0); 14 obj.mesh.moveWithCollisions(obj.vmove); 15 } 16 else if (obj.vmove.x == 0 && obj.vmove.y == 0 && obj.vmove.z == 0 && obj.rychange == 0 && obj.PlayAnnimation == true) { 17 obj.countstop++; 18 if (obj.countstop > 4)//連續4幀沒有該對象的運動信息傳過來,則該物體的運動計算進入休眠 19 { 20 obj.PlayAnnimation = false; 21 obj.stopSP(0); 22 } 23 } 24 } 25 break; 26 } 27 default : 28 { 29 break; 30 } 31 } 32 }
c、最後是對NPC物體運動同步的處理:
1 case "[admins]": 2 { 3 if(username=="admin") 4 {//adminserver不處理admin廣播 5 6 } 7 else 8 { 9 if(!scene.isReady() || !arr_myplayers) 10 { 11 return; 12 } 13 var arr_rabbitmove=JSON.parse(str_data.substr(8)); 14 var len=arr_rabbitmove.length; 15 for(var i=0;i<len;i++) 16 { 17 var arr=arr_rabbitmove[i]; 18 var rabbit=arr_animals[arr[0]]; 19 var rabbitmesh=rabbit.mesh; 20 rabbitmesh.position=arr[1]; 21 rabbitmesh.rotation=arr[2]; 22 rabbit.vmove=arr[3]; 23 rabbit.rychange=arr[4]; 24 25 if(arr[5]=="run"&&rabbit.state=="eat") 26 { 27 rabbit.state="run"; 28 rabbit.powerofmove=3; 29 scene.beginAnimation(rabbitmesh.skeleton, 0, 72, true, 2.4); 30 } 31 else if(arr[5]=="eat"&&rabbit.state=="run") 32 { 33 rabbit.state="eat"; 34 rabbit.powerofmove=1; 35 scene.beginAnimation(rabbitmesh.skeleton, 0, 72, true, 0.8); 36 } 37 } 38 } 39 break; 40 }
解開JSON,對每個NPC物體分別處理。
5、部署和使用:
程序完整代碼在能夠在https://github.com/ljzc002/WebGL2下載,我編寫的代碼基於MIT協議發佈,使用的第三方庫文件按其原有的發佈協議發佈。
部署:把PRACTICE/WebRoot/下的全部文件複製到PRACTICE3/目錄下,將PRACTICE3/複製到Tomcat的WebApps/目錄下,把PRACTICE3/更名爲PRACTICE/,啓動Tomcat,訪問scene_link.html頁面。
使用:第一個input輸入Websocket所在IP,第二個input輸入用戶名(輸入admin表示申請做爲主機),點擊「websocket鏈接」創建鏈接,點擊「啓動場景」啓動WebGL場景。
6、寫在後面的話:
限於時間和編程水平,程序中還有不少bug和缺陷,歡迎你們批評指正。
音樂、美術、文學等常規的人類自我表達方式都要求人不斷的在很短的時間片內對事物產生足夠充分的認識,非有過人之天賦與辛苦之訓練而不可成就;相對而言編程能夠經過分解、封裝、複用將空間複雜度轉化爲時間複雜度,任何普通人通過努力都能有所收穫。