找回密码
 立即注册
首页 业界区 安全 网页端3D编程小实验-一种即时战略游戏的编程原型 ...

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

唐茗 2025-11-12 13:30:03
本文尝试基于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可打开基础测试页面:

页面上半部为负责3D渲染的canvas区域,下部为可自定义设计UI的原生html区域,该页面基于百分比定位方法设计,可适用于多种分辨率场景,考虑到兼容移动设备,在canvas区也设计了一些gui按钮以代替键盘和鼠标滚轮功能。
canvas区域中渲染了一张简单的方形地图,地图四周为128*128泥土地,中间为64*64的林地,林地中间的白点为可移动单位的标志物,周围的白色边界为围墙。滚动鼠标滚轮可控制视角上下移动,左键拖动鼠标可同步拖动地图(显然,在不添加额外按钮时,左键拖动地图操作与“左键框选单位”相冲突,需根据实际需要取舍,如需框选单位功能可参考此文中的实现:https://juejin.cn/post/6968635340110168101),按o键可切换为bbl的自由相机视角,按i恢复为rts视角,以下是林地和标志物的侧视图:

rts视角下,右键点击地面则全部标志物将向点击位置移动,移动过程中各个标志物之间将自动保持距离,在林地中移动时的速度为泥土地的40%。
接下来两章先介绍bbl框架离线依赖的手动组织方法,如果读者已经熟悉bbl使用,可跳到第四章。
二、编程框架搭建
1、为什么基于经典方法组织
所谓“基于经典方法组织”指不依赖npm、typescript、umi等需要“编译”环节的框架,手动将依赖包配置在程序的特定位置,在实验性编程中这样作的好处包括:易于调试运行时代码、易于修改第三方依赖包、减少网络依赖性、专注于功能本身而非框架、加深对程序结构了解等。如果开发者需要使用流行的编译框架进行代码组织,则这些基于经典方法组织的代码,也可以容易的转为各种框架下的代码或嵌入到框架中(反之则很难)。
2、手动下载bbl依赖包并离线部署
首先随意打开一个官方训练场场景,例如基础演示场景https://playground.babylonjs.com/#WJXQP0:

左边红框处显示当前最新的bbl版本,例如图中为8.36.1,右边红框为下载按钮,需注意的是,这里下载的只是该训练场的索引html页面,而该页面中包含bbl依赖库的实际下载地址:
  1.                                 
复制代码
从上到下,其功能分别为:
官方示例资源地址(无用)
群组导航库地址(旧版,被替代)
ammo物理引擎(较旧,但功能基本可用)
havok物理引擎(较新,官方推荐使用)
cannon物理引擎(较旧,存在bug)
oimo物理引擎(较旧,存在bug)
“挖洞库”(体积不大,建议保留)
核心库
额外材质库(如水、火、毛绒等,按需加载)
程序纹理库(按需加载)
后期处理库(按需加载)
模型加载库(记忆中bbl默认可加载obj和bbl格式模型,如需加载更多格式模型需使用此库)
序列化库(可用来将bbl生成的场景导出为各种格式的三维模型)
gui库(用来在页面中绘制gui按钮,建议保留)
额外内容库(一些额外添加的功能,例如本文所使用的recast2导航库即通过此库加载)
调试库(bbl官方的页面调试工具)
在浏览器中访问对应的url即可下载相应的依赖库,需要注意的是,其中一些库内部又会引用其他依赖库,例如havok物理引擎库会引用havok.wasm文件,此时可通过浏览器调试工具获取其引用地址并进行下载,其中模型加载库的简介依赖包最多,可根据所需加载的模型类别选择性配置。还有个别依赖项缺少对离线原生部署的兼容,需手动修改其代码。
此次实验的离线依赖包包含以下内容:

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

其中assets中为图片、音频等资源,lib中为依赖库。
3、入口网页
WH-admin.html文件为程序的入口网页,其代码如下:
  1.   1   2   3   4       5     管理端包含actionloop,负责推演所有单位,并按视野将推演结论发送给player,同时接收player传来的有效指令  6      34      35      36      37      38      39      40      41      42      43      44      45      46      47      48      49      50      51      52      53      54  55  56      57      58      59  60      61      62      63     提
  2. 》 66      67      68  69      70  71 292
复制代码
View Code其中的入口代码为:
  1. 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个方面的初始化操作:
  1. async function Init()    {        await initScene();        await initArena();        InitMouse();        initGuiControl();        webGLStart2();    }
复制代码
以下将分别详细介绍每一项初始化操作,如程序有更多功能,则可在这里添加更多初始化方法,如WebSocket初始化、程序纹理初始化、数据库初始化等。
三、初始化操作
1、场景初始化
代码如下:
  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、场地初始化
  1. 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文件中:
[code]  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按钮时触发!! 0){//向下滚动356                     //if(int_z>0)357                     {358                         int_z--;359                     }360                 }else{//向上滚动361                     if(int_z

相关推荐

您需要登录后才可以回帖 登录 | 立即注册