首页 > 基础资料 博客日记

网页端3D编程小实验-一种多人自走棋游戏原型

2026-04-03 16:30:03基础资料围观1

极客资料网推荐网页端3D编程小实验-一种多人自走棋游戏原型这篇文章给大家,欢迎收藏极客资料网享受知识的乐趣

摘要:为解决常规自走棋游戏配置灵活度低且难以在局域网跨平台联机的问题,本文基于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  

解压后目录如下图:

image

 主目录由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框架程序,其系统架构为:

576241E82517241592AE9F4015FE1346

 

其入口方法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控制点设置

控制点绑定在一个固定单位(如防御塔、城堡)上,在这个单位被摧毁前敌方将不能在控制点影响区域内放置单位

image

 例如图中的上中下三塔和高地城堡为红蓝双方的控制点,分割地图的虚线表示控制点的控制范围,在这些控制点被摧毁前敌方不能在虚线范围内放置单位。

示例地图的控制点设置代码如下:

  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);

启用后可在地面纹理的下方看到导航网格的实际覆盖范围,可借助它调整单位的可通行区域,例如高低地之间的连接通道

image

 当前版本的调试工具会不断重新刷新调试网格,所以打开调试器后帧率明显下降——在只进行简单导航时reacst1版本可能更有性价比。

3 棋子角色设计

访问http://ip/tool/createUnit3/dwsj1/index.html可打开单位设计页面:

image

 该设计器页面由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 总结与展望

经过前面的环节,本项目成功实现了简单的局域网多人自走棋玩法,并创建了可用的地形编辑工具和角色编辑工具。接下来可向游戏中添加更多种类的棋子,并根据游玩体验进一步优化游戏玩法。

 


文章来源:https://www.cnblogs.com/ljzc002/p/19774986
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:jacktools123@163.com进行投诉反馈,一经查实,立即删除!

标签:

相关文章

本站推荐

标签云