首页 > 基础资料 博客日记
网页端3D编程小实验-一种多人自走棋游戏原型
2026-04-03 16:30:03基础资料围观1次
摘要:为解决常规自走棋游戏配置灵活度低且难以在局域网跨平台联机的问题,本文基于Babylon.js(以下简称bbl)和websocket(简称ws)技术实现了一个网页端多人自走棋游戏原型。该原型实现了棋盘地形设计、简单的棋子角色设计、自走棋操作UI、双方对抗逻辑、角色生命周期管理和基于websoket的局域网多人互联。项目达成了最初设计目标,且前端代码以原生方式编写易于调试和扩展,编程者可以此原型为基础编写更复杂的多人自走棋类游戏。
1、程序运行效果
视频地址:https://www.bilibili.com/video/BV1tvPfzMETk
画面上方为红蓝双方的控制页面,下方为裁判页面,红蓝双方分别可通过自己的页面向场景中放置单位,裁判页面负责进行所有逻辑计算并向红蓝双方同步状态,ws服务器需运行在window PC上,前端页面可通过浏览器运行在PC或移动设备上。
页面默认为rts控制方式,鼠标左键拖动地图,滚轮缩放视角范围,左键在卡组中选取单位,右键放置。按o键可切换为自由相机进行场景调试(鼠标拖动视角,上下左右控制移动),按i键可恢复为rts控制方式。
1.1代码下载
该项目基于MIT协议开源,可通过以下链接下载全部运行环境和代码:
链接: https://yun.139.com/shareweb/#/w/i/2u8ooDGV0gA4g 提取码:wrkg
解压后目录如下图:

主目录由nginx服务器改造而成,其中html下为前端代码与资源,jdk-22为java运行库,websocket下为以java语言编写的ws服务器,start.bat为一键启动脚本。
1.2启动运行环境
start.bat脚本内容如下:
1 rem 关闭已有的nginx 2 nginx -s stop 3 rem 设置环境变量 4 set JAVA_HOME=jdk-22 5 set JRE_HOME=%JAVA_HOME% 6 set CLASSPATH=.;%JAVA_HOME%\lib 7 set Path=%JAVA_HOME%\bin;%JAVA_HOME%\lib 8 rem 启动netty服务 9 start "startWS" java -jar websocket\target\websocket-0.0.1.jar 192.168.43.220-2323-80-routeWH.html >netty.log 10 timeout 5
其中websocket-0.0.1.jar为ws服务器的jar包,192.168.43.220为当前电脑的局域网IP,如不手动设置则程序将尝试自动检测IP地址,2323为ws服务监听端口,80为http服务监听端口,routeWH.html为启动服务器后自动打开的网页。
ws服务器是一个springboot+Netty框架程序,其系统架构为:

其入口方法FSPApplication如下:
1 @SpringBootApplication 2 public class FSPApplication implements CommandLineRunner{ 3 @Autowired 4 private ServerBootStrap ws; 5 6 public static void main(String[] args) {//args是java命令参数?? 7 SpringApplication.run(FSPApplication.class, args); 8 } 9 @Override 10 public void run(String... args) throws Exception { 11 12 String str_p=args[0]; 13 String[] arr_p=str_p.split("-"); 14 NettyConfig.WS_HOST=arr_p[0];//参数配置中的IP 15 NettyConfig.WS_HOST2=GetLocalIP();//自动获取的IP 16 17 if(!arr_p[1].equals("")) 18 { 19 NettyConfig.WS_PORT=Integer.parseInt(arr_p[1]); 20 } 21 if(!arr_p[2].equals("")) 22 { 23 NettyConfig.HTTP_PORT=Integer.parseInt(arr_p[2]); 24 } 25 if(!arr_p[3].equals("")) 26 { 27 NettyConfig.WS_ROUTE=arr_p[3];//自定义的网页URL 28 } 29 InetSocketAddress address; 30 ChannelFuture future; 31 try{//启动ws服务 32 address = new InetSocketAddress(NettyConfig.WS_HOST, NettyConfig.WS_PORT); 33 future = ws.start(address); 34 setConfigFile(NettyConfig.WS_HOST,NettyConfig.HTTP_PORT,NettyConfig.WS_PORT,NettyConfig.WS_ROUTE); 35 System.out.print("Netty开始监听:"+NettyConfig.WS_HOST+":"+NettyConfig.WS_PORT); 36 }catch(Exception e) 37 {//如参数配置的IP启动失败,则使用自动获取的IP启动 38 e.printStackTrace(); 39 address = new InetSocketAddress(NettyConfig.WS_HOST2, NettyConfig.WS_PORT); 40 future = ws.start(address); 41 setConfigFile(NettyConfig.WS_HOST2,NettyConfig.HTTP_PORT,NettyConfig.WS_PORT,NettyConfig.WS_ROUTE); 42 System.out.print("Netty开始监听:"+NettyConfig.WS_HOST2+":"+NettyConfig.WS_PORT); 43 } 44 45 //在强制关闭java命令行窗口时这一钩子方法没有生效!! 46 //一种可能的方法是,打开两个java cmd窗口,只要有其中一个被关闭,则主动关闭所有 47 Runtime.getRuntime().addShutdownHook(new Thread(){ 48 @Override 49 public void run() { 50 ws.destroy(); 51 //在这里顺便关闭nginx 52 String path_root=System.getProperty("user.dir");//java命令运行目录 53 String cmd="cd "+path_root+";nginx -s stop"; 54 Runtime runtime = Runtime.getRuntime(); 55 Process proce = null; 56 try{ 57 proce = runtime.exec(cmd); 58 }catch (Exception e) { 59 e.printStackTrace(); 60 } 61 } 62 }); 63 future.channel().closeFuture().syncUninterruptibly(); 64 } 65 public static void setConfigFile(String localIP,int httpPort,int wsPort,String wsRoute) 66 {//根据java的配置情况,去修改nginx和前端的配置文件!!!! 67 //要确保配置修改完毕后才启动nginx! 68 FileOutputStream outSTr=null; 69 BufferedOutputStream Buff=null; 70 FileOutputStream outSTr2=null; 71 BufferedOutputStream Buff2=null; 72 FileOutputStream outSTr3=null; 73 BufferedOutputStream Buff3=null; 74 try 75 { 76 String path_root=System.getProperty("user.dir");//java命令运行目录 77 //修改前端项目的配置文件 78 String path_c_html=path_root+"/html/config.js"; 79 String str_c_html="var localIP=\""+localIP+"\";\n" + 80 "var wsPort=\""+wsPort+"\";\n" + 81 "var httpPort=\""+httpPort+"\";"; 82 File file_c_html=new File(path_c_html); 83 outSTr=new FileOutputStream(file_c_html); 84 Buff=new BufferedOutputStream(outSTr); 85 StringBuffer write =new StringBuffer(); 86 write.append(str_c_html); 87 Buff.write(write.toString().getBytes("UTF-8")); 88 Buff.flush(); 89 outSTr.close(); 90 Buff.close(); 91 //修改nginx的配置文件 92 String path_c_nginx=path_root+"/conf/nginx.conf"; 93 String str_c_nginx="worker_processes 1;\n" + 94 "\n" + 95 "error_log logs/error.log;\n" + 96 "\n" + 97 "pid logs/nginx.pid;\n" + 98 "\n" + 99 "events {\n" + 100 " worker_connections 1024;\n" + 101 "}\n" + 102 "\n" + 103 "http {\n" + 104 " include mime.types;\n" + 105 " default_type application/octet-stream;\n" + 106 "\n" + 107 //" access_log logs/access.log main;\n" + 108 "\n" + 109 " sendfile on;\n" + 110 "\n" + 111 " keepalive_timeout 65;\n" + 112 "\n" + 113 " server {\n" + 114 " listen "+httpPort+";\n" + 115 " server_name "+localIP+";\n" + 116 "\n" + 117 " location / {\n" + 118 " root html;\n" + 119 " index index.html index.htm;\n" + 120 " }\n" + 121 "\n" + 122 " error_page 500 502 503 504 /50x.html;\n" + 123 " location = /50x.html {\n" + 124 " root html;\n" + 125 " }\n" + 126 " } \n" + 127 "}\n"; 128 File file_c_nginx=new File(path_c_nginx); 129 outSTr2=new FileOutputStream(file_c_nginx); 130 Buff2=new BufferedOutputStream(outSTr2); 131 StringBuffer write2 =new StringBuffer(); 132 write2.append(str_c_nginx); 133 Buff2.write(write2.toString().getBytes("UTF-8")); 134 Buff2.flush(); 135 outSTr2.close(); 136 Buff2.close(); 137 //生成启动nginx和chrome浏览器的脚本 138 String path_c_chrome=path_root+"/openChrome.bat"; 139 String str_c_chrome="start nginx -c conf\\nginx.conf\n" + 140 "timeout 1\n" + 141 "start chrome http://"+localIP+":"+httpPort+"/"+wsRoute;//"/qrRoute.html"; 142 File file_c_chrome=new File(path_c_chrome); 143 outSTr3=new FileOutputStream(file_c_chrome); 144 Buff3=new BufferedOutputStream(outSTr3); 145 StringBuffer write3 =new StringBuffer(); 146 write3.append(str_c_chrome); 147 Buff3.write(write3.toString().getBytes("UTF-8")); 148 Buff3.flush(); 149 outSTr3.close(); 150 Buff3.close(); 151 //有的系统版本不支持timeout命令!在此时进行nginx启动 152 String cmd="openChrome.bat"; 153 Runtime runtime = Runtime.getRuntime(); 154 Process proce = null; 155 InputStream stderr=null; 156 InputStreamReader isr=null; 157 BufferedReader br=null; 158 try{ 159 proce = runtime.exec(cmd);//执行刚才生成的启动脚本 160 stderr = proce.getErrorStream();//输出控制台用来测试 161 isr = new InputStreamReader(stderr); 162 br = new BufferedReader(isr); 163 String line = null; 164 while ((line = br.readLine()) != null) 165 { 166 System.out.println(line);//输出控制台日志 167 } 168 try { 169 proce.waitFor();//同步等待异步线程 170 stderr.close(); 171 isr.close(); 172 br.close(); 173 } catch (InterruptedException e) { 174 e.printStackTrace(); 175 }finally { 176 //proce.destroy(); 177 } 178 }catch (Exception e) { 179 e.printStackTrace(); 180 } 181 finally { 182 if(br!=null) 183 { 184 try{ 185 stderr.close(); 186 isr.close(); 187 br.close(); 188 }catch(Exception e) 189 { 190 e.printStackTrace(); 191 } 192 } 193 194 } 195 }catch(Exception e) 196 { 197 e.printStackTrace(); 198 } 199 finally { 200 try 201 { 202 outSTr.close(); 203 Buff.close(); 204 outSTr2.close(); 205 Buff2.close(); 206 outSTr3.close(); 207 Buff3.close(); 208 }catch(Exception e) 209 { 210 e.printStackTrace(); 211 } 212 } 213 } 214 }
以上java代码生成服务器配置文件,并自动启动服务,目前存在的一个问题是没有实现前端nginx服务器自动关闭,可通过再次执行start.bat脚本来关闭后台的nginx进程。
1.3导航页面routeWH.html
页面内容比较简单,包含裁判端、红方、蓝方的控制页面链接和相应二维码,可叫上三五好友扮演不同角色,使用一台PC和若干手机进行游戏。
2棋盘地形设计
2.1地形设置
本项目通过地形构建脚本修改地形,访问http://ip/testCard/WH-card2.html可打开一个不连接ws的测试页面,在该页面引用的createMap2b.js文件中的initMap方法里设置地形:
1 function initMap() 2 { 3 var ground1=new FrameGround(); 4 var obj_p={ 5 name:"ground1", 6 segs_x:160,//这个是顶点的细分精度,意思是x方向分为160段 7 segs_z:160, 8 size_per_x:4,//每段宽4单位 9 size_per_z:4, 10 mat:"mat_grass", 11 };//生成导航网格是不是正向的?因此不能旋转这个基础地面网格??《-开局时旋转相机 12 ground1.init(obj_p);//建立一个“flat”的条带网格,它是一个正方形 13 obj_ground["ground1"]=ground1; 14 15 cri();//刷新additionalscript.js文件,这个文件包含isInArea1等判断范围的方法 16 ct2(isInArea1,6);//这个范围内的地面高度设为6 17 ct2(isInArea2,-6); 18 ct3(60,60,-Math.PI/4,12,16,6,0);//在(60,60)处建立一个y轴角度为-45度,长度为12,宽度为16,高度从6变成0的斜坡 19 ct3(580,580,-Math.PI/4,12,16,0,6);//用来连接高低地形 20 //cri,ct2,ct3方法都可以在程序运行时通过浏览器命令行运行 21 ct3(475,145,-Math.PI/4,12,16,0,-6); 22 ct3(310,310,-Math.PI/4,12,32,0,-6); 23 ct3(145,474,-Math.PI/4,12,16,0,-6); 24 ct3(495,165,-Math.PI/4,12,16,-6,0); 25 ct3(330,330,-Math.PI/4,12,32,-6,0); 26 ct3(165,495,-Math.PI/4,12,16,-6,0); 27 var mesh_ground2=ground1.MakeLandtype1(function(vec){ 28 if(vec.y<-1)//将顶点高度小于-1的地区设为沙土地 29 { 30 return true; 31 } 32 },ground1.obj_mat.mat_sand,"ground_sand");//这一沙土形将紧密贴合前面设置的地形,在斜坡处明显体现 33 34 var mesh_ground3=ground1.MakeLandtype1(function(vec){ 35 if(vec.y<-5)//将高度小于-5的地区设为水面 36 { 37 return true; 38 } 39 }, 40 ground1.obj_mat.mat_shallowwater 41 //water 42 ,"ground_water",true,-5);//水面会水平的位于-5的高度 43 initCrowd([ground1.ground_base,mesh_ground2]);//初始化导航网格 44 var mesh_ground4=ground1.MakeLandtype1(function(vec){ 45 if(vec.y>5)//将高地设为雪地 46 { 47 return true; 48 } 49 },mat_global.mat_ice,"ground_ice"); 50 ground1.ground_base.rotation.y=Math.PI/4;//把正方形旋转45度 51 mesh_ground2.rotation.y=Math.PI/4; 52 mesh_ground3.rotation.y=Math.PI/4; 53 mesh_ground4.rotation.y=Math.PI/4; 54 55 56 57 }
修改15-49行的代码即可设计自己的棋盘地形,比较陡峭的地形会阻止棋子经过,棋子会自动寻找路径绕过障碍。该地形构造方法的具体细节可参考https://www.cnblogs.com/ljzc002/p/11105496.html的第二节,本项目在那篇文章的基础上又添加了对地面UV的重新计算,使地面纹理更加均匀。我们可以在createMap2b.js中调试出合适的地形后,将其复制到createMap2.js文件中使用。
2.2控制点设置
控制点绑定在一个固定单位(如防御塔、城堡)上,在这个单位被摧毁前敌方将不能在控制点影响区域内放置单位

例如图中的上中下三塔和高地城堡为红蓝双方的控制点,分割地图的虚线表示控制点的控制范围,在这些控制点被摧毁前敌方不能在虚线范围内放置单位。
示例地图的控制点设置代码如下:
1 //初始化上中下三路防御塔,以及高地城堡 2 function createControlPoint() 3 { 4 if(userId!="admin") 5 { 6 return null; 7 } 8 var rate=Math.pow(2,0.5); 9 var dis_temp=60*1.42//120/rate; 10 var offset_temp=3; 11 var pos=navigationPlugin.getClosestPoint(new BABYLON.Vector3(-320*rate+20,6,0)); 12 let army=createCard(obj_unittype_global["unit_2026011306_中世纪城堡"],pos,"l",true); 13 var path=[new BABYLON.Vector3(-320*rate-offset_temp,6.5,0),new BABYLON.Vector3(-320*rate+dis_temp,6.5,dis_temp+offset_temp),new BABYLON.Vector3(-320*rate+dis_temp,6.5,-dis_temp-offset_temp)]; 14 var mesh_extrude=new BABYLON.MeshBuilder.ExtrudePolygon("mesh_sidemask" 15 , {shape: path, depth: 1,sideOrientation:BABYLON.Mesh.DOUBLESIDE,updatable:false}); 16 mesh_extrude.renderingGroupId=0; 17 mesh_extrude.material=mat_global.mat_blue_e; 18 mesh_extrude.position.y=6.5; 19 //mesh_extrude.isVisible=false; 20 //mesh_extrude.alpha=0.2; 21 mesh_extrude.myType="sidemask"; 22 mesh_extrude.myType2="l"; 23 path.push(path[0].clone()); 24 //var lines=new BABYLON.MeshBuilder.CreateLineSystem("LineSystemL",{lines:[path],material:mat_global.mat_blue_ea}); 25 var lines=BABYLON.MeshBuilder.CreateDashedLines("LineSystemL",{points:path,material:mat_global.mat_blue_ea}) 26 lines.isPickable=false; 27 lines.renderingGroupId=3; 28 lines.color="blue"; 29 controlPoint.l["高地城堡"]={army:army,mesh:mesh_extrude,lines:lines}; 30 army.pointName="高地城堡"; 31 army.cp=true; 32 army.pathCP=path; 33 34 var pos=navigationPlugin.getClosestPoint(new BABYLON.Vector3(320*rate-20,6,0)); 35 let army2=createCard(obj_unittype_global["unit_2026011306_中世纪城堡"],pos,"r",true); 36 var path=[new BABYLON.Vector3(320*rate+offset_temp,6.5,0),new BABYLON.Vector3(320*rate-dis_temp,6.5,-dis_temp-offset_temp),new BABYLON.Vector3(320*rate-dis_temp,6.5,dis_temp+offset_temp)]; 37 var mesh_extrude=new BABYLON.MeshBuilder.ExtrudePolygon("mesh_sidemask" 38 , {shape: path, depth: 1,sideOrientation:BABYLON.Mesh.DOUBLESIDE,updatable:false}); 39 mesh_extrude.renderingGroupId=0; 40 mesh_extrude.material=mat_global.mat_red_e; 41 mesh_extrude.position.y=6.5; 42 mesh_extrude.myType="sidemask"; 43 mesh_extrude.myType2="r"; 44 path.push(path[0].clone()); 45 var lines=new BABYLON.MeshBuilder.CreateLineSystem("LineSystemR",{lines:[path],material:mat_global.mat_red_ea}); 46 lines.isPickable=false; 47 lines.renderingGroupId=3; 48 lines.color="red"; 49 controlPoint.r["高地城堡"]={army:army2,mesh:mesh_extrude,lines:lines}; 50 army2.pointName="高地城堡"; 51 army2.cp=true; 52 army2.pathCP=path; 53 54 createControlPoint2(new BABYLON.Vector3(-180,0,0) 55 ,[new BABYLON.Vector3(-320*rate+dis_temp,0.5,-dis_temp-offset_temp) 56 ,new BABYLON.Vector3(-320*rate+dis_temp,0.5,dis_temp+offset_temp) 57 ,new BABYLON.Vector3(-15,0.5,dis_temp+offset_temp) 58 ,new BABYLON.Vector3(-15,0.5,-dis_temp-offset_temp)] 59 ,"mesh_sidemask",obj_unittype_global["unit_2026011306_中世纪防御塔"],"l","中塔",0.5); 60 createControlPoint2(new BABYLON.Vector3(-180,0,180) 61 ,[ new BABYLON.Vector3(-320*rate+dis_temp,0.5,dis_temp+offset_temp) 62 ,new BABYLON.Vector3(-15,0.5,dis_temp+offset_temp) 63 ,new BABYLON.Vector3(-15,0.5,310*rate)] 64 ,"mesh_sidemask",obj_unittype_global["unit_2026011306_中世纪防御塔"],"l","上塔",0.5); 65 createControlPoint2(new BABYLON.Vector3(-180,0,-180) 66 ,[new BABYLON.Vector3(-320*rate+dis_temp,0.5,-dis_temp-offset_temp) 67 ,new BABYLON.Vector3(-15,0.5,-dis_temp-offset_temp),new BABYLON.Vector3(-15,0.5,-310*rate)] 68 ,"mesh_sidemask",obj_unittype_global["unit_2026011306_中世纪防御塔"],"l","下塔",0.5); 69 70 createControlPoint2(new BABYLON.Vector3(180,0,0) 71 ,[new BABYLON.Vector3(320*rate-dis_temp,0.5,-dis_temp-offset_temp) 72 ,new BABYLON.Vector3(320*rate-dis_temp,0.5,dis_temp+offset_temp) 73 ,new BABYLON.Vector3(15,0.5,dis_temp+offset_temp) 74 ,new BABYLON.Vector3(15,0.5,-dis_temp-offset_temp)] 75 ,"mesh_sidemask",obj_unittype_global["unit_2026011306_中世纪防御塔"],"r","中塔",0.5); 76 createControlPoint2(new BABYLON.Vector3(180,0,180) 77 ,[ new BABYLON.Vector3(320*rate-dis_temp,0.5,dis_temp+offset_temp) 78 ,new BABYLON.Vector3(15,0.5,dis_temp+offset_temp) 79 ,new BABYLON.Vector3(15,0.5,310*rate)] 80 ,"mesh_sidemask",obj_unittype_global["unit_2026011306_中世纪防御塔"],"r","上塔",0.5); 81 createControlPoint2(new BABYLON.Vector3(180,0,-180) 82 ,[new BABYLON.Vector3(320*rate-dis_temp,0.5,-dis_temp-offset_temp) 83 ,new BABYLON.Vector3(15,0.5,-dis_temp-offset_temp),new BABYLON.Vector3(15,0.5,-310*rate)] 84 ,"mesh_sidemask",obj_unittype_global["unit_2026011306_中世纪防御塔"],"r","下塔",0.5); 85 86 console.log("控制区初始化完成"); 87 } 88 function createControlPoint2(pos0,path,meshName,unitType,side,pointName,h) 89 { 90 var pos=navigationPlugin.getClosestPoint(pos0); 91 let army=createCard(unitType,pos,side,true); 92 //var path=[new BABYLON.Vector3(-320*rate-offset_temp,6.5,0),new BABYLON.Vector3(-320*rate+dis_temp,6.5,dis_temp+offset_temp),new BABYLON.Vector3(-320*rate+dis_temp,6.5,-dis_temp-offset_temp)]; 93 var mesh_extrude=new BABYLON.MeshBuilder.ExtrudePolygon(meshName 94 , {shape: path, depth: 1,sideOrientation:BABYLON.Mesh.DOUBLESIDE,updatable:false}); 95 mesh_extrude.renderingGroupId=0; 96 97 mesh_extrude.position.y=pos.y+h; 98 mesh_extrude.myType="sidemask"; 99 mesh_extrude.myType2=side; 100 path.push(path[0].clone()); 101 102 var lines 103 if(side=="l") 104 { 105 mesh_extrude.material=mat_global.mat_blue_e; 106 lines=BABYLON.MeshBuilder.CreateDashedLines("LineSystemL",{points:path,material:mat_global.mat_blue_ea}) 107 } 108 else if(side=="r") 109 { 110 mesh_extrude.material=mat_global.mat_red_e; 111 lines=BABYLON.MeshBuilder.CreateDashedLines("LineSystemR",{points:path,material:mat_global.mat_red_ea}) 112 } 113 lines.isPickable=false; 114 lines.renderingGroupId=3; 115 controlPoint[side][pointName]={army:army,mesh:mesh_extrude,lines:lines}; 116 if(!army.lifeSycle.onAfterDied) 117 { 118 army.lifeSycle.onAfterDied=[]; 119 } 120 121 army.pointName=pointName 122 army.cp=true; 123 army.pathCP=path; 124 125 return army; 126 }
2.3 导航网格
该项目使用最新的recast2群组导航库进行群组导航计算,目前bbl对该库的支持文档尚不十分完善,故本项目与此有关的一些设计可能并非最佳。
导航网格的初始化代码在ControlCrowd.js文件中,启用这两行代码可打开recast2的内置调试器:
1 debugger_nm=new ADDONS.NavigationDebugger(scene,{}) 2 debugger_nm.drawNavMeshPolysWithFlags(navigationPlugin.navMesh, 1, 0x0000ff);
启用后可在地面纹理的下方看到导航网格的实际覆盖范围,可借助它调整单位的可通行区域,例如高低地之间的连接通道

当前版本的调试工具会不断重新刷新调试网格,所以打开调试器后帧率明显下降——在只进行简单导航时reacst1版本可能更有性价比。
3 棋子角色设计
访问http://ip/tool/createUnit3/dwsj1/index.html可打开单位设计页面:

该设计器页面由claudecode制作,首先上传一张角色主图,例如通过大模型生成的奇幻生物图,程序会自动调整该图片的大小;然后从中框选一部分作为头像,该头像以dataUrl方式保存;接着在角色属性区编辑该角色的能力;然后在射弹属性区上传一张该角色的射弹图片,例如投石机上传一张石头图片、魔法师上传一张火焰图片等,这里需要注意两点:1、为了简化编程,近战单位的攻击也以射程较短的射弹表示,例如发射一把剑或刀,2、将射弹图片的背景设为透明可以让射弹更好的融入环境,对于ai生成的图片,可以先用图像处理工具去掉背景,对于纯色背景的图片,可使用tool目录下的ChangeOneColor2.html脚本将指定颜色替换为透明色。
最下面可以输入角色描述信息,将生成主图时的AI提示词复制到这里是一个快捷的选择,点击保存角色后可把该角色的主图和描述文件下载到本地,将这两个文件放置在testCard/unit目录下,然后在initUnit.js文件中导入这个单位则可将新建单位加入游戏。默认情况下双方将使用相同的“卡组”,如希望使用不同卡组则可在ControlWH2.js中搜索“初始化单位美术资源”手动配置。
4自走棋操作UI
4.1html UI
为简化编程,本项目采用html原生标签而非bbl内置的GUI组件实现用户交互,页面下部为纯粹的html区域,编程者可按需求自由利用这一空间。
4.2键盘鼠标操作
键盘鼠标的操作方法在ControlWH2.js文件中,该文件包含了html窗口常用的监听和游戏主循环,其详细设计可参考https://www.cnblogs.com/ljzc002/p/19207021。
4.3建立单位
创建单位的方法在createCard2.js文件中,为提高渲染速度,本项目首先通过createCardClass方法为每个单位类型建立一个“渲染类”:
1 function createCardClass(unitType) 2 { 3 var class_card=new BABYLON.MeshBuilder.CreatePlane("class_"+unitType.name 4 , {height:unitType.attributes.size*4/3,width:unitType.attributes.size}, scene); 5 var mat_ice = new BABYLON.StandardMaterial("mat_"+unitType.name, scene);//1 6 mat_ice.disableLighting = true; 7 mat_ice.emissiveTexture = new BABYLON.Texture("./unit/"+unitType.mainImage, scene); 8 mat_ice.useLogarithmicDepth = true; 9 mat_ice.freeze(); 10 11 class_card.material=mat_ice; 12 class_card.renderingGroupId=2; 13 class_card.position.z=-990; 14 class_card.rotation.x=Math.PI/6; 15 unitType.class_card=class_card; 16 17 var class_projectile=new BABYLON.MeshBuilder.CreatePlane("class_projectile_"+unitType.name 18 , {height:unitType.projectile.size,width:unitType.projectile.size}, scene); 19 var mat_ice = new BABYLON.StandardMaterial("mat_projectile_"+unitType.name, scene);//1 20 //mat_ice.disableLighting = true; 21 //mat_ice.emissiveTexture = new BABYLON.Texture(unitType.projectile.image, scene); 22 //mat_ice.emissiveTexture.hasAlpha = true;//似乎只有漫反射材质能使用纹理图中的不透明度属性!!!! 23 mat_ice.diffuseTexture = new BABYLON.Texture(unitType.projectile.image, scene); 24 mat_ice.diffuseTexture.hasAlpha = true; 25 mat_ice.backFaceCulling=false; 26 mat_ice.useLogarithmicDepth = true; 27 mat_ice.freeze(); 28 class_projectile.material=mat_ice; 29 class_projectile.renderingGroupId=2; 30 class_projectile.position.z=-990;//放在不可见处 31 class_projectile.sideOrientation=BABYLON.Mesh.DOUBLESIDE; 32 unitType.class_projectile=class_projectile; 33 }
预先渲染一份单位和射弹的网格和纹理,然后在每次建立单位时将实际单位生成为渲染类的实例,单位的血条、行动条、范围指示器也都以实例方式创建。
以下createCard方法负责建立单位:
1 function createCard(unitType,pos,side,free) 2 {//从备用的导航器中找一个空白的使用 3 var flag_found=false; 4 var obj_army={};//obj_army是一个单位对象 5 obj_army.side=side; 6 if(unitType.lifeSycle) 7 {//让这个单位实例获得单位类的生命周期方法 8 let army=obj_army; 9 obj_army.isAlive=true; 10 obj_army.pos=pos; 11 //obj_army.lifeSycle=JSON.parse(JSON.stringify(unitType.lifeSycle));//stringify会使function变成null 12 obj_army.lifeSycle=unitType.lifeSycle; 13 //这个parse过程可以把生命周期方法和army变量关联起来吗? 14 if(obj_army.lifeSycle.onBeforePut)//在单位放置前执行的方法,类似炉石传说的“战吼” 15 { 16 var len=obj_army.lifeSycle.onBeforePut.length; 17 for(var i=0;i<len;i++) 18 { 19 obj_army.lifeSycle.onBeforePut[i](obj_army); 20 } 21 22 } 23 if(!obj_army.isAlive) 24 { 25 console.log("onBeforePut阻止该单位建立") 26 return null; 27 } 28 } 29 else { 30 obj_army.lifeSycle={}; 31 obj_army.isAlive=true; 32 obj_army.pos=pos; 33 } 34 for(var i=0;i<size_agent;i++) 35 {//为提升recast导航器使用效率,使用导航器池管理导航器,即预先建立300个空白导航器,每次创建单位时 36 var agentTransform=arr_agenttransform[i];//从池中分配导航器 37 if(!agentTransform.mydata.agentMesh)//是空白的导航器 38 { 39 //agentTransform.mydata.agentIndex=i; 40 flag_found=true; 41 var agentParams=JSON.parse(JSON.stringify(agentParams0));//根据单位的尺寸和移动速度设置导航器的属性 42 agentParams.radius=unitType.attributes.size*4;//原数据是直径《-此设定似乎未生效?? 43 agentParams.maxSpeed=unitType.attributes.moveSpeed*2;//@@@@移动速度 44 // if(agentParams.maxSpeed==0)//如果将速度设为0会阻止群组碰撞计算??!! 45 // { 46 // agentParams.maxSpeed=0.01; 47 // } 48 49 crowd.updateAgentParameters(i,agentParams); 50 //agentTransform.position=pos; 51 crowd.agentTeleport(i,pos); 52 //var agentMesh = BABYLON.MeshBuilder.CreateCylinder("agentMesh_"+i, { height: 1, diameter: agentParams.radius ,tessellation:8}, scene) 53 var agentMesh;//=class_cylinder.createInstance("agentMesh_"+i); 54 obj_army.agentIndex=i; 55 obj_army.id=count_army; 56 obj_army.name=unitType.name; 57 obj_army.typeName=unitType.typeName; 58 obj_army.charge=0; 59 obj_army.health0=unitType.attributes.health; 60 obj_army.health=unitType.attributes.health; 61 obj_army.attackSpeed=unitType.attributes.attackSpeed; 62 obj_army.attackPower=unitType.attributes.attackPower; 63 obj_army.range=unitType.attributes.range;//暂时不考虑射弹的爆炸范围《-暂时不考虑范围攻击 64 obj_army.moveSpeed=unitType.attributes.moveSpeed; 65 obj_army.cost=unitType.attributes.cost; 66 obj_army.agentTransform=agentTransform; 67 obj_army.schedule_move=0;//行动进度达到100时行动,这个行动进度累加要在每一帧进行! 68 obj_army.class_projectile=unitType.class_projectile; 69 obj_army.p_speed=unitType.projectile.speed; 70 obj_army.p_explosionRange=unitType.projectile.explosionRange; 71 obj_army.size=unitType.attributes.size; 72 obj_army.doing="waiting"; 73 //射弹属性根据typeName去obj_unittype_global中读 74 obj_armys[count_army]=obj_army; 75 count_army++; 76 agentTransform.mydata.army=obj_army; 77 if(side=="l") 78 { 79 agentMesh=class_cylinder_l.createInstance("agentMesh_"+i); 80 //agentMesh.material=mat_global.mat_blue_e; 81 var color=new BABYLON.Color3(0,0,1); 82 agentMesh.overlayColor=color; 83 agentMesh.outlineColor=color; 84 arr_armys_l.push(obj_army); 85 if(!free)//如果不是免费的 86 { 87 resource_l-=obj_army.cost; 88 div_resource_l.innerHTML=resource_l; 89 } 90 resource_l2+=obj_army.cost; 91 div_resource_l2.innerHTML=resource_l2; 92 } 93 else if(side=="r") 94 { 95 agentMesh=class_cylinder_r.createInstance("agentMesh_"+i); 96 //agentMesh.material=mat_global.mat_red_e; 97 var color=new BABYLON.Color3(1,0,0); 98 agentMesh.overlayColor=color; 99 agentMesh.outlineColor=color; 100 arr_armys_r.push(obj_army); 101 if(!free) 102 { 103 resource_r-=obj_army.cost; 104 div_resource_r.innerHTML=resource_r; 105 } 106 resource_r2+=obj_army.cost; 107 div_resource_r2.innerHTML=resource_r2; 108 } 109 agentMesh.position=new BABYLON.Vector3(0,0.5,0); 110 agentMesh.scaling.x=unitType.attributes.size; 111 agentMesh.scaling.z=unitType.attributes.size; 112 agentMesh.renderingGroupId=2; 113 agentMesh.myType="unit"; 114 agentMesh.myType1=agentTransform.mydata.agentIndex; 115 //agentMesh.position.y = 0.5 ; 116 //agentMesh.bakeCurrentTransformIntoVertices(); 117 agentMesh.parent=agentTransform; 118 agentTransform.mydata.agentMesh=agentMesh; 119 var cardMesh=unitType.class_card.createInstance("card_"+i); 120 cardMesh.position=new BABYLON.Vector3(0,1+unitType.attributes.size*2/3,0); 121 //cardMesh.position.y=1+unitType.attributes.size*2/3; 122 cardMesh.parent=agentTransform; 123 agentTransform.mydata.cardMesh=cardMesh; 124 var plane_health=class_plane.createInstance("plane_health_"+i);//血条 125 var color=new BABYLON.Color3(0,1,0); 126 plane_health.overlayColor=color; 127 plane_health.outlineColor=color; 128 plane_health.scaling.x=unitType.attributes.size;//实际缩放值其实是一个比例乘以尺寸 129 plane_health.position=new BABYLON.Vector3(handlePlaneOffset(unitType.attributes.size,obj_army.health0,obj_army.health),unitType.attributes.size*2/3+0.6,0); 130 //plane_health.position.y=unitType.attributes.size*2/3; 131 plane_health.parent=cardMesh; 132 agentTransform.mydata.plane_health=plane_health; 133 var plane_move=class_plane2.createInstance("plane_move_"+i);//行动条 134 var color=new BABYLON.Color3(0.5,0.5,0.5); 135 plane_move.overlayColor=color; 136 plane_move.outlineColor=color; 137 plane_move.scaling.x=0*obj_army.size; 138 plane_move.position=new BABYLON.Vector3(handlePlaneOffset(unitType.attributes.size,100,0),unitType.attributes.size*2/3,-0.1); 139 //plane_move.position.y=unitType.attributes.size*2/3-0.5; 140 plane_move.parent=cardMesh; 141 agentTransform.mydata.plane_move=plane_move; 142 return obj_army; 143 144 //break; 145 } 146 } 147 if(!flag_found) 148 { 149 console.log("导航器不足!"); 150 } 151 }
5、卡牌对抗逻辑
代码位于actionLoop3.js文件的actionLoop方法中,该方法由裁判端每0.5秒执行一次,计算每个单位的攻击目标(最近的敌方单位),并调用moveAndFire方法令单位向目标移动攻击,然后将这些决策信息同步给红蓝双方的玩家端:
1 var obj_groupchange={}; 2 var obj_fightgroup={}; 3 var count_fightgroupid=1; 4 var obj_armybfound={}; 5 var obj_armyrfound={}; 6 var arr_fightgroup=[]; 7 var flag_pause=false; 8 var count_instance=0; 9 var obj_arrows_global={}; 10 var obj_simplify={}; 11 var obj_army_simplify={}; 12 var obj_arrow_simplify={}; 13 var obj_cp_simplify={}; 14 //基于距离计算单位的可见性、AI的全局策略、进入战斗的遭遇 15 //暂时假设各种军团的视野、移动速度、进入战斗距离都是一样的,后期可能添加差异性,所以距离判断算法要进行两次!? 16 function actionLoop() 17 { 18 if(!crowd)//如果导航群组还未初始化 19 { 20 return false; 21 } 22 if(type_client=="admin") 23 { 24 resource_r+=10;//增加双方钱数 25 resource_l+=10; 26 div_resource_r.innerHTML=resource_r; 27 div_resource_l.innerHTML=resource_l; 28 29 obj_groupchange={}; 30 obj_fightgroup={}; 31 count_fightgroupid=1; 32 //obj_armybfound={};//被b(AI)方发现的r方军团 33 arr_fightgroup=[]; 34 //obj_armyrfound={}; 35 for(var id in obj_armys) 36 { 37 var army=obj_armys[id]; 38 if(army.health<=0) 39 { 40 army.isAlive=false; 41 } 42 if(!army.isAlive) 43 { 44 removeCard(army.agentIndex); 45 //delete obj_armys[id]; 46 } 47 } 48 var lenl=arr_armys_l.length; 49 var lenr=arr_armys_r.length; 50 //先找到最近目标,然后向最近目标移动攻击,进入射程后发射射弹,射弹添加到全局变量中,追随目标移动,命中后进行结算(帧间计算) 51 for(var j=0;j<lenr;j++) { 52 var armyr = arr_armys_r[j]; 53 armyr.NearestDis = null; 54 armyr.NearestArmy = null; 55 } 56 for(var i=0;i<lenl;i++) { 57 var armyl = arr_armys_l[i]; 58 armyl.NearestDis = null; 59 armyl.NearestArmy = null; 60 if (!obj_armys[armyl.id] || !armyl.isAlive) { 61 continue; 62 } 63 for (var j = 0; j < lenr; j++) { 64 var armyr = arr_armys_r[j]; 65 if (!obj_armys[armyr.id] || armyr.生命 <= 0) { 66 continue; 67 } 68 var dis = vxz.distance(armyl.agentTransform.position, armyr.agentTransform.position); 69 isNearestArmyR(armyl, dis, armyr); 70 isNearestArmyR(armyr, dis, armyl); 71 } 72 } 73 for(var id in obj_armys) 74 { 75 var army=obj_armys[id]; 76 if(!army.fightingWith) 77 { 78 army.fightingWith=army.NearestArmy 79 } 80 else { 81 if(!army.fightingWith.isAlive) 82 { 83 army.fightingWith=army.NearestArmy; 84 } 85 //在平常情况下,单位倾向于一直攻击范围内的一个单位,直到将其摧毁 86 // if(army.charge==0) 87 // { 88 // army.fightingWith=army.NearestArmy; 89 // } 90 else { 91 if(vxz.distance(army.agentTransform.position,army.fightingWith.agentTransform.position)>army.range) 92 {//但如果正在集火时,敌方单位脱离的射程,则重新选择最近的敌方单位为目标,但装填进度仍保留 93 army.fightingWith=army.NearestArmy; 94 } 95 } 96 97 } 98 //如果让单位在每次发现更近的新单位时都切换瞄准目标,则单位有可能一直在重复瞄准,无法攻击!!!! 99 //所以如果已经开始装填,则不切换目标! 100 army.aimmingAt=army.fightingWith; 101 moveAndFire(army); 102 } 103 obj_simplify={};//用于同步状态的对象 104 //除单位外,还需同步射弹、资源、控制区 105 obj_army_simplify={}; 106 for(var id in obj_armys) 107 { 108 var army=obj_armys[id]; 109 simplifyArmy(army)//将单位信息整理为易于传输的格式 110 } 111 obj_simplify.obj_army_simplify=obj_army_simplify 112 obj_simplify.resource_r=resource_r; 113 obj_simplify.resource_l=resource_l; 114 obj_simplify.resource_r2=resource_r2; 115 obj_simplify.resource_l2=resource_l2; 116 obj_arrow_simplify={}; 117 for(var id in obj_arrows_global) 118 { 119 var arrow=obj_arrows_global[id]; 120 simplifyArrow(arrow)//将射弹信息整理为易于传输的格式 121 } 122 obj_simplify.obj_arrow_simplify=obj_arrow_simplify 123 obj_cp_simplify={}; 124 for(var side in controlPoint) 125 { 126 var obj_temp=controlPoint[side]; 127 obj_cp_simplify[side]={}; 128 for(var pointName in obj_temp) 129 { 130 var obj_temp2=obj_temp[pointName]; 131 if(obj_temp2) 132 { 133 obj_cp_simplify[side][pointName]=true;//这个控制区还存在 134 } 135 } 136 } 137 obj_simplify.obj_cp_simplify=obj_cp_simplify; 138 var str_message=JSON.stringify(obj_simplify); 139 var obj_msg={}; 140 obj_msg.data=str_message; 141 obj_msg.type="adminTalk2Player"; 142 obj_msg.userId=userId; 143 sendMessage(JSON.stringify(obj_msg)); 144 } 145 else if(type_client=="player") 146 { 147 148 } 149 }
moveAndFire方法对单位下达行动指令:
1 function moveAndFire(army) 2 { 3 if(army.aimmingAt&&army.aimmingAt.isAlive) 4 { 5 var dis = vxz.distance(army.agentTransform.position, army.aimmingAt.agentTransform.position); 6 if(dis>army.range)//在射程范围外,则进行移动 7 { 8 if(army.moveSpeed>0) 9 { 10 army.post=navigationPlugin.getClosestPoint(army.aimmingAt.agentTransform.position); 11 crowd.agentGoto(army.agentIndex, army.post)//对recast库下达指令 12 army.doing="walking"; 13 } 14 } 15 else { 16 //进入射程后停止移动,在最远距离攻击 17 if(crowd._agentDestinationArmed[army.agentIndex]) 18 { 19 crowd.agentTeleport(army.agentIndex,navigationPlugin.getClosestPoint(army.agentTransform.position)); 20 } 21 army.doing="fighting"; 22 if(army.charge>=100)//认为攻击动作是瞬间完成的?? 23 {//行动条达到100后发出攻击 24 army.charge-=100; 25 //army.agentTransform.mydata.plane_move.scaling.x=army.charge/100; 26 //发射 27 let mesh_arrow=army.class_projectile.createInstance("arrow_"+count_instance); 28 mesh_arrow.position=army.agentTransform.position.clone(); 29 mesh_arrow.position.y+=army.size*4/3; 30 mesh_arrow.mydata={ 31 speed:army.p_speed, 32 explosionRange:army.p_explosionRange, 33 attackPower: army.attackPower, 34 aimmingAt:army.aimmingAt,//击中敌人或敌人死亡(范围攻击会因此被削弱?)则消失?存在过长时间后也消失? 35 target:army.aimmingAt.agentTransform.position, 36 index:count_instance, 37 side:army.side, 38 from:army 39 }; 40 obj_arrows_global[count_instance]=mesh_arrow; 41 count_instance++; 42 } 43 else { 44 //army.charge+=army.attackSpeed; 45 //army.agentTransform.mydata.plane_move.scaling.x=army.charge/100; 46 } 47 } 48 } 49 //没有敌人时也可以装填呀!! 50 if(army.charge<100) 51 { 52 army.charge+=army.attackSpeed; 53 } 54 if(army.charge>100) 55 { 56 army.charge=100; 57 } 58 //处理行动条 59 handlePlane(army.agentTransform.mydata.plane_move,army.charge,100,army.size,0,-0.1) 60 //army.agentTransform.mydata.plane_move.scaling.x=army.charge/100; 61 62 }
在这个方法中还包括对行动条的处理和射弹发射代码,需要注意的是这里的单位移动和射弹发射只是下达了移动和发射命令(0.5秒执行一次),并不进行实际的移动,实际的单位移动计算由recast库进行,射弹移动计算由ControlWH2.js文件中的scene.registerAfterRender调用HandleArrows方法进行(每秒最多执行60次)。
射弹处理方法如下:
1 function HandleArrows(deltaTime) 2 { 3 for(var key in obj_arrows_global) 4 { 5 var arrow=obj_arrows_global[key]; 6 if(arrow.mydata.aimmingAt&&arrow.mydata.aimmingAt.isAlive)//如果目标还存在 7 { 8 arrow.mydata.target=arrow.mydata.aimmingAt.agentTransform.position; 9 moveOneArrow(arrow,arrow.mydata.target,arrow.mydata.speed,deltaTime,true) 10 } 11 else {//否则向最后追踪的位置移动,对于单体攻击在到达最终位置时消散,对于范围攻击在最终位置爆炸 12 moveOneArrow(arrow,arrow.mydata.target,arrow.mydata.speed,deltaTime,false) 13 } 14 } 15 } 16 function moveOneArrow(arrow,pos,speed,deltaTime,isTargetAlive)//向目标移动射弹 17 { 18 19 var dis=vxz.distance(arrow.position, pos); 20 if(dis<0.1) 21 { 22 explodeArrow(arrow,isTargetAlive); 23 } 24 else { 25 var deltaPos=speed*deltaTime/2000; 26 if(dis<=deltaPos) 27 { 28 arrow.position=pos;//到达事件,在下一帧触发?? 29 } 30 else { 31 var deltaPos2=pos.subtract(arrow.position).scale(deltaPos/dis); 32 if(deltaPos2.x>=0) 33 { 34 arrow.rotation.x=Math.PI/6; 35 arrow.rotation.y=0; 36 } 37 else { 38 arrow.rotation.x=-Math.PI/6; 39 arrow.rotation.y=Math.PI; 40 41 } 42 arrow.position.addInPlace(deltaPos2); 43 } 44 } 45 } 46 function explodeArrow(arrow,isTargetAlive)//射弹爆炸 47 { 48 if(userId!="admin") 49 {//如果需要添加爆炸效果?<-还是主要由admin计算!! 50 return null 51 } 52 if(arrow.mydata.explosionRange==0)//如果是单体攻击 53 { 54 if(!isTargetAlive) 55 { 56 57 } 58 else { 59 var armyTarget=arrow.mydata.aimmingAt; 60 armyTarget.health-=arrow.mydata.attackPower; 61 if(armyTarget.health<0) 62 { 63 armyTarget.health=0 64 }//处理血条 65 handlePlane(armyTarget.agentTransform.mydata.plane_health,armyTarget.health,armyTarget.health0,armyTarget.size,0.6,0) 66 //armyTarget.agentTransform.mydata.plane_health.scaling.x=armyTarget.health/armyTarget.health0 67 } 68 removeArrow(arrow); 69 } 70 else { 71 //var arr_army=[]; 72 if(arrow.mydata.side=="l") 73 {//如果是范围攻击,则对范围内的所有敌人造成伤害 74 hurtArmyAround(arr_armys_r,arrow.position,arrow.mydata.explosionRange,arrow.mydata.attackPower); 75 } 76 else if(arrow.mydata.side=="r") 77 { 78 hurtArmyAround(arr_armys_l,arrow.position,arrow.mydata.explosionRange,arrow.mydata.attackPower); 79 } 80 removeArrow(arrow);//移除爆炸后的射弹 81 } 82 } 83 function removeArrow(arrow) 84 { 85 arrow.dispose(); 86 delete obj_arrows_global[arrow.mydata.index]; 87 } 88 function hurtArmyAround(arr_army,pos,range,power) 89 { 90 var len=arr_army.length; 91 for(var i=0;i<len;i++) { 92 let armyTarget = arr_army[i]; 93 if (!obj_armys[armyTarget.id] || !armyTarget.isAlive) { 94 continue; 95 } 96 else { 97 var dis=vxz.distance(armyTarget.agentTransform.position, pos); 98 if(dis<=range) 99 { 100 armyTarget.health-=power; 101 if(armyTarget.health<0) 102 { 103 armyTarget.health=0 104 } 105 handlePlane(armyTarget.agentTransform.mydata.plane_health,armyTarget.health,armyTarget.health0,armyTarget.size,0.6,0) 106 //armyTarget.agentTransform.mydata.plane_health.scaling.x=armyTarget.health/armyTarget.health0; 107 } 108 } 109 } 110 }
为简化编程,本项目直接用主线程执行actionLoop方法,也可参考https://www.cnblogs.com/ljzc002/p/15119505.html将这些计算转移到work线程中,进一步释放多核CPU算力。
6、生命周期方法
本项目采用“生命周期方法”为卡牌添加类似“战吼”、“亡语”之类的触发式能力,编程者可手动将这些方法添加到单位类或单位实例的属性中,例如2026011306_中世纪防御塔.js文件中的:
1 lifeSycle:{ 2 onBeforePut:[function(army)//可在生命周期数组中插入多个生命周期响应方法 3 { 4 //console.log(army);//尝试能否关联这一变量!!!! 5 //console.log(this); 6 cantPutNearEnemy(army,100) 7 }], 8 onAfterDied:[function(army){ 9 if(army.pointName&&army.cp) 10 { 11 controlPoint[army.side][army.pointName].mesh.dispose(); 12 controlPoint[army.side][army.pointName].lines.dispose(); 13 delete controlPoint[army.side][army.pointName]; 14 } 15 }] 16 }
这样防御塔将在建立前进行敌方检测,阻止玩家将建筑放置在过于接近敌方的位置;将在被摧毁后进行检测,如果该建筑关联控制点则摧毁控制点。编程者可根据需要添加更多的生命周期方法。
7 基于ws的多人互联
7.1状态同步
裁判端的actionLoop方法每0.5秒向玩家端发起一次状态同步命令,此时单位、射弹等信息将被整理为适宜发送的格式:
1 function simplifyArmy(army) 2 { 3 var obj_army={}; 4 obj_army.side=army.side; 5 obj_army.isAlive=true; 6 obj_army.posx=army.pos.x; 7 obj_army.posy=army.pos.y; 8 obj_army.posz=army.pos.z; 9 obj_army.cost=army.cost; 10 obj_army.agentIndex=army.agentIndex; 11 obj_army.id=army.id; 12 obj_army.name=army.name; 13 obj_army.typeName=army.typeName; 14 obj_army.charge=army.charge; 15 obj_army.health0=army.health0; 16 obj_army.health=army.health; 17 obj_army.moveSpeed=army.moveSpeed; 18 obj_army.size=army.size; 19 obj_army.doing=army.doing; 20 if(army.post) 21 { 22 obj_army.postx=army.post.x;//移动目标 23 obj_army.posty=army.post.y; 24 obj_army.postz=army.post.z; 25 } 26 27 obj_army.cp=army.cp; 28 obj_army.pointName=army.pointName; 29 if(obj_army.cp) 30 { 31 obj_army.pathCP=simplifyPath(army.pathCP);//把控制区信息也传过去!!!! 32 } 33 34 obj_army_simplify[obj_army.id]=obj_army; 35 } 36 function simplifyArrow(arrow) 37 { 38 var obj_arrow={}; 39 obj_arrow.id=arrow.mydata.index; 40 obj_arrow.speed=arrow.mydata.speed; 41 obj_arrow.explosionRange=arrow.mydata.explosionRange; 42 obj_arrow.attackPower=arrow.mydata.attackPower; 43 obj_arrow.aimmingAt=arrow.mydata.aimmingAt.id; 44 obj_arrow.posx=arrow.position.x;//位置 45 obj_arrow.posy=arrow.position.y; 46 obj_arrow.posz=arrow.position.z; 47 obj_arrow.targetx=arrow.mydata.target.x;//位置 48 obj_arrow.targety=arrow.mydata.target.y; 49 obj_arrow.targetz=arrow.mydata.target.z; 50 obj_arrow.index=arrow.mydata.index; 51 obj_arrow.side=arrow.mydata.side; 52 obj_arrow.armyid=arrow.mydata.from.id; 53 obj_arrow_simplify[obj_arrow.id]=obj_arrow; 54 }
之所以需要这样做是因为bbl的网格等对象内部存在循环调用,无法直接序列化,所以需要先转为描述符号,再由接收端根据描述重建,玩家端处理状态同步的代码在handleWS.js文件中:
1 function initWSPlayer(callback)//玩家端的ws初始化代码 2 { 3 ws=new WebSocket("ws://"+localIP+":"+wsPort);//建立ws链接 4 //FrameGround.ImportObjGround("../assets/map/","ObjGround20210427.babylon",webGLStart2,obj_ground,false); 5 ws.onopen = function (e) {//连接建立后执行 6 console.log('Connection to server opened'); 7 wsConnected = true; 8 //@@@@这里应该到admin去获取初始化信息以及自身在ws中的通道id,获取成功后才算连接完成!!!! 9 var obj_msg={}; 10 //obj_msg.data=str_message; 11 obj_msg.type="login"; 12 obj_msg.userId=userId; 13 sendMessage(JSON.stringify(obj_msg));//建立连接后需把前端的身份信息发送到后端,并由后端记录, 14 //才可按用户id精确发送信息,可以把这个过程叫做注册或登录 15 //callback(); 16 } 17 ws.onclose = function(e) { 18 // 可以在 onclose 和 onerror 中处理重连的逻辑,再决定是否将状态更新为未连接状态 19 wsConnected = false; 20 } 21 22 ws.onerror = function(e) { 23 wsConnected = false; 24 } 25 ws.onmessage = function(e) {//收到信息时执行 26 27 var msg = JSON.parse(e.data); 28 switch(msg.type) { 29 case "adminTalk2Player"://admin发来的状态同步命令 30 { 31 if(!crowd) 32 { 33 return false; 34 } 35 var userId=msg.userId; 36 var obj_simplify=JSON.parse(msg.data);//解析数据,同步状态 37 if(obj_simplify.obj_army_simplify)//adminTalk2Player是信息发送方式,以这种方式发送的信息又可能有多种目的 38 {//如果是状态同步 39 resource_r=obj_simplify.resource_r; 40 div_resource_r.innerHTML=resource_r; 41 resource_r2=obj_simplify.resource_r2; 42 div_resource_r2.innerHTML=resource_r2; 43 resource_l=obj_simplify.resource_l; 44 div_resource_l.innerHTML=resource_l; 45 resource_l2=obj_simplify.resource_l2; 46 div_resource_l2.innerHTML=resource_l2; 47 var obj_army_simplify=obj_simplify.obj_army_simplify; 48 for(var id in obj_army_simplify) 49 { 50 var obj_army=obj_army_simplify[id]; 51 if(!obj_armys[id])//该用户端尚无此单位 52 { 53 var obj_army2; 54 if(obj_army.cp) 55 { 56 obj_army2=createControlPoint2(new BABYLON.Vector3(obj_army.posx,obj_army.posy,obj_army.posz) 57 ,parsePath(obj_army.pathCP) 58 ,"mesh_sidemask",obj_unittype_global[obj_army.typeName],obj_army.side,obj_army.pointName,0.5); 59 } 60 else { 61 obj_army2=createCard(obj_unittype_global[obj_army.typeName] 62 ,new BABYLON.Vector3(obj_army.posx,obj_army.posy,obj_army.posz) 63 ,obj_army.side,true); 64 } 65 obj_army2.health=obj_army.health; 66 handlePlane(obj_army2.agentTransform.mydata.plane_health,obj_army.health,obj_army.health0,obj_army.size,0.6,0); 67 obj_army2.charge=obj_army.charge; 68 handlePlane(obj_army2.agentTransform.mydata.plane_move,obj_army.charge,100,obj_army.size,0,-0.1) 69 if(obj_army.doing=="walking") 70 { 71 obj_army2.doing="walking"; 72 obj_army2.post=new BABYLON.Vector3(obj_army.postx,obj_army.posty,obj_army.postz) 73 crowd.agentGoto(obj_army2.agentIndex, obj_army2.post);//不同端的agentIndex可能是不同的!!!! 74 } 75 76 } 77 else { 78 var obj_army2=obj_armys[id]; 79 obj_army2.pos=new BABYLON.Vector3(obj_army.posx,obj_army.posy,obj_army.posz); 80 obj_army2.health=obj_army.health; 81 handlePlane(obj_army2.agentTransform.mydata.plane_health,obj_army.health,obj_army.health0,obj_army.size,0.6,0); 82 obj_army2.charge=obj_army.charge; 83 handlePlane(obj_army2.agentTransform.mydata.plane_move,obj_army.charge,100,obj_army.size,0,-0.1) 84 if(obj_army.doing=="walking") 85 { 86 obj_army2.post=new BABYLON.Vector3(obj_army.postx,obj_army.posty,obj_army.postz) 87 crowd.agentGoto(obj_army2.agentIndex, obj_army2.post);//不同端的agentIndex可能是不同的!!!! 88 } 89 else {//admin端已经停止 90 if(obj_army2.doing=="walking") 91 { 92 crowd.agentTeleport(obj_army2.agentIndex,navigationPlugin.getClosestPoint(obj_army2.agentTransform.position)); 93 } 94 } 95 obj_army2.doing=obj_army.doing; 96 } 97 } 98 for(var id in obj_armys)//用户端有,但管理端未同步状态,认为死亡 99 { 100 if(!obj_army_simplify[id]) 101 { 102 removeCard(obj_armys[id].agentIndex); 103 } 104 } 105 var obj_arrow_simplify=obj_simplify.obj_arrow_simplify;//同步所有射弹 106 for(var id in obj_arrow_simplify) 107 { 108 var obj_arrow=obj_arrow_simplify[id]; 109 if(!obj_arrows_global[id])//该用户端尚无此射弹 110 { 111 var army=obj_armys[obj_arrow.armyid]; 112 if(!army) 113 { 114 console.error("未在用户端找到"+id+"射弹的发射者"+obj_arrow.armyid); 115 } 116 else 117 { 118 let mesh_arrow=army.class_projectile.createInstance("arrow_"+count_instance); 119 mesh_arrow.position=army.agentTransform.position.clone(); 120 mesh_arrow.position.y+=army.size*4/3; 121 var aimmingAt=obj_armys[obj_arrow.aimmingAt]; 122 mesh_arrow.mydata={ 123 speed:army.p_speed, 124 explosionRange:army.p_explosionRange, 125 attackPower: army.attackPower, 126 aimmingAt:aimmingAt,//击中敌人或敌人死亡(范围攻击会因此被削弱?)则消失?存在过长时间后也消失? 127 target:aimmingAt.agentTransform.position, 128 index:obj_arrow.id, 129 side:army.side, 130 from:army 131 }; 132 obj_arrows_global[obj_arrow.id]=mesh_arrow; 133 //count_instance++; 134 } 135 136 } 137 else {//更新已有射弹 138 var mesh_arrow=obj_arrows_global[id]; 139 mesh_arrow.position=new BABYLON.Vector3(obj_arrow.posx,obj_arrow.posy,obj_arrow.posz); 140 141 142 } 143 } 144 for(var id in obj_arrows_global ) 145 { 146 if(!obj_arrow_simplify[id]) 147 { 148 removeArrow(obj_arrows_global[id]) 149 } 150 } 151 //控制点不应再移除一次,因为这个单位已经在army同步时被删除一次!!!! 152 var obj_cp_simplify=obj_simplify.obj_arrow_simplify; 153 // for(var side in obj_cp_simplify) 154 // { 155 // var obj_temp=obj_cp_simplify[side]; 156 // var obj_temp2=controlPoint[side]; 157 // if(obj_temp2&&!obj_temp)//这一方的所有控制点被摧毁 158 // { 159 // for(var pointName in obj_temp2) 160 // { 161 // var obj_cp=obj_temp2[pointName]; 162 // if(obj_cp) 163 // { 164 // removeCard(obj_cp.army.agentIndex); 165 // } 166 // } 167 // //delete controlPoint[side]; 168 // } 169 // else { 170 // for(var pointName in obj_temp2) 171 // { 172 // var obj_cp=obj_temp2[pointName]; 173 // if(obj_cp&&!obj_temp[pointName]) 174 // { 175 // removeCard(obj_cp.army.agentIndex); 176 // } 177 // } 178 // } 179 // } 180 break; 181 } 182 //logWs(userId+":"+msg.data) 183 } 184 case "loginBack": 185 { 186 console.log('完成身份注册'); 187 callback(); 188 break; 189 } 190 } 191 } 192 }
7.2命令同步
玩家端放置棋子时,将放置棋子的命令同步到裁判端,由裁判端判定可以放置后,先由裁判端生成棋子,然后通过每0.5秒一次的状态同步发送到所有玩家端。
玩家端的发送方法为:
1 function createCardMsg(unitType,pos,side_current) 2 { 3 var unitTypeName="unit_"+unitType.timestamp+"_"+unitType.name; 4 var str_message=JSON.stringify({ 5 unitTypeName:unitTypeName, 6 posx:pos.x,posy:pos.y,posz:pos.z,side:side_current 7 }); 8 var obj_msg={}; 9 obj_msg.data=str_message; 10 obj_msg.type="playerTalk2Admin"; 11 obj_msg.userId=userId; 12 sendMessage(JSON.stringify(obj_msg)); 13 }
裁判端的处理方法为:
1 function initWSAdmin(callback) 2 { 3 ws=new WebSocket("ws://"+localIP+":"+wsPort); 4 //FrameGround.ImportObjGround("../assets/map/","ObjGround20210427.babylon",webGLStart2,obj_ground,false); 5 ws.onopen = function (e) { 6 console.log('Connection to server opened'); 7 wsConnected = true; 8 //@@@@需增加一个身份注册环节,该注册环节完成后才可正常进行状态同步!!!! 9 var obj_msg={}; 10 //obj_msg.data=str_message; 11 obj_msg.type="login"; 12 obj_msg.userId=userId; 13 sendMessage(JSON.stringify(obj_msg)); 14 15 //callback(); 16 } 17 ws.onclose = function(e) { 18 // 可以在 onclose 和 onerror 中处理重连的逻辑,再决定是否将状态更新为未连接状态 19 wsConnected = false; 20 } 21 22 ws.onerror = function(e) { 23 wsConnected = false; 24 } 25 ws.onmessage = function(e) { 26 27 var msg = JSON.parse(e.data); 28 switch(msg.type) { 29 case "playerTalk2Admin"://player发来的操作请求 30 { 31 if(!crowd) 32 { 33 return false; 34 } 35 var userId=msg.userId; 36 var obj_simplify=JSON.parse(msg.data); 37 if(obj_simplify.unitTypeName)//如果是player发来的新建army请求 38 { 39 createCard(obj_unittype_global[obj_simplify.unitTypeName] 40 ,new BABYLON.Vector3(obj_simplify.posx,obj_simplify.posy,obj_simplify.posz),obj_simplify.side); 41 } 42 break; 43 } 44 case "loginBack": 45 { 46 console.log('完成身份注册'); 47 callback(); 48 break; 49 } 50 } 51 } 52 }
7.3 ws服务端转发管理
ws服务端用两层结构管理各类信息的转发,WebSocketServer类处理偏底层的转发:
1 public class WebSocketServer extends SimpleChannelInboundHandler<Object> { 2 // @Autowired 3 // WebSocketHandler webSocketHandler; 4 5 // public WebSocketServer(){ 6 // 7 // } 8 @PreDestroy 9 public void cleanup() { 10 // 在这里编写关闭前需要执行的代码 11 // 例如:关闭文件流、数据库连接等 12 String path_root=System.getProperty("user.dir");//java命令运行目录 13 String cmd="cd "+path_root+";nginx -s stop"; 14 Runtime runtime = Runtime.getRuntime(); 15 Process proce = null; 16 try{ 17 proce = runtime.exec(cmd); 18 }catch (Exception e) { 19 e.printStackTrace(); 20 } 21 22 } 23 private WebSocketServerHandshaker handshaker; 24 private AsciiString contentType = HttpHeaderValues.TEXT_PLAIN; 25 // onmsg 26 // 有信号进来时 27 @Override 28 protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception 29 {//ChannelHandlerContext在handler之间传递上下文 30 if(msg instanceof FullHttpRequest){ 31 handHttpRequest(ctx, (FullHttpRequest) msg); 32 }else if(msg instanceof WebSocketFrame){ 33 handWsMessage(ctx, (WebSocketFrame) msg); 34 } 35 } 36 37 // onopen 38 // Invoked when a Channel is active; the Channel is connected/bound and ready. 39 // 当连接打开时,这里表示有数据将要进站。 40 @Override 41 public void channelActive(ChannelHandlerContext ctx) throws Exception { 42 NettyConfig.group.add(ctx.channel());//group中保存所有建立会话的channel 43 WsSession wsSession=new WsSession(); 44 wsSession.ChannelId=ctx.channel().id().toString(); 45 46 NettyConfig.mapSession.put(wsSession.ChannelId,wsSession);//为连接通道关联一个session 47 } 48 49 // onclose 50 // Invoked when a Channel leaves active state and is no longer connected to its remote peer. 51 // 当连接要关闭时 52 @Override 53 public void channelInactive(ChannelHandlerContext ctx) throws Exception { 54 //broadcastWsMsg( ctx, new WsMessage("oneChannelClose", ctx.channel().id().toString() ) ); 55 NettyConfig.group.remove(ctx.channel()); 56 NettyConfig.mapSession.remove(ctx.channel().id().toString());//同步删除session 57 } 58 59 // onmsgover 60 // Invoked when a read operation on the Channel has completed. 61 @Override 62 public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { 63 ctx.flush(); 64 } 65 66 // onerror 67 // 发生异常时 68 @Override 69 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 70 cause.printStackTrace(); 71 ctx.close(); 72 } 73 74 // 集中处理 ws 中的消息 75 private void handWsMessage(ChannelHandlerContext ctx, WebSocketFrame msg) { 76 if(msg instanceof CloseWebSocketFrame){ 77 // 关闭指令 78 handshaker.close(ctx.channel(), (CloseWebSocketFrame) msg.retain()); 79 } 80 if(msg instanceof PingWebSocketFrame) { 81 // ping 消息 82 ctx.channel().write(new PongWebSocketFrame(msg.content().retain())); 83 }else if(msg instanceof TextWebSocketFrame) 84 { 85 TextWebSocketFrame message = (TextWebSocketFrame) msg; 86 // 文本消息 87 //WsMessage wsMessage = JSON.parseObject(message.text(),WsMessage.class);//gson.fromJson(message.text(), WsMessage.class); 88 WebSocketHandler webSocketHandler = new WebSocketHandler();//每次传来信息都建立一个处理器 89 webSocketHandler.SwitchMessageType(message.text(),ctx); 90 91 }else { 92 // donothing, 暂时不处理二进制消息 93 } 94 } 95 96 private SimpleDateFormat df=new SimpleDateFormat("yyyy-MM-dd"); 97 private String str_gzrq=df.format(new Date()); 98 // 处理 http 请求,WebSocket 初始握手 (opening handshake ) 都始于一个 HTTP 请求 99 private void handHttpRequest(ChannelHandlerContext ctx, FullHttpRequest msg) { 100 if(!msg.decoderResult().isSuccess())//如果解码失败 101 { 102 sendHttpResponse(ctx, new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, 103 HttpResponseStatus.BAD_REQUEST)); 104 return; 105 } 106 else if(!("websocket".equals(msg.headers().get("Upgrade")))) 107 {//这部分http的内容交给tomcat去做 108 //如果不是ws握手请求 109 //decoderResult是指解码是否成功,而不是实际的解码结果! 110 //String str_res=msg.decoderResult().toString(); 111 //这个返回的是msg的描述信息,比如长度之类的! 112 //String str_res=msg.content().toString(); 113 String str_uri=msg.uri();//发现favicon.ico的请求也会到达这里。。。 114 115 String str_param=MyNettyUtil.convertByteBufToString(msg.content());//数据被netty整理成了类似get参数字符串的形式??!! 116 Map<String, String> mapParam = MyNettyUtil.SplitParam(str_param); 117 String str_func=mapParam.get("func"); 118 //String str_res=""; 119 JSONObject obj_json=new JSONObject(); 120 121 DefaultFullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, 122 HttpResponseStatus.OK, 123 Unpooled.wrappedBuffer(JSON.toJSONString(obj_json).getBytes())); 124 HttpHeaders heads = response.headers(); 125 heads.add(HttpHeaderNames.CONTENT_TYPE, contentType + "; charset=UTF-8"); 126 heads.add(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes()); 127 heads.add(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE); 128 ctx.write(response); 129 return; 130 } 131 WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory("ws://" 132 + NettyConfig.WS_HOST + NettyConfig.WS_PORT, null, false); 133 handshaker = factory.newHandshaker(msg); 134 if(handshaker == null){ 135 WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel()); 136 } else { 137 handshaker.handshake(ctx.channel(), msg); 138 } 139 } 140 141 // 处理错误的Http请求 142 private void sendHttpResponse(ChannelHandlerContext ctx, DefaultFullHttpResponse res) { 143 if(res.status().code() != 200){ 144 ByteBuf buf = Unpooled.copiedBuffer(res.status().toString(), CharsetUtil.UTF_8); 145 res.content().writeBytes(buf); 146 buf.release(); 147 } 148 ChannelFuture f = ctx.channel().writeAndFlush(res); 149 if(res.status().code() != 200){ 150 f.addListener(ChannelFutureListener.CLOSE); 151 } 152 } 153 //广播,也给自己发! 154 public static void broadcastWsMsg0(String str_json) { 155 NettyConfig.group.stream() 156 //.filter(channel -> channel.id().toString() != ChannelId) 157 .forEach(channel -> { 158 channel.writeAndFlush( new TextWebSocketFrame( str_json)); 159 }); 160 } 161 // 广播 websocket 消息(不给自己发) 162 public static void broadcastWsMsg(String str_json,String ChannelId) { 163 NettyConfig.group.stream() 164 .filter(channel -> channel.id().toString() != ChannelId) 165 .forEach(channel -> { 166 channel.writeAndFlush( new TextWebSocketFrame( str_json)); 167 }); 168 } 169 // 向一个user的所有连接广播 websocket 消息 170 public static void broadcastWsMsg2(String str_json,String UserId,String ChannelId) { 171 NettyConfig.group.stream() 172 .filter(channel -> ((NettyConfig.mapSession.get(channel.id().toString()).UserId.equals(UserId)&&channel.id().toString()!=ChannelId ))) 173 .forEach(channel -> { 174 channel.writeAndFlush( new TextWebSocketFrame( str_json)); 175 }); 176 } 177 // 向一个user的第一个连接发送 websocket 消息 178 public static void broadcastWsMsg3(String str_json,String UserId,String ChannelId) { 179 180 NettyConfig.group.stream() 181 .filter(channel -> ((NettyConfig.mapSession.get(channel.id().toString()).UserId.equals(UserId)&&!channel.id().toString().equals(ChannelId) ))) 182 .forEach(channel -> { 183 channel.writeAndFlush( new TextWebSocketFrame( str_json)); 184 return ; 185 }); 186 } 187 //向一个user以外的所有连接发送websocket消息 188 public static void broadcastWsMsg4(String str_json,String UserId,String ChannelId) { 189 190 NettyConfig.group.stream() 191 .filter(channel -> ((!NettyConfig.mapSession.get(channel.id().toString()).UserId.equals(UserId) 192 &&!channel.id().toString().equals(ChannelId) ))) 193 .forEach(channel -> { 194 channel.writeAndFlush( new TextWebSocketFrame( str_json)); 195 return ; 196 }); 197 } 198 199 //针对某一连接的私聊 200 public static void PrivateTalk(String str_json,String ChannelId) 201 { 202 NettyConfig.group.stream() 203 .filter(channel -> channel.id().toString().equals(ChannelId) )//这里可能不只一个?? 204 .forEach(channel -> { 205 channel.writeAndFlush( new TextWebSocketFrame( str_json)); 206 }); 207 /*for(Websocket item: webSocketSet) 208 { 209 if(item.sessionId.equals(sessionId)) 210 { 211 try { 212 item.sendMessage(str_json); 213 } catch (IOException e) { 214 e.printStackTrace(); 215 //continue; 216 } 217 break; 218 } 219 }*/ 220 } 221 222 }
WebSocketHandler类根据前端请求类型的不同,调用WebSocketServer类的方法完成转发:
1 @Controller 2 @ResponseBody 3 @Component 4 //@Service 5 public class WebSocketHandler { 6 7 //private String str_pgid="test";//暂时规定只有一个test playground 8 //@Autowired 9 public WebSocketHandler(){ 10 } 11 //= GetBeanUtil.getBean(JdbcTemplate.class); 12 13 public void SwitchMessageType(String str_json, ChannelHandlerContext ctx) 14 { 15 try { 16 //ApplicationContext app=new ClassPathXmlApplicationContext("application.properties");//这个方法是用来读xml文件的,prop读不了! 17 //JdbcTemplate jdbcTemplate = new JdbcTemplate(); 18 String ChannelId = ctx.channel().id().toString(); 19 WsSession wsSession = NettyConfig.mapSession.get(ChannelId);//获取建立连接时记录的身份对象 20 // ,这里并没有httpsession,这个WsSession是存在后台java中的 21 JSONObject obj_json=new JSONObject(); 22 String type=""; 23 try { 24 obj_json = JSON.parseObject(str_json);//前台可能传来异常的JSON? 25 type = obj_json.getString("type"); 26 } 27 catch(Exception e) 28 { 29 e.printStackTrace(); 30 System.out.println(wsSession.UserId); 31 System.out.println(str_json); 32 } 33 34 switch (type) { 35 //用户分为管理员、操作者、观众三种 36 case "oneChangePlayer2Admin"://操作者向管理员发送一步操作,处理该操作前需确定管理员处的世界状态未发生变化 37 //,如世界状态发生变化则操作者的这次操作不能生效。 38 { 39 String userId=obj_json.getString("userId");//从前台传来的身份信息 40 String stateId=obj_json.getString("stateId");//前台携带的世界状态id 41 if(stateId.equals(NettyConfig.stateId)) 42 { 43 WebSocketServer.broadcastWsMsg4(str_json,userId,ChannelId);//原样发送 44 } 45 else 46 { 47 JSONObject obj_msg=new JSONObject(); 48 obj_msg.put("type", "forcedAlign");//强制用管理员的状态对齐操作者的状态 49 obj_msg.put("data",NettyConfig.worldState); 50 WebSocketServer.PrivateTalk(JSON.toJSONString(obj_msg), ChannelId); 51 } 52 break; 53 } 54 case "talk2EveryOne": 55 { 56 //String userId=obj_json.getString("userId"); 57 WebSocketServer.broadcastWsMsg0(str_json); 58 break; 59 } 60 case "adminTalk2Player": 61 { 62 String userId=obj_json.getString("userId"); 63 WebSocketServer.broadcastWsMsg4(str_json,"admin","all"); 64 break; 65 } 66 case "playerTalk2Admin": 67 { 68 //String userId=obj_json.getString("userId"); 69 WebSocketServer.broadcastWsMsg2(str_json,"admin","all"); 70 break; 71 } 72 case "login": 73 { 74 //String userId=obj_json.getString("userId"); 75 String userId=obj_json.getString("userId"); 76 wsSession.UserId=userId;//将前端传来的用户id和后端的wsSession对象关联起来(注册) 77 obj_json.put("type","loginBack"); 78 str_json=JSON.toJSONString(obj_json); 79 WebSocketServer.PrivateTalk(str_json,ChannelId);//不解析原样返回? 80 //WebSocketServer.broadcastWsMsg2(str_json,"admin","all"); 81 break; 82 } 83 default: 84 break; 85 86 } 87 } 88 catch(Exception e) 89 { 90 e.printStackTrace(); 91 } 92 } 93 }
开发者可根据自己的需要编写更多的ws服务端方法。
8 总结与展望
经过前面的环节,本项目成功实现了简单的局域网多人自走棋玩法,并创建了可用的地形编辑工具和角色编辑工具。接下来可向游戏中添加更多种类的棋子,并根据游玩体验进一步优化游戏玩法。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!
标签:
相关文章
最新发布
- Ant Design Ellipsis 中的判断逻辑 isEleEllipsis 方法非常消耗性能
- Harness Engineering 学习与实践
- 聊聊 ASP.NET Core 中间件和过滤器的区别
- 鱼皮 AI 导航网站,突然起飞了!
- 基于 Irrlicht 和 WASAPI 的 Simple Audio Visualization 技术开发报告
- .NET 8 性能优化实战:让你的应用起飞
- Kthena + vLLM-Ascend:云原生大模型推理的编排与调度实践
- 网页端3D编程小实验-一种多人自走棋游戏原型
- 标书智能体(四)——提示词顺序优化,让缓存命中,输入成本直降10倍
- 一文吃透 Spring AI Alibaba + MCP:服务端搭建 + 客户端调用全流程

