摘要:为解决常规自走棋游戏配置灵活度低且难以在局域网跨平台联机的问题,本文基于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.log10 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 try201 {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);//这个范围内的地面高度设为617 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 ((!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");//前台携带的世界状态id41 if(stateId.equals(NettyConfig.stateId))42 {43 WebSocketServer.broadcastWsMsg4(str_json,userId,ChannelId);//原样发送44 }45 else46 {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 总结与展望
经过前面的环节,本项目成功实现了简单的局域网多人自走棋玩法,并创建了可用的地形编辑工具和角色编辑工具。接下来可向游戏中添加更多种类的棋子,并根据游玩体验进一步优化游戏玩法。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |