网页端3D编程小实验-一种即时战略游戏的编程原型

本文尝试基于Babylon.js引擎(以下简称bbl)和recast2导航库,采用“经典代码组织方式”编写一个可作为即时战略(以下简称rts)游戏编程原型的程序,该程序收集了进行此类开发所需的离线依赖包,总结了该类程序的一种页面和程序结构设计方法,并且基于这些依赖和方法实现了简单的地图构建、单位控制、受地形影响的群组导航功能。

一、代码地址与运行效果

该程序在https://github.com/ljzc002/A-rts-game-fream/tree/main基于MIT许可证开源,它是https://www.bilibili.com/opus/1128695548365242388项目的一个组成部分,运行/html/WH-admin.html可打开基础测试页面:

屏幕截图 2025-11-10 100923

页面上半部为负责3D渲染的canvas区域,下部为可自定义设计UI的原生html区域,该页面基于百分比定位方法设计,可适用于多种分辨率场景,考虑到兼容移动设备,在canvas区也设计了一些gui按钮以代替键盘和鼠标滚轮功能。

canvas区域中渲染了一张简单的方形地图,地图四周为128*128泥土地,中间为64*64的林地,林地中间的白点为可移动单位的标志物,周围的白色边界为围墙。滚动鼠标滚轮可控制视角上下移动,左键拖动鼠标可同步拖动地图(显然,在不添加额外按钮时,左键拖动地图操作与“左键框选单位”相冲突,需根据实际需要取舍,如需框选单位功能可参考此文中的实现:https://juejin.cn/post/6968635340110168101),按o键可切换为bbl的自由相机视角,按i恢复为rts视角,以下是林地和标志物的侧视图:

屏幕截图 2025-11-10 103409

rts视角下,右键点击地面则全部标志物将向点击位置移动,移动过程中各个标志物之间将自动保持距离,在林地中移动时的速度为泥土地的40%。

接下来两章先介绍bbl框架离线依赖的手动组织方法,如果读者已经熟悉bbl使用,可跳到第四章。

二、编程框架搭建

1、为什么基于经典方法组织

所谓“基于经典方法组织”指不依赖npm、typescript、umi等需要“编译”环节的框架,手动将依赖包配置在程序的特定位置,在实验性编程中这样作的好处包括:易于调试运行时代码、易于修改第三方依赖包、减少网络依赖性、专注于功能本身而非框架、加深对程序结构了解等。如果开发者需要使用流行的编译框架进行代码组织,则这些基于经典方法组织的代码,也可以容易的转为各种框架下的代码或嵌入到框架中(反之则很难)。

2、手动下载bbl依赖包并离线部署

首先随意打开一个官方训练场场景,例如基础演示场景https://playground.babylonjs.com/#WJXQP0:

屏幕截图 2025-11-10 111912

左边红框处显示当前最新的bbl版本,例如图中为8.36.1,右边红框为下载按钮,需注意的是,这里下载的只是该训练场的索引html页面,而该页面中包含bbl依赖库的实际下载地址:

<!-- Babylon globals (kept so window.BABYLON is available if your app expects it) -->
  <script src="https://assets.babylonjs.com/generated/Assets.js"></script>
  <script src="https://cdn.babylonjs.com/recast.js"></script>
  <script src="https://cdn.babylonjs.com/ammo.js"></script>
  <script src="https://cdn.babylonjs.com/havok/HavokPhysics_umd.js"></script>
  <script src="https://cdn.babylonjs.com/cannon.js"></script>
  <script src="https://cdn.babylonjs.com/Oimo.js"></script>
  <script src="https://cdn.babylonjs.com/earcut.min.js"></script>
  <script src="https://cdn.babylonjs.com/babylon.js"></script>
  <script src="https://cdn.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
  <script src="https://cdn.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
  <script src="https://cdn.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
  <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.js"></script>
  <script src="https://cdn.babylonjs.com/serializers/babylonjs.serializers.min.js"></script>
  <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>
  <script src="https://cdn.babylonjs.com/addons/babylonjs.addons.min.js"></script>
  <script src="https://cdn.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>

从上到下,其功能分别为:

官方示例资源地址(无用)

群组导航库地址(旧版,被替代)

ammo物理引擎(较旧,但功能基本可用)

havok物理引擎(较新,官方推荐使用)

cannon物理引擎(较旧,存在bug)

oimo物理引擎(较旧,存在bug)

“挖洞库”(体积不大,建议保留)

核心库

额外材质库(如水、火、毛绒等,按需加载)

程序纹理库(按需加载)

后期处理库(按需加载)

模型加载库(记忆中bbl默认可加载obj和bbl格式模型,如需加载更多格式模型需使用此库)

序列化库(可用来将bbl生成的场景导出为各种格式的三维模型)

gui库(用来在页面中绘制gui按钮,建议保留)

额外内容库(一些额外添加的功能,例如本文所使用的recast2导航库即通过此库加载)

调试库(bbl官方的页面调试工具)

在浏览器中访问对应的url即可下载相应的依赖库,需要注意的是,其中一些库内部又会引用其他依赖库,例如havok物理引擎库会引用havok.wasm文件,此时可通过浏览器调试工具获取其引用地址并进行下载,其中模型加载库的简介依赖包最多,可根据所需加载的模型类别选择性配置。还有个别依赖项缺少对离线原生部署的兼容,需手动修改其代码。

此次实验的离线依赖包包含以下内容:

屏幕截图 2025-11-10 114459

图中@recast-navigation目录为通过mybabylonjs.addons.min.js文件加载的recast2导航库。

项目整体目录结构为:

屏幕截图 2025-11-10 140523

其中assets中为图片、音频等资源,lib中为依赖库。

3、入口网页

WH-admin.html文件为程序的入口网页,其代码如下:

  1 <!DOCTYPE html>
  2 <html lang="en">
  3 <head>
  4     <meta charset="UTF-8">
  5     <title>管理端包含actionloop,负责推演所有单位,并按视野将推演结论发送给player,同时接收player传来的有效指令</title>
  6     <style>
  7         html, body {
  8             overflow: hidden;
  9             width: 100%;
 10             height: 100%;
 11             margin: 0;
 12             padding: 0;
 13         }
 14         #renderCanvas {
 15             width: 100%;
 16             height: 70%;
 17             touch-action: none;
 18         }
 19         #fps {    position: absolute;    right: 20px;    top: 5em;    font-size: 20px;    color: white;/*帧数显示*/
 20             text-shadow: 2px 2px 0 black;}
 21         #div_bottom div{
 22             position: absolute;
 23         }
 24         .div_btn{
 25             font-size: 20px;line-height: 20px;position: absolute;cursor:crosshair;
 26         }
 27         .div_htmlmesh{
 28             font-size: 100%;line-height: 100%;position: absolute;border:1px solid;top:-50%;left:-50%
 29         }
 30         .mod_group,.mod_unit{
 31             font-size: 100%;line-height: 100%;position: absolute;
 32         }
 33     </style>
 34     <script src="./lib/bbl8c/babylon.js"></script>
 35     <script src="./lib/bbl8c/babylon.inspector.bundle.js"></script>
 36     <script src="./lib/bbl8c/babylon.gui.min.js"></script>
 37     <script src="./lib/bbl8c/mybabylonjs.addons.min.js"></script>
 38     <script src="./lib/bbl8c/HavokPhysics_umd.js"></script>
 39     <script src="./lib/bbl8c/earcut.min.js"></script>
 40     <script src="./lib/newland.js"></script>
 41     <script src="./lib/nohurry.js"></script>
 42     <script src="./lib/VTools.js"></script>
 43     <!--<script src="./lib/sqliteClient.js"></script>-->
 44     <script src="./lib/ControlWH.js"></script>
 45     <script src="./lib/ControlGui.js"></script>
 46     <script src="./lib/ControlCrowd.js"></script>
 47     <!--<script src="./lib/drawStellaris6.js"></script>-->
 48     <script src="./lib/cyhz.js"></script>
 49     <script src="./lib/drawSprites.js"></script>
 50     <script src="./lib/createMap.js"></script>
 51     <!--<script src="./lib/drawGalaxy6.js"></script>-->
 52     <!--<script src="./lib/drawStar.js"></script>-->
 53     <!--<script src="./lib/drawPlanet.js"></script>-->
 54 </head>
 55 <body>
 56     <canvas id="renderCanvas" touch-action="none"></canvas>
 57     <div id="fps" style="z-index: 302;"></div>
 58     <div id="div_bottom" style="z-index: 301;position: absolute;bottom: 0px;width: 100%;height: 30%;font-size: 12px;border-top: 1px solid">
 59 
 60     </div>
 61     <div id="div_comment" style="z-index: 302;position:fixed;display: none;width: 200px;height: 400px;left: 0px;bottom:0px;border: 1px solid black">
 62     </div>
 63     <div id="btn_comment" style="z-index: 303;position:fixed;display: block;width: 20px;height: 80px;left: 0px;bottom:150px;text-align: center;
 64                 background-color:gainsboro;cursor: pointer;border: 1px solid black"
 65          onclick="showComment()"><br/><br/> 66     </div>
 67     <div id="div_hidden" style="display: none">
 68 
 69     </div>
 70 </body>
 71 <script>
 72     console.log(new Date().getTime());
 73     var dir_lib_header="\.\/lib/";
 74     var flag_runningstate="等待场景初始化";
 75     var VERSION=1.0,AUTHOR="1113908055@qq.com";
 76     var machine,canvas,engine,scene,gl,MyGame,camera0,camera_minimap,experience;
 77     //var camera_x,camera_x2,camera_y,camera_y2,camera_z,camera_z2;//用来生成即时天空盒
 78     machine=navigator;
 79     canvas = document.getElementById("renderCanvas");
 80     var div_bottom=document.getElementById("div_bottom");
 81     var divFps = document.getElementById("fps");
 82     var div_hidden= document.getElementById("div_hidden");
 83     var flag_webgpu=false;
 84 
 85     var one=null;//如果有第一人称或第三人称控制的核心对象
 86     var int_z=0;//这个值越大代表距离越近,显示的svg细节越仔细,规定在小于-2时减少显示细节
 87     var int0=1,int0b=1;
 88     var divFps = document.getElementById("fps");
 89     window.onload=beforewebGL;
 90 
 91     async function beforewebGL()
 92     {
 93         engine=new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true,  disableWebGL2Support: false});
 94         scene = new BABYLON.Scene(engine);
 95         Init();
 96     }
 97     async function Init()
 98     {
 99         await initScene();
100         await initArena();
101         InitMouse();
102         initGuiControl();
103         webGLStart2();
104     }
105     var mat_global={},hd_camera0=Math.PI/2;
106     var initpos_camera0,initrot_camera0;
107     async function initScene()
108     {
109         //scene.clearColor=new BABYLON.Color3(0,0,0);
110         var light0 = new BABYLON.HemisphericLight("light0", new BABYLON.Vector3(0, 1, 0), scene);
111         light0.diffuse = new BABYLON.Color3(1,1,1);//这道“颜色”是从上向下的,底部收到100%,侧方收到50%,顶部没有
112         light0.specular = new BABYLON.Color3(0,0,0);
113         light0.groundColor = new BABYLON.Color3(1,1,1);//这个与第一道正相反
114 
115         camera0= new BABYLON.UniversalCamera("FreeCamera", new BABYLON.Vector3(0, 600, 0), scene);
116         camera0.rotation.x=hd_camera0;//需要45度斜向下视角
117         camera0.maxZ=20000;
118         camera0.minZ=0.1;
119         //scene.activeCameras.push(camera0);
120         camera0.speed=1;
121         camera0.myRotation={x:0,y:0,z:0};
122         initpos_camera0=camera0.position.clone();
123         initrot_camera0=camera0.rotation.clone();
124         var node_hand=new BABYLON.TransformNode("hand");
125         node_hand.position.z=1;
126         node_hand.parent=camera0;
127         camera0.node_hand=node_hand;
128         var node_pick=new BABYLON.TransformNode("pick");
129         node_pick.position.z=100;
130         node_pick.parent=camera0;
131         camera0.node_pick=node_pick;
132         scene.activeCameras = [camera0];
133         var points1=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0.01,0,0)];
134         var points2=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(-0.01,0,0)];
135         var points3=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0,0.01,0)];
136         var points4=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0,-0.01,0)];
137         var lines=new BABYLON.MeshBuilder.CreateLineSystem("LineSystem",{lines:[points1,points2,points3,points4]});
138         lines.isPickable=false;
139         lines.parent=node_hand;
140         lines.isVisible=false;
141         lines.renderingGroupId=3;
142         lines.color="green";
143         camera0.lines=lines;
144         lines.alwaysSelectAsActiveMesh=true;
145 
146         var mat_green=new BABYLON.StandardMaterial("mat_green", scene);
147         mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0);
148         mat_green.freeze();
149         mat_global.mat_green=mat_green;
150         var mat_frame=new BABYLON.StandardMaterial("mat_frame", scene);
151         mat_frame.wireframe=true;
152         mat_frame.freeze();
153         mat_global.mat_frame=mat_frame;
154         var mat_white_e=new BABYLON.StandardMaterial("mat_white_e", scene);
155         mat_white_e.disableLighting = true;
156         mat_white_e.emissiveColor = BABYLON.Color3.White();
157         mat_white_e.backFaceCulling=false;
158         mat_white_e.freeze();
159         mat_global.mat_white_e=mat_white_e;
160     }
161     var skybox;
162     var HK,physicsPlugin,observable;
163     //碰撞过滤器
164     var filter_group_l=1;
165     var filter_group_r=2;
166     var filter_group_g=4;
167     var filter_group_a=8;
168     var navigationPlugin,navMesh,navMeshQuery;
169     async function initArena()
170     {
171         // var skybox = BABYLON.Mesh.CreateBox("skyBox", 1500.0, scene);//尺寸存在极限,设为15000后显示异常
172         // var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
173         // skyboxMaterial.backFaceCulling = false;
174         // skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("./assets/image/SKYBOX/skybox", scene);
175         // skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
176         // skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
177         // skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
178         // skyboxMaterial.disableLighting = true;
179         // skybox.material = skyboxMaterial;
180         // skybox.renderingGroupId = 1;
181         // skybox.isPickable=false;
182         // skybox.infiniteDistance = true;
183 
184         HK=await HavokPhysics();
185         physicsPlugin = new BABYLON.HavokPlugin();
186         scene.enablePhysics(new BABYLON.Vector3(0, -9.8, 0), physicsPlugin);
187         observable = physicsPlugin.onCollisionObservable;
188         var observer = observable.add((collisionEvent) => {
189             //fight0(collisionEvent);//碰撞事件
190         });
191 
192 
193 
194         var mesh_ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 128, height: 128}, scene);
195         mesh_ground.alwaysSelectAsActiveMesh = true;
196         //mesh_ground.renderingGroupId=2;//renderingGroupId会与htmlmesh冲突吗?
197         mesh_ground.position.x=0;
198         mesh_ground.position.z=0;
199         var mat = new BABYLON.StandardMaterial("mat_ground", scene);//1
200         mat.disableLighting = true;
201         mat.emissiveTexture = new BABYLON.Texture("./assets/image/LANDTYPE/terre.png", scene);
202         mat.emissiveTexture.uScale = 8;
203         mat.emissiveTexture.vScale = 8;
204         mat.freeze();
205         mat_global.mat_ground=mat;
206         mesh_ground.material = mat;
207         mesh_ground.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
208             , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
209         mesh_ground.physicsImpostor.shape.filterMembershipMask=filter_group_g;
210         mesh_ground.myType="ground";
211         mesh_ground.myType1="base";
212         //mesh_ground.physicsImpostor.shape.filterCollideMask=filter_group_g;
213         //四周的围栏,它们是一直存在的,随着用户的操作还会生成一些临时的围栏
214         var mesh_ground1 = BABYLON.MeshBuilder.CreateGround("ground1", {width: 128, height: 10}, scene);
215         mesh_ground1.position.x=0;
216         mesh_ground1.position.z=64;
217         mesh_ground1.rotation.x=Math.PI/2;
218         mesh_ground1.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
219             , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
220         mesh_ground1.physicsImpostor.body.mesh_ground=mesh_ground;
221         mesh_ground1.physicsImpostor.shape.filterMembershipMask=filter_group_g;
222         mesh_ground1.material=mat_global.mat_white_e;
223         mesh_ground1.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
224         var mesh_ground2 = BABYLON.MeshBuilder.CreateGround("ground2", {width: 128, height: 10}, scene);
225         mesh_ground2.position.x=0;
226         mesh_ground2.position.z=-64;
227         mesh_ground2.rotation.x=Math.PI/2;
228         mesh_ground2.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
229             , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
230         mesh_ground2.physicsImpostor.body.mesh_ground=mesh_ground;
231         mesh_ground2.physicsImpostor.shape.filterMembershipMask=filter_group_g;
232         mesh_ground2.material=mat_global.mat_white_e;
233         mesh_ground2.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
234         var mesh_ground3 = BABYLON.MeshBuilder.CreateGround("ground3", {width: 10, height: 128}, scene);
235         mesh_ground3.position.x=-64;
236         mesh_ground3.position.z=0;
237         mesh_ground3.rotation.z=Math.PI/2;
238         mesh_ground3.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
239             , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
240         mesh_ground3.physicsImpostor.body.mesh_ground=mesh_ground;
241         mesh_ground3.physicsImpostor.shape.filterMembershipMask=filter_group_g;
242         mesh_ground3.material=mat_global.mat_white_e;
243         mesh_ground3.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
244         var mesh_ground4 = BABYLON.MeshBuilder.CreateGround("ground4", {width: 10, height: 128}, scene);
245         mesh_ground4.position.x=64;
246         mesh_ground4.position.z=0;
247         mesh_ground4.rotation.z=Math.PI/2;
248         mesh_ground4.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
249             , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
250         mesh_ground4.physicsImpostor.body.mesh_ground=mesh_ground;
251         mesh_ground4.physicsImpostor.shape.filterMembershipMask=filter_group_g;
252         mesh_ground4.material=mat_global.mat_white_e;
253         mesh_ground4.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
254         initCrowd([mesh_ground]);
255         initMap();
256 
257     }
258 
259     function webGLStart2()
260     {
261         flag_runningstate="场景初始化完成";
262         //sr_global= new BABYLON.SnapshotRenderingHelper(scene);
263         createManagers();
264 
265         //scene.debugLayer.show();
266         //initStellaris();
267         MyBeforeRender0();
268         //scene.debugLayer.show();
269     }
270 
271 
272 
273     var flag_comment=false;
274     var div_comment=document.getElementById("div_comment");
275     var btn_comment=document.getElementById("btn_comment");
276     function showComment()
277     {
278         if(!flag_comment)
279         {
280             div_comment.style.display="block";
281             btn_comment.style.left="201px";
282             btn_comment.innerHTML="提<br/>示<br/>《";
283         }
284         else {
285             div_comment.style.display="none";
286             btn_comment.style.left="0px";
287             btn_comment.innerHTML="提<br/>示<br/>》";
288         }
289         flag_comment=!flag_comment;
290     }
291 </script>
292 </html>
View Code

其中的入口代码为:

window.onload=beforewebGL;

    async function beforewebGL()
    {
        engine=new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true,  disableWebGL2Support: false});
        scene = new BABYLON.Scene(engine);
        Init();
    }

此处使用bbl的WebGL模式进行场景渲染,如追求更快的渲染速度可在此处判断运行环境是否支持WebGPU技术,如支持则使用WebGPU渲染,这里考虑到当前版本的WebGPU服务必须在https协议下发布,采用更易部署的WebGL模式。

本实验的初始化方法包括5个方面的初始化操作:

async function Init()
    {
        await initScene();
        await initArena();
        InitMouse();
        initGuiControl();
        webGLStart2();
    }

以下将分别详细介绍每一项初始化操作,如程序有更多功能,则可在这里添加更多初始化方法,如WebSocket初始化、程序纹理初始化、数据库初始化等。

三、初始化操作

1、场景初始化

代码如下:

async function initScene()
    {
        //scene.clearColor=new BABYLON.Color3(0,0,0);
        var light0 = new BABYLON.HemisphericLight("light0", new BABYLON.Vector3(0, 1, 0), scene);
        light0.diffuse = new BABYLON.Color3(1,1,1);//这道“颜色”是从上向下的,底部收到100%,侧方收到50%,顶部没有
        light0.specular = new BABYLON.Color3(0,0,0);
        light0.groundColor = new BABYLON.Color3(1,1,1);//这个与第一道正相反

        camera0= new BABYLON.UniversalCamera("FreeCamera", new BABYLON.Vector3(0, 600, 0), scene);
        camera0.rotation.x=hd_camera0;//需要垂直向下视角
        camera0.maxZ=20000;
        camera0.minZ=0.1;
        //scene.activeCameras.push(camera0);
        camera0.speed=1;
        camera0.myRotation={x:0,y:0,z:0};
        initpos_camera0=camera0.position.clone();
        initrot_camera0=camera0.rotation.clone();
        var node_hand=new BABYLON.TransformNode("hand");
        node_hand.position.z=1;
        node_hand.parent=camera0;
        camera0.node_hand=node_hand;
        var node_pick=new BABYLON.TransformNode("pick");
        node_pick.position.z=100;
        node_pick.parent=camera0;
        camera0.node_pick=node_pick;
        scene.activeCameras = [camera0];
        var points1=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0.01,0,0)];
        var points2=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(-0.01,0,0)];
        var points3=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0,0.01,0)];
        var points4=[new BABYLON.Vector3(0,0,0),new BABYLON.Vector3(0,-0.01,0)];
        var lines=new BABYLON.MeshBuilder.CreateLineSystem("LineSystem",{lines:[points1,points2,points3,points4]});
        lines.isPickable=false;
        lines.parent=node_hand;
        lines.isVisible=false;
        lines.renderingGroupId=3;
        lines.color="green";
        camera0.lines=lines;
        lines.alwaysSelectAsActiveMesh=true;

        var mat_green=new BABYLON.StandardMaterial("mat_green", scene);
        mat_green.diffuseColor = new BABYLON.Color3(0, 1, 0);
        mat_green.freeze();
        mat_global.mat_green=mat_green;
        var mat_frame=new BABYLON.StandardMaterial("mat_frame", scene);
        mat_frame.wireframe=true;
        mat_frame.freeze();
        mat_global.mat_frame=mat_frame;
        var mat_white_e=new BABYLON.StandardMaterial("mat_white_e", scene);
        mat_white_e.disableLighting = true;
        mat_white_e.emissiveColor = BABYLON.Color3.White();
        mat_white_e.backFaceCulling=false;
        mat_white_e.freeze();
        mat_global.mat_white_e=mat_white_e;
    }

包括对全局光源(本次实验采用自发光材质)、相机以及相机附属物(包括相机的准星、相机周围的参考点,还可能包括小地图、代表玩家自身的模型等)、全局材质的初始化

2、场地初始化

async function initArena()
    {
        // var skybox = BABYLON.Mesh.CreateBox("skyBox", 1500.0, scene);//尺寸存在极限,设为15000后显示异常
        // var skyboxMaterial = new BABYLON.StandardMaterial("skyBox", scene);
        // skyboxMaterial.backFaceCulling = false;
        // skyboxMaterial.reflectionTexture = new BABYLON.CubeTexture("./assets/image/SKYBOX/skybox", scene);
        // skyboxMaterial.reflectionTexture.coordinatesMode = BABYLON.Texture.SKYBOX_MODE;
        // skyboxMaterial.diffuseColor = new BABYLON.Color3(0, 0, 0);
        // skyboxMaterial.specularColor = new BABYLON.Color3(0, 0, 0);
        // skyboxMaterial.disableLighting = true;
        // skybox.material = skyboxMaterial;
        // skybox.renderingGroupId = 1;
        // skybox.isPickable=false;
        // skybox.infiniteDistance = true;

        HK=await HavokPhysics();
        physicsPlugin = new BABYLON.HavokPlugin();
        scene.enablePhysics(new BABYLON.Vector3(0, -9.8, 0), physicsPlugin);
        observable = physicsPlugin.onCollisionObservable;
        var observer = observable.add((collisionEvent) => {
            //fight0(collisionEvent);//碰撞事件
        });



        var mesh_ground = BABYLON.MeshBuilder.CreateGround("ground", {width: 128, height: 128}, scene);
        mesh_ground.alwaysSelectAsActiveMesh = true;
        //mesh_ground.renderingGroupId=2;//renderingGroupId会与htmlmesh冲突吗?
        mesh_ground.position.x=0;
        mesh_ground.position.z=0;
        var mat = new BABYLON.StandardMaterial("mat_ground", scene);//1
        mat.disableLighting = true;
        mat.emissiveTexture = new BABYLON.Texture("./assets/image/LANDTYPE/terre.png", scene);
        mat.emissiveTexture.uScale = 8;
        mat.emissiveTexture.vScale = 8;
        mat.freeze();
        mat_global.mat_ground=mat;
        mesh_ground.material = mat;
        mesh_ground.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
            , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
        mesh_ground.physicsImpostor.shape.filterMembershipMask=filter_group_g;
        mesh_ground.myType="ground";
        mesh_ground.myType1="base";
        //mesh_ground.physicsImpostor.shape.filterCollideMask=filter_group_g;
        //四周的围栏,它们是一直存在的,随着用户的操作还会生成一些临时的围栏
        var mesh_ground1 = BABYLON.MeshBuilder.CreateGround("ground1", {width: 128, height: 10}, scene);
        mesh_ground1.position.x=0;
        mesh_ground1.position.z=64;
        mesh_ground1.rotation.x=Math.PI/2;
        mesh_ground1.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
            , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
        mesh_ground1.physicsImpostor.body.mesh_ground=mesh_ground;
        mesh_ground1.physicsImpostor.shape.filterMembershipMask=filter_group_g;
        mesh_ground1.material=mat_global.mat_white_e;
        mesh_ground1.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
        var mesh_ground2 = BABYLON.MeshBuilder.CreateGround("ground2", {width: 128, height: 10}, scene);
        mesh_ground2.position.x=0;
        mesh_ground2.position.z=-64;
        mesh_ground2.rotation.x=Math.PI/2;
        mesh_ground2.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
            , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
        mesh_ground2.physicsImpostor.body.mesh_ground=mesh_ground;
        mesh_ground2.physicsImpostor.shape.filterMembershipMask=filter_group_g;
        mesh_ground2.material=mat_global.mat_white_e;
        mesh_ground2.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
        var mesh_ground3 = BABYLON.MeshBuilder.CreateGround("ground3", {width: 10, height: 128}, scene);
        mesh_ground3.position.x=-64;
        mesh_ground3.position.z=0;
        mesh_ground3.rotation.z=Math.PI/2;
        mesh_ground3.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
            , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
        mesh_ground3.physicsImpostor.body.mesh_ground=mesh_ground;
        mesh_ground3.physicsImpostor.shape.filterMembershipMask=filter_group_g;
        mesh_ground3.material=mat_global.mat_white_e;
        mesh_ground3.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
        var mesh_ground4 = BABYLON.MeshBuilder.CreateGround("ground4", {width: 10, height: 128}, scene);
        mesh_ground4.position.x=64;
        mesh_ground4.position.z=0;
        mesh_ground4.rotation.z=Math.PI/2;
        mesh_ground4.physicsImpostor=new BABYLON.PhysicsAggregate(mesh_ground, BABYLON.PhysicsShapeType.MESH
            , { mass: 0, restitution: 0.1 ,friction:0.9,move:false,margin:0}, scene);
        mesh_ground4.physicsImpostor.body.mesh_ground=mesh_ground;
        mesh_ground4.physicsImpostor.shape.filterMembershipMask=filter_group_g;
        mesh_ground4.material=mat_global.mat_white_e;
        mesh_ground4.sideOrientation=BABYLON.Mesh.DOUBLESIDE;
        initCrowd([mesh_ground]);
        initMap();

    }

包括对天空盒、物理引擎(可选)、地面网格的初始化,本次实验中还包括对导航群组和游戏地图的初始化。

此处还需注意两点:

a、bbl默认支持四级renderingGroupId,一般可用0级渲染隐形物体,1级渲染无限远处物体,2级渲染普通物体,3级渲染强调物体。但本实验预计使用htmlMesh的特性与renderingGroupId属性冲突,故注释renderingGroupId属性配置。

b、物理引擎与导航网格的物理模拟功能相似,区别在于前者适用于需计算多个物体多维度相互碰撞作用的场景,后者则适用于多个物体相互阻挡进行寻路的场景,一般的模拟程序选用其中一种即可,但也可以根据实际需要同时使用这两种基于物理效果的模拟。

3、控制初始化

InitMouse方法位于ControlWH.js文件中:

  1 var node_temp,rate_fov,rate_screen;
  2 var sr_global=null;
  3 var pos_stack_one,pos_stack_rts;
  4 function InitMouse()
  5 {
  6     rate_fov=Math.tan(camera0.fov/2)*2;
  7     var sizex=engine.getRenderWidth()//_gl.drawingBufferWidth;
  8     var sizey=engine.getRenderHeight()//_gl.drawingBufferHeight;
  9     rate_screen=sizex/sizey;
 10     canvas.addEventListener("blur",function(evt){//监听失去焦点
 11         releaseKeyStateOut();
 12     })
 13     canvas.addEventListener("focus",function(evt){//改为监听获得焦点,因为调试失去焦点时事件的先后顺序不好说
 14         releaseKeyStateIn();
 15     })
 16     // canvas.addEventListener("click", function(evt) {//这个监听也会在点击GUI按钮时触发!!<-click仅指鼠标左键单击??!!
 17     //     onMouseClick(evt);//
 18     // }, false);
 19     // canvas.addEventListener("dblclick", function(evt) {//是否要用到鼠标双击??
 20     //     onMouseDblClick(evt);//
 21     // }, false);
 22     canvas.addEventListener("pointermove",onMouseMove,false)
 23     canvas.addEventListener("pointerdown",onMouseDown,false)
 24     canvas.addEventListener("pointerup",onMouseUp,false)
 25     window.addEventListener("keydown", onKeyDown, false);//按键按下
 26     window.addEventListener("keyup", onKeyUp, false);//按键抬起
 27     window.onmousewheel=onMouseWheel;
 28     window.addEventListener("resize", function () {//canvas没有resize事件!
 29         if (engine) {
 30             engine.resize();
 31             var sizex=engine.getRenderWidth()//_gl.drawingBufferWidth;
 32             var sizey=engine.getRenderHeight()//_gl.drawingBufferHeight;
 33             var rate=sizex/sizey;
 34             rate_screen=rate;
 35             var size_minimap=150;
 36             if(sr_global)
 37             {
 38                 console.log("resize and reset");
 39                 sr_global.enableSnapshotRendering();
 40             }
 41         }
 42     },false);
 43     node_temp=new BABYLON.TransformNode("node_temp",scene);//用来提取相机的姿态矩阵
 44     node_temp.rotation=camera0.rotation;
 45 }
 46 function onMouseDblClick(evt)
 47 {
 48     //var pickInfo = scene.pick(scene.pointerX, scene.pointerY, null, false, camera0);
 49 }
 50 function onMouseClick(evt)
 51 {
 52     //console.log("onMouseClick");
 53     //onMouseClick会在onMouseDown和onMouseUp之后发生!所以拖拽被错误的识别为点击!!
 54     //if(!flag_drag)
 55     //evt.preventDefault();
 56 
 57 }
 58 var lastPointerX,lastPointerY;
 59 var flag_view="rts";
 60 var flag_view_index=0;//建立两重切换:按V键时在当前位置切换为自由相机,并把原位置和控制方式入栈,《-数字按键要预留其他作用!
 61 var arr_flag_view=[{key:"i",view:"rts"},{key:"o",view:"free"},{key:"p",view:"one"},{key:"Escape",view:"free"}];//iop三键?
 62 var obj_keystate=[];
 63 var pso_stack;
 64 var flag_moved=false;//在拖拽模式下有没有移动,如果没移动则等同于click
 65 var point0,point;//拖拽时点下的第一个点
 66 var MaxMovement;
 67 function onMouseMove(evt)
 68 {
 69     if(!evt)
 70     {
 71         evt={}
 72         evt.clientX=scene.pointerX;
 73         evt.clientY=scene.pointerY;
 74     }
 75 
 76     if(flag_view=="rts"&&flag_drag)
 77     {
 78         if(evt.preventDefault)
 79             evt.preventDefault();
 80         //var dx=-(scene.pointerX-lastPointerX)/window.innerWidth;
 81         //var dz=(scene.pointerY-lastPointerY)/window.innerHeight;
 82         var dx=-(evt.clientX-lastPointerX)/canvas.width;//engine.getRenderWidth()
 83         var dz=(evt.clientY-lastPointerY)/canvas.height;
 84         //Field Of View is set in Radians. (default is 0.8)
 85         camera0.position.z+=dz*(camera0.position.y)*rate_fov;
 86         camera0.position.x+=dx*(camera0.position.y)*rate_fov*rate_screen;
 87         // camera0.position.z+=dz*(camera0.position.y/600)*600;
 88         // camera0.position.x+=dx*(camera0.position.y/600)*600;
 89     }
 90     else if(flag_view=="one")
 91     {
 92         if(evt.preventDefault)
 93             evt.preventDefault();
 94         //绕y轴的旋转角度是根据x坐标计算的
 95         // var rad_y=((evt.clientX-lastPointerX)/canvas.width)*(Math.PI/1);
 96         // var rad_x=((evt.clientY-lastPointerY)/canvas.height)*(Math.PI/1);
 97         var rad_y=((evt.movementX)/canvas.width)*(Math.PI/1);
 98         var rad_x=((evt.movementY)/canvas.height)*(Math.PI/1);
 99         camera0.myRotation.y+=rad_y;
100         camera0.myRotation.x+=rad_x;
101         //console.log(evt.movementX);
102         //one.node.rotation=camera0.rotation.clone();
103     }
104     lastPointerX=evt.clientX;
105     lastPointerY=evt.clientY;
106 }
107 var flag_drag=false;
108 var downPointerX,downPointerY;
109 var count_myController=0;
110 function onMouseDown(evt)
111 {
112     //bbl内置的pick事件导致半秒钟的延迟?!
113     //不是gui或瘦实例和精灵导致的延迟,后续测试把pointerdown事件关闭
114     //console.log("onMouseDown");
115     //evt.preventDefault();
116     if(flag_view=="rts")
117     {
118 
119         flag_drag=true;
120     }
121 }
122 function onMouseUp(evt)
123 {
124     //console.log("onMouseUp");
125     //evt.preventDefault();
126     if(flag_view=="rts")
127     {
128 
129         flag_drag=false;
130     }
131     //左键框选和左键拖动相冲突,可能要添加基于meshname和额外按键的功能判定
132     if(evt.button==2)
133     {
134         //这里的点击是指针点击而不是准心点击
135         //优先点击精灵?
136         var pickResult = scene.pickSprite(evt.clientX, evt.clientY, null, false, camera0);
137         if (pickResult.hit) {
138             console.log(pickResult);
139             console.log(pickResult.pickedSprite.name);
140             //console.log(pickResult);
141         }
142         else {
143             var pickInfo = scene.pick(evt.clientX, evt.clientY, null, false, camera0);
144             if(pickInfo.hit)
145             {
146                 //console.log(pickInfo);
147                 //console.log(pickInfo.thinInstanceIndex);
148                 //console.log(pickInfo.pickedMesh.id);
149                 if(pickInfo.pickedMesh.myType=="ground")
150                 {
151                     var len=agents.length;
152                     for (let i = 0; i < len; i++) {
153                         crowd.agentGoto(agents[i], navigationPlugin.getClosestPoint(pickInfo.pickedPoint))
154                     }
155                 }
156             }
157         }
158     }
159 }
160 function onKeyDown(event)
161 {
162     if(flag_view=="one") {
163         event.preventDefault();
164         var key = event.key;
165         obj_keystate[key] = 1;
166         if(obj_keystate["Shift"]==1)
167         {
168             obj_keystate[key.toLowerCase()] = 1;
169         }
170     }
171 }
172 function handleView(lastView,nextView)
173 {
174     if(nextView=="free")
175     {
176         flag_drag=false;
177         camera0.attachControl(canvas, true);
178         if(lastView=="rts")
179         {
180             pos_stack_rts=camera0.position.clone();
181         }
182         else if(lastView=="one")
183         {
184             document.exitPointerLock();
185             if(camera0.lines)
186             {
187                 camera0.lines.isVisible=false;
188             }
189         }
190         flag_view=nextView;
191         releaseKeyStateIn();
192     }
193     else if(nextView=="rts")
194     {
195         camera0.detachControl();
196         if(pos_stack_rts)
197         {
198             camera0.position=pos_stack_rts;
199             camera0.rotation=new BABYLON.Vector3(hd_camera0,0,0);
200         }
201         if(lastView=="one")
202         {
203             document.exitPointerLock();
204             if(camera0.lines)
205             {
206                 camera0.lines.isVisible=false;
207             }
208         }
209         flag_view=nextView;
210         releaseKeyStateIn();
211     }
212     else if(nextView=="one")
213     {
214         if(one)
215         {
216             flag_drag=false;
217             camera0.detachControl();
218             camera0.position=one.node.node_back.getAbsolutePosition();
219             camera0.rotation=one.node.rotation.clone();
220             canvas.requestPointerLock(options = {unadjustedMovement: false});
221             //camera0.rotation.x=0;
222             //camera0.rotation.y=one.node.rotation.y;
223             if(camera0.lines)
224             {
225                 camera0.lines.isVisible=true;
226             }
227             flag_view=nextView;
228         }//如果没有可驾驶的“载具”则这个物理运动方式不能生效!!
229         else
230         {
231 
232         }
233     }
234     //sr_global.enableSnapshotRendering();
235 }
236 function onKeyUp(event)
237 {
238     var key = event.key;
239     var len=arr_flag_view.length;
240     for(var i=0;i<len;i++)//切换视角
241     {
242         var view=arr_flag_view[i];
243         if(key==view.key)
244         {
245             event.preventDefault();
246             if(flag_view==view.view)
247             {
248 
249             }
250             else {
251 
252                 handleView(flag_view,view.view);
253 
254             }
255             break;
256         }
257     }
258 
259     if(flag_view=="rts") {
260         event.preventDefault();
261         var key2=key.toLowerCase();
262         if(key2=="w")
263         {
264             camera0.position.z+=step_move*(camera0.position.y/1800);
265             if(camera0.position.z>10000)
266             {
267                 camera0.position.z=10000
268             }
269         }
270         else if(key2=="s")
271         {
272             camera0.position.z-=step_move*(camera0.position.y/1800)
273             if(camera0.position.z<-10000)
274             {
275                 camera0.position.z=-10000
276             }
277         }
278         else if(key2=="a")
279         {
280             camera0.position.x-=step_move*(camera0.position.y/1800)
281             if(camera0.position.x<-10000)
282             {
283                 camera0.position.x=-10000
284             }
285         }
286         else if(key2=="d")
287         {
288             camera0.position.x+=step_move*(camera0.position.y/1800)
289             if(camera0.position.x>10000)
290             {
291                 camera0.position.x=10000
292             }
293         }
294     }
295     else if(flag_view=="one")
296     {
297         event.preventDefault();
298 
299         obj_keystate[key] = 0;
300         if(key=="f")
301         {
302             one.mesh.physicsImpostor.body.setLinearVelocity(new BABYLON.Vector3(0,0,0));
303             one.mesh.physicsImpostor.body.setAngularVelocity(new BABYLON.Vector3(0,0,0));
304         }
305         else if(key=="c")
306         {
307             if(one.node.node_back.position.y==1)
308             {
309                 one.node.node_back.position.y=0.5;
310             }
311             else {
312                 one.node.node_back.position.y=1;
313             }
314         }
315         else if(key==" ")
316         {
317             var pos=one.mesh.getAbsolutePosition();
318             var ray =BABYLON.Ray.CreateNewFromTo(pos,new BABYLON.Vector3(pos.x,-1,pos.z));
319             var hit=scene.pickWithRay(ray,(mesh)=>(mesh.id!="node_one"&&mesh.id!="mesh_one"));
320             if(hit&&hit.pickedMesh&&hit.distance<1)
321             {
322                 console.log(hit.pickedMesh.id,pos);
323                 one.mesh.physicsImpostor.body.applyForce(new BABYLON.Vector3(0,600,0),pos);
324             }
325 
326 
327         }
328     }
329 }
330 
331 
332 function onMouseWheel(event){
333     //var delta =event.wheelDelta/120;
334     if(flag_view=="rts"||flag_view=="free")
335     {
336         if(flag_drag==false)//禁止一边拖拽一边缩放
337         {
338             //事件的兼容性写法
339             var oEvent = event || window.event;
340             //oEvent.preventDefault();
341             if(oEvent.wheelDelta){//非火狐
342                 if(oEvent.wheelDelta > 0){//向上滚动
343                     if(int_z<16)
344                     {
345                         int_z++;
346                     }
347 
348                 }else{//向下滚动
349                     //if(int_z>0)
350                     {
351                         int_z--;
352                     }
353                 }
354             }else if(oEvent.detail){
355                 if(oEvent.detail > 0){//向下滚动
356                     //if(int_z>0)
357                     {
358                         int_z--;
359                     }
360                 }else{//向上滚动
361                     if(int_z<16)
362                     {
363                         int_z++;
364                     }
365 
366                 }
367             }
368             int0b=Math.pow(2,-int_z/4);//向上滚动,拉近变细,int_z越大y越小
369             //var int=Math.floor(Math.pow(2,int_z/4));//放大倍数
370 
371         }
372     }
373 
374 }
375 function onContextMenu(evt)
376 {
377 
378 }
379 
380 function movePOV(node,node2,vector3)
381 {
382     var m_view=node.getWorldMatrix();
383     var v_delta=BABYLON.Vector3.TransformCoordinates(vector3,m_view);
384     var pos_temp=node2.position.add(v_delta);
385     node2.position=pos_temp;
386 }
387 function releaseKeyStateIn(evt)
388 {
389     for(var key in obj_keystate)
390     {
391         obj_keystate[key]=0;
392     }
393     //lastPointerX=scene.pointerX;
394     //lastPointerY=scene.pointerY;
395     flag_drag=false;
396     camera0.myRotation={x:0,y:0,z:0};
397 }
398 function releaseKeyStateOut()
399 {
400     for(var key in obj_keystate)
401     {
402         obj_keystate[key]=0;
403     }
404     flag_drag=false;
405     camera0.myRotation={x:0,y:0,z:0};
406 }
407 
408 var pos_last;
409 var delta;
410 var v_delta;
411 var x_lastquery,z_lastquery,y_lastquery;
412 var count_initasync=2;//有两项异步初始化内容
413 function MyBeforeRender0()
414 {
415     MyBeforeRender();
416 }
417 //var sr_global=null;
418 var sceneInstrumentation;
419 var count_lost=0;
420 var flag_lost=false;
421 var bool_render=true;
422 var flag_showlayerindex=false;
423 function MyBeforeRender()
424 {
425     //sr_global= new BABYLON.SnapshotRenderingHelper(scene);
426     //@@@@engine.resize();//似乎要以某种方式重建canvas才可使用快速SR,否则会在一秒后丢失内容
427     //是某些与SR有关的变量失去了引用,触发了浏览器的CPP GC,在调试模式下则可能是一直有引用存在的!!!!
428     //sr_global.enableSnapshotRendering();
429     //int0=1*(camera0.position.y/600);
430     //int0b=int0;
431 
432     console.log("MyBeforeRender");
433     //sceneInstrumentation = new BABYLON.SceneInstrumentation(scene);//用来进行计时
434     //sceneInstrumentation.captureFrameTime = true;
435     engine.hideLoadingUI();
436     //sr_global.enableSnapshotRendering();
437 
438 
439     // setTimeout(function(){
440     //     canvas.width=canvas.width-1;
441     //     engine.resize();
442     //     sr_global.enableSnapshotRendering();
443     // },1000)
444 
445     scene.registerBeforeRender(
446         function(){
447             if(skybox)
448             {
449                 //sr_global.updateMesh(skybox);
450             }
451 
452 
453             if(flag_view=="rts"||flag_view=="free")
454             {
455                 if(int0b!=int0)
456                 {
457 
458                     if(int0b<1)
459                     {
460                         //reDrawThings(int0b);//在事件中触发会造成屏幕闪烁!!
461 
462                     }
463 
464                 }
465             }
466             if(flag_view=="one")
467             {
468                 //one.node.rotation.y=camera0.rotation.y;
469                 var flag_speed=2;
470                 //var m_view=node_temp.getWorldMatrix();
471                 if(obj_keystate["Shift"]==1)//Shift+w的event.key不是Shift和w,而是W!!!!
472                 {
473                     flag_speed=10;
474                 }
475                 delta=engine.getDeltaTime();
476                 flag_speed=flag_speed*engine.getDeltaTime()/10;
477                 var v_temp=new BABYLON.Vector3(0,0,0);//用它来改变物体的受力状态
478                 if(obj_keystate["w"]==1)
479                 {
480                     v_temp.z+=1*flag_speed;
481 
482                 }
483                 if(obj_keystate["s"]==1)
484                 {
485                     v_temp.z-=1*flag_speed;
486                 }
487                 if(obj_keystate["d"]==1)
488                 {
489                     v_temp.x+=0.5*flag_speed;
490                 }
491                 if(obj_keystate["a"]==1)
492                 {
493                     v_temp.x-=0.5*flag_speed;
494                 }
495                 if(v_temp.x!=0||v_temp.z!=0)
496                 {
497                     //console.log("force",v_temp)
498                     var force=newland.vecToGlobal(v_temp,one.node);
499                     force=force.subtract(one.node.getAbsolutePosition()).scale(1);//取姿态信息,去除位置信息
500                     var pos=one.node.getAbsolutePosition();
501                     one.mesh.physicsImpostor.body.applyForce(force,pos);
502                 }
503                 //var force=new BABYLON.Vector3(0,-1,0);
504                 //one.node.position=one.mesh.position.clone();//此修改可改善显示体落后于仿真体的情况,但在高速运动时会发生剧烈抖动,相反的落后式设置在极高速时则保持平稳
505                 //v_delta=BABYLON.Vector3.TransformCoordinates(v_temp,m_view);
506             }
507         }
508     )
509     scene.registerAfterRender(
510         function(){
511 
512             for(var key in obj_agentTrans)//对于每一个导航代理的变换节点
513             {
514                 var agentTrans=obj_agentTrans[key];
515                 var agentIndex=agentTrans.mydata.agentIndex;
516                 var maxSpeed=agentTrans.mydata.maxSpeed;//当前速度
517                 if(crowd._agentDestinationArmed[agentIndex])
518                 //if(crowd.getAgentNextTargetPath(agentIndex))//如果这个导航器有下一移动目标
519                 {
520                     var flag_found=false;
521                     var position=crowd.getAgentPosition(agentIndex);
522                     for(var key2 in obj_maparea)
523                     {
524                         if(flag_found)
525                         {
526                             break;
527                         }
528                         // if(key2==maxSpeed)//相同的速度区域不必重复考虑<-也要判断是否在同速区域内,如不在同速区则可能在base区域中!!!!
529                         // {
530                         //     continue;
531                         // }
532                         var arr_path=obj_maparea[key2];
533                         var len2=arr_path.length;
534                         for(var j=0;j<len2;j++)
535                         {
536                             var path=arr_path[j];
537                             if(queryPtInPolygon({x:position.x,y:position.z},path))//如果在这个速度区域内
538                             {
539                                 if(key2!=(maxSpeed+""))
540                                 {
541                                     var v2=parseFloat(key2);
542                                     agentTrans.mydata.maxSpeed=v2;
543                                     agentParams.maxSpeed=v2;
544                                     crowd.updateAgentParameters(agentIndex,agentParams);
545                                 }
546 
547                                 flag_found=true;
548                                 break;
549                             }
550                         }
551                     }
552                     if(!flag_found&&maxSpeed!=baseSpeed)
553                     {
554                         agentTrans.mydata.maxSpeed=baseSpeed;
555                         agentParams.maxSpeed=baseSpeed;
556                         crowd.updateAgentParameters(agentIndex,agentParams);
557                     }
558                 }
559 
560             }
561             if(flag_view=="rts"||flag_view=="free")
562             {
563                 if(int0b!=int0)
564                 {
565                     camera0.position.y=600*int0b;
566                     int0=int0b;//更新上一放大倍数
567                 }
568             }
569 
570             if(camera0.position.y<75)//较近时显示文字,较远时隐藏
571             {
572                 if(!flag_showlayerindex)
573                 {
574                     flag_showlayerindex=true;
575                     objSpriteManager.manager.renderingGroupId=2;//精灵管理器是否受快照影响?
576                     //sr_global.enableSnapshotRendering();
577                 }
578             }
579             else
580             {
581                 if(flag_showlayerindex)
582                 {
583                     flag_showlayerindex=false;
584                     objSpriteManager.manager.renderingGroupId=0;
585                     //sr_global.enableSnapshotRendering();
586                 }
587             }
588         }
589     )
590     engine.runRenderLoop(function () {
591         //engine.hideLoadingUI();
592         var int_fps=engine.getFps().toFixed();
593         if (divFps) {
594             divFps.innerHTML =  int_fps+ " fps";
595         }
596 
597         //sr_global.updateMesh(one.node);//物理引擎似乎不受快照模式限制??!!
598         //sr_global.updateMesh(one.mesh);
599         //if(flag_runningstate=="fastsr")
600         //{
601         scene.render();
602         //}
603 
604     });
605 }
606 function sort_compare(a,b)//从近到远
607 {
608     return a.distance-b.distance;
609 }
610 function sort_compare2(a,b)//从远到近
611 {
612     return b.distance-a.distance;
613 }
614 var flag_text_near=false;
View Code

这一文件中包含对页面失去/获得焦点、光标移动、光标按下、光标抬起、按键按下、按键抬起、鼠标滚轮滚动、窗口大小变化等事件的监听,这些监听都是直接对html元素本身的监听,而不使用bbl内置的scene对象事件监听,因为bbl内置的光标事件会默认进行射线检测,在一些场景下会产生较大的延迟。

每一种事件可能在不同“控制模式下”对应不同的响应方法,例如在rts控制模式下拖动鼠标的响应是移动地图,在自由相机模式下则是改变视角,每一种事件的响应中都需根据不同的控制模式编写对应的响应代码。

web3D中常见的控制模式包括:bbl内置的自由相机和弧形旋转相机、rts、带有物理效果的第一或第三人称控制(例如https://gitee.com/ljzc002/maze)、带有z轴自由度的浮空姿态变换(例如https://www.cnblogs.com/ljzc002/p/11589973.html)等,可使用不同按键在多种控制模式间切换。

该文件还包含对渲染循环的初始化:

registerBeforeRender、registerAfterRender、runRenderLoop分别表示渲染前执行(此时各种与渲染有关的内置变量并未变化)、渲染后执行、渲染时执行。这里没有使用bbl官方推荐的观察者模式,目的是将这些循环代码整理在一处避免分散。

4、gui初始化

initGuiControl方法位于ControlGui.js文件中:

  1 var step_move=10;
  2 var advancedTexture,global_panel_text,global_panel2;
  3 //在移动端情况下,机体上没有实体按键,故gui按钮为必须选择,且在不同控制模式下gui按钮须有不同作用!
  4 //在free模式下需要仿制freecamera作选定方向运动,在one模式下需作物理引擎推动,在rts模式下需作水平卷动
  5 function initGuiControl()
  6 {
  7     advancedTexture = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI("ui1");
  8     advancedTexture.layer.layerMask=1;
  9     var camera=camera0;
 10     //左侧的运动控制按钮
 11     var panel2=new BABYLON.GUI.Rectangle();
 12     panel2.width="200px"//0.25;
 13     panel2.top="10px";
 14     panel2.left="10px";
 15     panel2.height="170px"//0.25;
 16     panel2.horizontalAlignment=BABYLON.GUI.Control.HORIZONTAL_ALIGNMENT_LEFT;
 17     panel2.verticalAlignment=BABYLON.GUI.Control.VERTICAL_ALIGNMENT_TOP;
 18     panel2.thickness=0
 19     advancedTexture.addControl(panel2);
 20     global_panel2=panel2;
 21     var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_fw","复位");
 22     //button_fw.width=0.2;
 23     button_fw.height="40px";
 24     button_fw.width="40px";
 25     //button_fw.top="10px";
 26     button_fw.left="-20px";
 27     button_fw.color="white";
 28     button_fw.cornerRadius=20;
 29     button_fw.background="green";
 30     button_fw.onPointerUpObservable.add(function(){
 31         if(flag_view=="rts")//在one模式下没有复位效果?
 32         {
 33 
 34             camera.position=initpos_camera0.clone();
 35             camera.rotation=initrot_camera0.clone();
 36         }
 37         else if(flag_view=="free")
 38         {
 39             if(pos_stack_rts)
 40             {
 41                 camera.position=pos_stack_rts;
 42                 //camera0.rotation=new BABYLON.Vector3(hd_camera0,0,0);
 43             }
 44 
 45         }
 46         else if(flag_view=="one")
 47         {
 48             one.node.rotation=new BABYLON.Vector3(0,0,0);
 49             camera0.position=one.node.node_back.getAbsolutePosition();
 50             camera0.rotation=one.node.rotation.clone();
 51 
 52         }
 53     });
 54     panel2.addControl(button_fw);
 55 
 56     var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_q","前");
 57     //button_fw.width=0.2;
 58     button_fw.height="40px";
 59     button_fw.width="40px";
 60     button_fw.left="-20px";
 61     button_fw.top="-60px";
 62 
 63     button_fw.color="white";
 64     button_fw.cornerRadius=20;
 65     button_fw.background="green";
 66     button_fw.onPointerUpObservable.add(function(){
 67         if(flag_view=="rts")
 68         {
 69             camera.position.z=camera.position.z+step_move;
 70         }
 71     });
 72     panel2.addControl(button_fw);
 73 
 74     var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_h","后");
 75     //button_fw.width=0.2;
 76     button_fw.height="40px";
 77     button_fw.width="40px";
 78     button_fw.left="-20px";
 79     button_fw.top="60px";
 80     button_fw.color="white";
 81     button_fw.cornerRadius=20;
 82     button_fw.background="green";
 83     button_fw.onPointerUpObservable.add(function(){
 84         if(flag_view=="rts")
 85         {
 86 
 87             camera.position.z=camera.position.z-step_move;
 88         }
 89     });
 90     panel2.addControl(button_fw);
 91 
 92     var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_s","上");
 93     //button_fw.width=0.2;
 94     button_fw.height="40px";
 95     button_fw.width="40px";
 96     button_fw.left="80px";
 97     button_fw.top="-60px";
 98     button_fw.color="white";
 99     button_fw.cornerRadius=20;
100     button_fw.background="green";
101     button_fw.onPointerUpObservable.add(function(){
102         if(flag_view=="rts")
103         {
104             int_z--;
105             int0b=Math.pow(2,-int_z/4);
106             //camera.position.y=camera.position.y+step_move;
107         }
108     });
109     panel2.addControl(button_fw);
110 
111     var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_x","下");
112     //button_fw.width=0.2;
113     button_fw.height="40px";
114     button_fw.width="40px";
115     button_fw.left="80px";
116     button_fw.top="60px";
117     button_fw.color="white";
118     button_fw.cornerRadius=20;
119     button_fw.background="green";
120     button_fw.onPointerUpObservable.add(function(){
121         if(flag_view=="rts")
122         {
123             if(int_z<16)
124             {
125                 int_z++;
126             }
127             //camera.position.y=camera.position.y-step_move;
128             int0b=Math.pow(2,-int_z/4);
129         }
130     });
131     panel2.addControl(button_fw);
132 
133     var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_z","左");
134     //button_fw.width=0.2;
135     button_fw.height="40px";
136     button_fw.width="40px";
137     button_fw.left="-80px";
138     //button_fw.top="-40px";
139     button_fw.color="white";
140     button_fw.cornerRadius=20;
141     button_fw.background="green";
142     button_fw.onPointerUpObservable.add(function(){
143         if(flag_view=="rts")
144         {
145 
146             camera.position.x=camera.position.x-step_move;
147 
148         }
149     });
150     panel2.addControl(button_fw);
151 
152     var button_fw=BABYLON.GUI.Button.CreateSimpleButton("button_y","右");
153     //button_fw.width=0.2;
154     button_fw.height="40px";
155     button_fw.width="40px";
156     button_fw.left="40px";
157     button_fw.color="white";
158     button_fw.cornerRadius=20;
159     button_fw.background="green";
160     button_fw.onPointerUpObservable.add(function(){
161         if(flag_view=="rts")
162         {
163 
164             camera.position.x=camera.position.x+step_move;
165         }
166     });
167     panel2.addControl(button_fw);
168 }
View Code

其作用是在三维场景中绘制按钮、文本框等控件,bbl内置gui的功能不如html标签丰富,使用难度也更高,其优点是只存在于3D场景中,不与canvas之外的内容发生关系。

5、启动渲染循环前的其他初始化操作

function webGLStart2()
    {
        flag_runningstate="场景初始化完成";
        //sr_global= new BABYLON.SnapshotRenderingHelper(scene);
        createManagers();

        //scene.debugLayer.show();
        //initStellaris();
        MyBeforeRender0();
        //scene.debugLayer.show();
    }

根据实际需求编写,例如这里createManagers方法用来生成精灵管理器,scene.debugLayer.show();语句则是用来启动bbl的内置调试工具。

6、用精灵管理器实现大量汉字渲染

bbl官方推荐使用gui或自定义纹理在场景中显示文字,但二者均在渲染大量分离文字时,存在渲染速度慢和定位困难问题,本文采用精灵管理器实现rts环境下的大量汉字渲染功能,其代码位于drawSprites.js文件中,cyhz.js文件则是常用汉字字库。

 1 var arr_sprites=[];
 2 var char_global="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz01234567890+-_.|"+str_cyhz;//通用字符+常用汉字字库
 3 var obj_czpos={},objSpriteManager={};
 4 //要取常用的星系名!
 5 function createManagers()
 6 {
 7     for(var zm in obj_czpos) {//一些场景中额外定义的可操作对象
 8         var len=zm.length;
 9         for(var i=0;i<len;i++)
10         {
11             if(char_global.indexOf(zm[i])==-1)//如果字表中还没有这个字
12             {
13                 char_global+=zm[i];
14             }
15         }
16     }
17     var can_temp_label=document.createElement("canvas");
18     var len=char_global.length;
19     var fontsize=32;
20     var color="#ffffff";//后期染色?
21     can_temp_label.width=4096;//可能是计算误差?粒子系统取的方块比canvas的方块小一点?一共4000多个字,最大估计10000个字?
22     can_temp_label.height=4096;//预设7225个字的空间!
23     var context=can_temp_label.getContext("2d");
24     context.fillStyle="rgba(0,0,0,0)";//完全透明的背景
25     context.fillRect(0,0,can_temp_label.width,can_temp_label.height);
26     context.fillStyle = color;
27     context.font = "bold "+fontsize+"px monospace";
28     for(var i=0;i<len;i++)
29     {
30         var char=char_global[i];
31         var x=(i%128)*fontsize;//一行85个字,在达到上百个字后误差比较明显《-这个误差是非整数跨行导致的
32         //要让半角字符和全角字符和谐显示,一种可用的方法是修改这个横坐标计算规则?<-在实际添加精灵时修改
33         var y=(Math.ceil((i+0.5)/128))*fontsize;
34         context.fillText(char,x,y-3);
35     }
36     var png=can_temp_label.toDataURL("image/png");//生成PNG图片
37     //console.log(png);
38     //建立精灵管理器
39     var spriteManager = new BABYLON.SpriteManager("spriteManagerLabel", png, 50000, fontsize, scene);
40     spriteManager.renderingGroupId=3;
41     spriteManager.cellWidth=fontsize;
42     spriteManager.cellHeight=fontsize;
43     spriteManager.isPickable = true;
44     //spriteManager.isVisible=false;《-此属性无作用
45     objSpriteManager.manager=spriteManager;
46 }
47 var size_char=12;
48 //在指定位置以精灵方式添加文字
49 function addSpritelabel(str,px,py,id,size,color,manager,arr_sprites,h)
50 {
51     if(!h)
52     {
53         h=2
54     }
55     var len=str.length;
56     var arr=[];
57     var size_offset=0;
58     for(var i=0;i<len;i++)
59     {
60         var char=str[i];
61         var s_char=new BABYLON.Sprite("s_big_"+id+"_"+i, manager);
62         var index=char_global.indexOf(char);
63         if(index<0)
64         {
65             index=0;//用A补位
66         }
67         s_char.cellIndex=index;
68         s_char.size=size;
69         s_char.isPickable=true;
70         s_char.position=new BABYLON.Vector3(px+size_offset,h,py);
71         if(index<67)
72         {
73             s_char.isBJ=true;//是半角字符
74             size_offset+=size/2;
75         }
76         else {
77             s_char.isBJ=false;//不是半角字符
78             size_offset+=size;
79         }
80 
81         if(color)
82         {
83             s_char.color=color;
84         }
85         else {
86             s_char.color={r:0,g:0,b:0,a:1}
87         }
88         arr.push(s_char);
89     }
90     var len=arr.length;
91     for(var i=0;i<len;i++)
92     {
93         var s=arr[i];
94         s.position.x-=size_offset/2;
95     }
96     arr_sprites.push({
97         arr:arr,size_offset:size_offset,id:id,str:str,px:px,py:py,color:color
98     })
99 }

其基本思路是将常用字符绘制在一张4096*4096的图片中,然后以这张图片建立带有动画效果的精灵管理器,以精灵动画的每个动画帧索引对应图片中的每个字符小块,如此便可基于精灵的高性能特性实现大量汉字渲染。该方法的缺点是,在非rts控制模式下,用户的视点可能移动到精灵后侧,此时精灵的排列顺序将显示为倒序。该方法也考虑了全角字符和半角字符的宽度差异。

四、实现群组导航

在四年前的这篇文章https://www.cnblogs.com/ljzc002/p/15119505.html中,作者基于recast1群组导航库实现了简单的平面群组导航。相较于前代,recast2的优势主要包括:导航性能提升、多层导航、跳跃与传送、基于瓦片的地块分块更新、更精细的导航网格调试、为每个分块设置不同导航属性等。bbl通过babylonjs.addons.min.js文件引入了这个新版导航库,但目前bbl官方引入还存在缺陷,比如:官方示例错误、分块和调试功能支持不完善等,因此作者对bbl官方示例和babylonjs.addons.min.js库进行了一些自定义修改。

创建导航群组的代码在ControlCrowd.js文件中:

  1 //用来控制群组导航
  2 async function initCrowd(arr_mesh)
  3 {
  4     //尝试使用TileCacheMeshProcess 技术分块构造导航网格
  5     navigationPlugin = await ADDONS.CreateNavigationPluginAsync();//引入导航网格插件,这时会下载@recast-navigation目录下的三个js文件
  6     //const cellSize = 0.05;
  7     const navmeshParameters = {//建立导航网格的参数
  8         cs: 0.1,
  9         ch: 0.2,maxObstacles: 64,
 10         expectedLayersPerTile: 2,
 11         tileCacheMeshProcess:tp,tileSize: 32,
 12     }
 13     const { navMesh,tileCache, navMeshQuery } = await navigationPlugin.createNavMeshAsync(arr_mesh, navmeshParameters);
 14     visualizeCrowd(navMeshQuery);
 15 }
 16 var count_tile;
 17 function tp(navMeshCreateParams,polyAreas,polyFlags){
 18 
 19     const STAIRS_AREA = 1;
 20     const DEFAULT_AREA = 0;
 21     const WALK_FLAG = 1;//这是水平移动消耗?
 22     const STAIRS_FLAG = 10;
 23     count_tile=0;
 24     const vertsCount = navMeshCreateParams.vertCount();
 25     const polyCount=navMeshCreateParams.polyCount();
 26     const indicesCount = polyCount;
 27     var tileX=navMeshCreateParams.tileX();//这个瓦片的水平偏移量?
 28     var tileY=navMeshCreateParams.tileY();
 29     for (let i = 0; i < polyCount; i++) {//对于每个多边形
 30         // compute average y of polygon vertices
 31         const idx0 = navMeshCreateParams.polys(i * 3 + 0);//polys是顶点的索引,例如一个正方形多边形包括四个顶点
 32         const idx1 = navMeshCreateParams.polys(i * 3 + 1);
 33         const idx2 = navMeshCreateParams.polys(i * 3 + 2);//polys(4)是65535
 34         //verts是顶点数据,
 35         //const avgY = (navMeshCreateParams.verts(idx0 * 3 + 1) + navMeshCreateParams.verts(idx1 * 3 + 1) + navMeshCreateParams.verts(idx2 * 3 + 1)) / 3;
 36 
 37         if (tileX>=10) {
 38             polyAreas.set(i,STAIRS_AREA)//当前生效写法
 39             polyFlags.set(i,STAIRS_FLAG)
 40             //polyAreas[i] = STAIRS_AREA;//bbl文档的不生效写法
 41             //polyFlags[i] = STAIRS_FLAG;
 42         } else {
 43             polyAreas.set(i,DEFAULT_AREA)
 44             polyFlags.set(i,WALK_FLAG)
 45             //polyAreas[i] = DEFAULT_AREA;
 46             //polyFlags[i] = WALK_FLAG;
 47         }
 48         count_tile++;
 49     }
 50 
 51 }
 52 var agents,crowd,obj_agentv={};
 53 var baseSpeed=5.0;//默认最大速度为5
 54 var agentParams = {
 55     radius: 0.1 + Math.random() * 0.05,
 56     height: 1.5,
 57     maxAcceleration: 11.0,
 58     maxSpeed: baseSpeed,
 59     separationWeight: 1.0,
 60 }
 61 function visualizeCrowd(navMeshQuery) {
 62     crowd = navigationPlugin.createCrowd(100, 0.15, scene);
 63     obj_agentv={};
 64     for (let i = 0; i < 100; i++) {
 65 
 66         const { randomPoint: position } =
 67             navMeshQuery.findRandomPointAroundCircle({ x: 0, y: 0, z: 0 }, 1);
 68         var flag_found=false;
 69         for(var key in obj_maparea)
 70         {
 71             if(flag_found)
 72             {
 73                 break;
 74             }
 75             var arr_path=obj_maparea[key];
 76             var len2=arr_path.length;
 77             for(var j=0;j<len2;j++)
 78             {
 79                 var path=arr_path[j];
 80                 if(queryPtInPolygon({x:position.x,y:position.z},path))//如果在这个速度区域内
 81                 {
 82                     agentParams.maxSpeed=parseFloat(key);
 83                     flag_found=true;
 84                     break;
 85                 }
 86             }
 87         }
 88         if(!flag_found)
 89         {
 90             agentParams.maxSpeed=baseSpeed;
 91         }
 92         createAgent(agentParams, position, crowd)
 93     }
 94     //agents有固定的id吗?
 95     agents = crowd.getAgents();//这个方法返回的是数字索引!!
 96 
 97     //var len=
 98     //这些代理器可能被导航到不同的目标
 99     // function moveCrowdAgents(pickedPoint) {
100     //     for (let i = 0; i < agents.length; i++) {
101     //         crowd.agentGoto(agents[i], navigationPlugin.getClosestPoint(pickedPoint))
102     //     }
103     // }
104     // return {  moveCrowdAgents, agents }
105 
106 }
107 var obj_agentTrans={};
108 function createAgent(agentParams, position, crowd) {
109     //单位初始化在不同区域,需使用不同的速度初始化参数!!!!
110 
111     const agentTransform = new BABYLON.TransformNode();
112     const agentIndex = crowd.addAgent(position, agentParams, agentTransform);
113 
114     agentTransform.name = `agent-transform-${agentIndex}`
115 
116     const agentMesh = createAgentMesh(agentParams, agentIndex)
117     agentMesh.parent = agentTransform;
118     agentTransform.mydata={};
119     agentTransform.mydata.maxSpeed=agentParams.maxSpeed;
120     agentTransform.mydata.agentIndex=agentIndex;
121     obj_agentTrans[agentTransform.name]=agentTransform;
122     return { agentIndex, agentMesh, agentTransform }
123 }
124 function createAgentMesh(agentParams, agentIndex) {
125     const meshName = `agent-${agentIndex}`
126     let agentMesh = scene.getMeshByName(meshName);
127     if (!agentMesh) {
128         agentMesh = BABYLON.MeshBuilder.CreateCylinder(meshName, { height: agentParams.height, diameter: agentParams.radius * 2 }, scene)
129         agentMesh.position.y += agentParams.height / 2
130         agentMesh.bakeCurrentTransformIntoVertices();
131         agentMesh.material=mat_global.mat_white_e;
132     }
133 
134     // const matName = `agent-${agentIndex}`
135     // const matAgent = scene.getMaterialByName(matName) ?? new BABYLON.StandardMaterial(matName, scene)
136     // const variation = Math.random()
137     // matAgent.diffuseColor = new BABYLON.Color3(0.4 + variation * 0.6, 0.3, 1.0 - variation * 0.3)
138     // agentMesh.material = matAgent
139 
140     return agentMesh
141 }
142 function isPointinArena(v,path)
143 {
144     var point={x:v[0],y:v[1]}
145     return queryPtInPolygon(point,path);
146 }

1、导航网格配置

其中navmeshParameters是用于建立导航网格对象的参数,recast2可将一个导航网格分为若干个“瓦片”,每个瓦片包含多个方格,然后以瓦片为单位进行导航网格更新,并优化导航计算。参数中tileSize指一个瓦片的尺寸,它的计算单位是方格的尺寸(cs),例如在128*128的地图里,设定cs为0.1,tileSize为32,则会分为40*40=1600个瓦片。

tileCacheMeshProcess参数对应的方法被用来设置每个瓦片的属性,可根据瓦片的索引、位置和瓦片内的分层情况为它设置指定的瓦片类型(polyAreas)和移动力消耗(polyFlags),然后可以为每个导航体分配不同的移动类型,控制其是否可通过特定的地块,或者根据不同瓦片的移动力消耗进行导航线路计算,但现有的web端示例(例如three.js的示例https://navcat.dev/,bbl目前尚无)只实现了瓦片类型设置,并未实现移动力消耗设置,故本文则采用其他方式实现不同地块的速度差异效果。

2、导航体配置

visualizeCrowd方法中随机建立了100个导航体,为在体现不同地块中的速度差异,在createMap.js中建立一些封闭路径,将路径内围成的区域作为移动速度不同的地形区域:

 1 var obj_maparea={
 2     2:[]//最大速度为2的地区
 3 }
 4 function initMap()
 5 {
 6     var path_tree=[];
 7     path_tree.push(new BABYLON.Vector3(-32,0,32));
 8     path_tree.push(new BABYLON.Vector3(32,0,32));
 9     path_tree.push(new BABYLON.Vector3(32,0,-32));
10     path_tree.push(new BABYLON.Vector3(-32,0,-32));
11     var points_tree=[];
12     var len=path_tree.length;
13     for(var i=0;i<len;i++)
14     {
15         var pos=path_tree[i];
16         points_tree.push({x:pos.x,y:pos.z});
17     }
18     obj_maparea[2].push(points_tree);
19     var mesh_extrude=new BABYLON.MeshBuilder.ExtrudePolygon("mesh_extrude"
20         , {shape: path_tree, depth: 1,sideOrientation:BABYLON.Mesh.DOUBLESIDE,updatable:true});
21     mesh_extrude.position.y=0.5;
22     mesh_extrude.mydata={};
23     mesh_extrude.mydata.maxSpeed=2;
24     var mat = new BABYLON.StandardMaterial("mat_tree", scene);//1
25     mat.disableLighting = true;
26     mat.emissiveTexture = new BABYLON.Texture("./assets/image/LANDTYPE/yulin.png", scene);
27     //mat.emissiveTexture.uScale = 8;//手动设定
28     //mat.emissiveTexture.vScale = 8;
29     mat.freeze();
30     mat_global.mat_tree=mat;
31     mesh_extrude.material=mat;
32     //mesh_extrude.renderingGroupId=2;
33     mesh_extrude.myType="ground";
34     mesh_extrude.myType1="tree";
35     //根据boundbox比例重调uv?
36     mesh_extrude.convertToFlatShadedMesh();//自动顶点展开
37     var data_position =mesh_extrude.getVerticesData(BABYLON.VertexBuffer.PositionKind);
38     var data_uv=mesh_extrude.getVerticesData(BABYLON.VertexBuffer.UVKind);
39     //var data_uv2=[];
40     var data_index=mesh_extrude._geometry._indices;
41     var len=data_index.length;
42     var rate_scale=10;
43     for(var i=0;i<len;i+=3)//默认是以2单位长度为1uv单位的,这里改为10
44     {
45         var i1=data_index[i];
46         var i2=data_index[i+1];
47         var i3=data_index[i+2];
48         var vec1=new BABYLON.Vector3(data_position[i1*3],data_position[i1*3+1],data_position[i1*3+2]);
49         var vec2=new BABYLON.Vector3(data_position[i2*3],data_position[i2*3+1],data_position[i2*3+2]);
50         var vec3=new BABYLON.Vector3(data_position[i3*3],data_position[i3*3+1],data_position[i3*3+2]);
51         var mx=Math.max(Math.abs(vec1.x-vec2.x),Math.abs(vec1.x-vec3.x),Math.abs(vec3.x-vec2.x));
52         var my=Math.max(Math.abs(vec1.y-vec2.y),Math.abs(vec1.y-vec3.y),Math.abs(vec3.y-vec2.y));
53         var mz=Math.max(Math.abs(vec1.z-vec2.z),Math.abs(vec1.z-vec3.z),Math.abs(vec3.z-vec2.z));
54         if(my<=mx&&my<=mz)//这个三角形倾向于朝向y轴
55         {
56 
57             data_uv[i1*2]=vec1.x/rate_scale;
58             data_uv[i1*2+1]=vec1.z/rate_scale;
59             data_uv[i2*2]=vec2.x/rate_scale;
60             data_uv[i2*2+1]=vec2.z/rate_scale;
61             data_uv[i3*2]=vec3.x/rate_scale;
62             data_uv[i3*2+1]=vec3.z/rate_scale;
63         }
64         else {
65             if(mx>=mz)
66             {
67                 var rate=Math.pow(mx*mx+mz*mz,0.5)/mx;
68                 data_uv[i1*2]=vec1.x*rate/rate_scale;
69                 data_uv[i1*2+1]=vec1.y/rate_scale;
70                 data_uv[i2*2]=vec2.x*rate/rate_scale;
71                 data_uv[i2*2+1]=vec2.y/rate_scale;
72                 data_uv[i3*2]=vec3.x*rate/rate_scale;
73                 data_uv[i3*2+1]=vec3.y/rate_scale;
74             }
75             else {
76                 var rate=Math.pow(mx*mx+mz*mz,0.5)/mz;
77                 data_uv[i1*2]=vec1.z*rate/rate_scale;
78                 data_uv[i1*2+1]=vec1.y/rate_scale;
79                 data_uv[i2*2]=vec2.z*rate/rate_scale;
80                 data_uv[i2*2+1]=vec2.y/rate_scale;
81                 data_uv[i3*2]=vec3.z*rate/rate_scale;
82                 data_uv[i3*2+1]=vec3.y/rate_scale;
83             }
84         }
85     }
86     mesh_extrude.updateVerticesData(BABYLON.VertexBuffer.UVKind,data_uv);
87 }

例如这里是在地图中央建立一个正方形(实际使用时可随意设置联通的多边形形状)的林地区域,并且将这一区域路径保存在obj_maparea对象中,同时还为它设置了均匀的树林纹理。

然后在建立导航体时,使用isPointinArena方法对其位置是否在这个区域路径中进行判断,如果在这个区域中则应用这个区域的最大速度属性,如果不在任何区域中则应用默认的最大速度,如此即可在不同区域内应用不同的移动速度。

在每次渲染循环后,也会对移动导航体的位置进行判断,如果导航体进入不同的区域则应用新区域的最大速度,其代码位于registerAfterRender函数中:

for(var key in obj_agentTrans)//对于每一个导航体的变换节点
            {
                var agentTrans=obj_agentTrans[key];
                var agentIndex=agentTrans.mydata.agentIndex;
                var maxSpeed=agentTrans.mydata.maxSpeed;//当前速度
                if(crowd._agentDestinationArmed[agentIndex])//如果导航体正在移动
                //if(crowd.getAgentNextTargetPath(agentIndex))//如果这个导航器有下一移动目标
                {
                    var flag_found=false;
                    var position=crowd.getAgentPosition(agentIndex);
                    for(var key2 in obj_maparea)
                    {
                        if(flag_found)
                        {
                            break;
                        }
                        // if(key2==maxSpeed)//相同的速度区域不必重复考虑<-也要判断是否在同速区域内,如不在同速区则可能在base区域中!!!!
                        // {
                        //     continue;
                        // }
                        var arr_path=obj_maparea[key2];
                        var len2=arr_path.length;
                        for(var j=0;j<len2;j++)
                        {
                            var path=arr_path[j];
                            if(queryPtInPolygon({x:position.x,y:position.z},path))//如果在这个速度区域内
                            {
                                if(key2!=(maxSpeed+""))
                                {
                                    var v2=parseFloat(key2);
                                    agentTrans.mydata.maxSpeed=v2;
                                    agentParams.maxSpeed=v2;
                                    crowd.updateAgentParameters(agentIndex,agentParams);
                                }

                                flag_found=true;
                                break;
                            }
                        }
                    }
                    if(!flag_found&&maxSpeed!=baseSpeed)
                    {
                        agentTrans.mydata.maxSpeed=baseSpeed;
                        agentParams.maxSpeed=baseSpeed;
                        crowd.updateAgentParameters(agentIndex,agentParams);
                    }
                }

            }

3、babylonjs.addons.min.js代码修改

a、在“"@recast-navigation"”字符附近,增加了一个dir_lib_header变量,并且根据实际部署目录结构调整依赖包的路径,这是为了解决本地部署依赖路径错误问题。

b、在“NavigationPlugin: Tile cache is enabled. Recommended tileSize is 32 to 64. Other values may lead to unexpected behavior.”字符附近,添加了TileCacheMeshProcess类的类型转换代码:

//@@@@create TileCacheMeshProcess instance
    var h=c?rt(t):u?it(t):tt(t);
    if(h.tileCacheMeshProcess)
        h.tileCacheMeshProcess=new (Qe().TileCacheMeshProcess)((h.tileCacheMeshProcess));

这是因为bbl库建立的tileCacheMeshProcess对象是普通object对象而非TileCacheMeshProcess类对象。

 五、总结与计划

程序成功完成实验目标,bbl官方可能在后续版本修正代码问题,并给出完整的官方案例。

下一步计划在地图中放置可建设发展的“城市”,使用htmlMesh为导航器设计“军队”图标,在渲染循环基础上建立游戏逻辑循环,实现单机、单势力运营。

posted @ 2025-11-12 12:21  ljzc002  阅读(32)  评论(0)    收藏  举报