websocket实战(4) websocket版贪食蛇游戏(tomcat官方自带)

简介:

通过前面3篇的阐述,相信可以构建一个简单的socket应用了。当然,也会遗漏了许多知识点,相信会在以后分享的实例中捎带说明下。

本文的主要是分析下tomcat官方自带的贪食蛇游戏。为什么选择分析这个项目呢。

  1. 贪食蛇游戏规则,人人明白,业务方面不需要过多解释(当然这款websocket版的游戏规则也有一定特色)。

  2. 游戏设计简单,一个对象足以完成游戏,但不涉及到一些复杂的逻辑算法。

  3. 通过游戏,有很好的代入感

1.游戏规则介绍

1.能够实现贪吃蛇自动向前移动,一旦贪食蛇选择了方向,贪食蛇就按所选方向开始运动,可以任意。移动方向为贪吃蛇当前行走方向。

2.游戏通过键盘的上下左右四个方向控制贪吃蛇当前行走方向。(没有可以吃的食物)。

3.支持对战功能,如果发生碰撞情况,后蛇会自杀,重置信息,重新来玩。

4.如果移动出画布外,从对立方向进入,移动方向不变。

界面是"群蛇乱舞”界面。

wKioL1fmgm_BD8kHAABZHlwP-Pc747.png

2.贪食蛇设计

贪食蛇状态快照

wKioL1fmgwKQ4IHJAACnQxhS7nQ267.png

贪食蛇类图

wKiom1fmhD6T-8RFAAAavKnQfwQ348.png

贪食蛇:有几个重要属性。颜色,头(head),身体(tail),行动方向。

颜色:随机生成。

头&身体:决定蛇的长度,在画布中的位置。还有决定是否发生碰撞。有(x,y)坐标说明。

行动方向:东西南北四个方向。

重点说一下和websocket相关的信息。贪食蛇的session属性。

session主要负责贪食蛇状态信息的传播,将自己的颜色和位置信息传递到前端。

传播时机

  1. 状态变化要传播(kill,join,..)

  2. 位置变化要传播(包括方向,其实也是状态变化)

  3. 重置要传播(也是状态变化)

wKioL1fmiJKAUdk6AAApMbwl_rI734.png

分析序列图得知,其实作为游戏的websocket的EndPoint,做的事情很简单。两件事

  1. 有新需求:创建贪食蛇,发送渲染命令(join)

  2. 响应客户端的命令(方向命令)

不难分析,游戏贪食蛇的移动,是应该有定时器驱动的,所有贪食蛇位置的变化,都是通过SnakeTimer驱动的。然后更新位置信息,最后调用贪食蛇,将自己信息传递到前端。所以定时器,需要维护贪食蛇的聚合信息。

1.贪食蛇聚合信息维护(CRD,没有更新,贪食蛇信息的更新不属于聚合信息范畴)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected  static  synchronized  void  addSnake(Snake snake) {
     if  (snakes.size() ==  0 ) {
         startTimer();
     }
     snakes.put(Integer.valueOf(snake.getId()), snake);
}
 
 
protected  static  Collection<Snake> getSnakes() {
     return  Collections.unmodifiableCollection(snakes.values());
}
 
 
protected  static  synchronized  void  removeSnake(Snake snake) {
     snakes.remove(Integer.valueOf(snake.getId()));
     if  (snakes.size() ==  0 ) {
         stopTimer();
     }
}

2. 消息广播(将贪食蛇最新状态信息,实时广播到前端)

就是调用snake自动发送,不难猜,调用session相关的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//SnakeTimer.java
protected  static  void  broadcast(String message) {
     for  (Snake snake : SnakeTimer.getSnakes()) {
         try  {
             snake.sendMessage(message);
         catch  (IllegalStateException ise) {
             // An ISE can occur if an attempt is made to write to a
             // WebSocket connection after it has been closed. The
             // alternative to catching this exception is to synchronise
             // the writes to the clients along with the addSnake() and
             // removeSnake() methods that are already synchronised.
         }
     }
}
//Snake.java
protected  void  sendMessage(String msg) {
     try  {
         session.getBasicRemote().sendText(msg);
     catch  (IOException ioe) {
         CloseReason cr =
                 new  CloseReason(CloseCodes.CLOSED_ABNORMALLY, ioe.getMessage());
         try  {
             session.close(cr);
         catch  (IOException ioe2) {
             // Ignore
         }
     }
}

实时更新位置信息

websocket.snake.SnakeTimer.tick()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected  static  void  tick() {
     StringBuilder sb =  new  StringBuilder();
     for  (Iterator<Snake> iterator = SnakeTimer.getSnakes().iterator();
             iterator.hasNext();) {
         Snake snake = iterator.next();
         snake.update(SnakeTimer.getSnakes());
         sb.append(snake.getLocationsJson());
         if  (iterator.hasNext()) {
             sb.append( ',' );
         }
     }
     broadcast(String.format( "{'type': 'update', 'data' : [%s]}" ,
             sb.toString()));
}

按方向计算贪食蛇头下一个的位置

websocket.snake.Location. getAdjacentLocation(Direction direction)

没有方向,不变化位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public  Location getAdjacentLocation(Direction direction) {
     switch  (direction) {
         case  NORTH:
             return  new  Location(x, y - SnakeAnnotation.GRID_SIZE);
         case  SOUTH:
             return  new  Location(x, y + SnakeAnnotation.GRID_SIZE);
         case  EAST:
             return  new  Location(x + SnakeAnnotation.GRID_SIZE, y);
         case  WEST:
             return  new  Location(x - SnakeAnnotation.GRID_SIZE, y);
         case  NONE:
             // fall through
         default :
             return  this ;
     }
}


websocket.snake.Snake. update(Collection<Snake> snakes)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public  synchronized  void  update(Collection<Snake> snakes) {
     Location nextLocation = head.getAdjacentLocation(direction);
     if  (nextLocation.x >= SnakeAnnotation.PLAYFIELD_WIDTH) {
         nextLocation.x =  0 ;
     }
     if  (nextLocation.y >= SnakeAnnotation.PLAYFIELD_HEIGHT) {
         nextLocation.y =  0 ;
     }
     if  (nextLocation.x <  0 ) {
         nextLocation.x = SnakeAnnotation.PLAYFIELD_WIDTH;
     }
     if  (nextLocation.y <  0 ) {
         nextLocation.y = SnakeAnnotation.PLAYFIELD_HEIGHT;
     }
     if  (direction != Direction.NONE) {
         tail.addFirst(head);
         if  (tail.size() > length) {
             tail.removeLast(); //这一步很关键,实现动态位置变化,否则蛇就无限增长了
         }
         head = nextLocation;
     }
        //处理蛇是否发生碰撞 
     handleCollisions(snakes);
}

判断是否发生碰撞

判断和其他,是否发生重叠。是否迎头碰撞,还是头尾碰撞。

1
2
3
4
5
6
7
8
9
10
11
12
private  void  handleCollisions(Collection<Snake> snakes) {
     for  (Snake snake : snakes) {
         boolean  headCollision = id != snake.id && snake.getHead().equals(head);
         boolean  tailCollision = snake.getTail().contains(head);
         if  (headCollision || tailCollision) {
             kill(); //牺牲自己,触发dead类型信息
             if  (id != snake.id) {
                 snake.reward(); //成全别人,让别人长度增加1.触发kill类型信息
             }
         }
     }
}

主要业务逻辑就分析完毕了。有对canvas感兴趣的,可以关注前端js.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
var  Game = {};
 
Game.fps = 30;
Game.socket =  null ;
Game.nextFrame =  null ;
Game.interval =  null ;
Game.direction =  'none' ;
Game.gridSize = 10;
 
function  Snake() {
     this .snakeBody = [];
     this .color =  null ;
}
 
Snake.prototype.draw =  function (context) {
     for  ( var  id  in  this .snakeBody) {
         context.fillStyle =  this .color;
         context.fillRect( this .snakeBody[id].x,  this .snakeBody[id].y, Game.gridSize, Game.gridSize);
     }
};
 
Game.initialize =  function () {
     this .entities = [];
     canvas = document.getElementById( 'playground' );
     if  (!canvas.getContext) {
         Console.log( 'Error: 2d canvas not supported by this browser.' );
         return ;
     }
     this .context = canvas.getContext( '2d' );
     window.addEventListener( 'keydown' function  (e) {
         var  code = e.keyCode;
         if  (code > 36 && code < 41) {
             switch  (code) {
                 case  37:
                     if  (Game.direction !=  'east' ) Game.setDirection( 'west' );
                     break ;
                 case  38:
                     if  (Game.direction !=  'south' ) Game.setDirection( 'north' );
                     break ;
                 case  39:
                     if  (Game.direction !=  'west' ) Game.setDirection( 'east' );
                     break ;
                 case  40:
                     if  (Game.direction !=  'north' ) Game.setDirection( 'south' );
                     break ;
             }
         }
     },  false );
     if  (window.location.protocol ==  'http:' ) {
         Game.connect( 'ws://'  + window.location.host +  '/wsexample/websocket/snake' );
     else  {
         Game.connect( 'wss://'  + window.location.host +  '/wsexample/websocket/snake' );
     }
};
 
Game.setDirection  =  function (direction) {
     Game.direction = direction;
     Game.socket.send(direction);
     Console.log( 'Sent: Direction '  + direction);
};
 
Game.startGameLoop =  function () {
     if  (window.webkitRequestAnimationFrame) {
         Game.nextFrame =  function  () {
             webkitRequestAnimationFrame(Game.run);
         };
     else  if  (window.mozRequestAnimationFrame) {
         Game.nextFrame =  function  () {
             mozRequestAnimationFrame(Game.run);
         };
     else  {
         Game.interval = setInterval(Game.run, 1000 / Game.fps);
     }
     if  (Game.nextFrame !=  null ) {
         Game.nextFrame();
     }
};
 
Game.stopGameLoop =  function  () {
     Game.nextFrame =  null ;
     if  (Game.interval !=  null ) {
         clearInterval(Game.interval);
     }
};
 
Game.draw =  function () {
     this .context.clearRect(0, 0, 640, 480);
     for  ( var  id  in  this .entities) {
         this .entities[id].draw( this .context);
     }
};
 
Game.addSnake =  function (id, color) {
     Game.entities[id] =  new  Snake();
     Game.entities[id].color = color;
};
 
Game.updateSnake =  function (id, snakeBody) {
     if  ( typeof  Game.entities[id] !=  "undefined" ) {
         Game.entities[id].snakeBody = snakeBody;
     }
};
 
Game.removeSnake =  function (id) {
     Game.entities[id] =  null ;
     // Force GC.
     delete  Game.entities[id];
};
 
Game.run = ( function () {
     var  skipTicks = 1000 / Game.fps, nextGameTick = ( new  Date).getTime();
 
     return  function () {
         while  (( new  Date).getTime() > nextGameTick) {
             nextGameTick += skipTicks;
         }
         Game.draw();
         if  (Game.nextFrame !=  null ) {
             Game.nextFrame();
         }
     };
})();
 
Game.connect = ( function (host) {
     if  ( 'WebSocket'  in  window) {
         Game.socket =  new  WebSocket(host);
     else  if  ( 'MozWebSocket'  in  window) {
         Game.socket =  new  MozWebSocket(host);
     else  {
         Console.log( 'Error: WebSocket is not supported by this browser.' );
         return ;
     }
 
     Game.socket.onopen =  function  () {
         // Socket open.. start the game loop.
         Console.log( 'Info: WebSocket connection opened.' );
         Console.log( 'Info: Press an arrow key to begin.' );
         Game.startGameLoop();
         setInterval( function () {
             // Prevent server read timeout.
             Game.socket.send( 'ping' );
         }, 5000);
     };
 
     Game.socket.onclose =  function  () {
         Console.log( 'Info: WebSocket closed.' );
         Game.stopGameLoop();
     };
 
     Game.socket.onmessage =  function  (message) {
         // _Potential_ security hole, consider using json lib to parse data in production.
         var  packet = eval( '('  + message.data +  ')' );
         switch  (packet.type) {
             case  'update' :
                 for  ( var  i = 0; i < packet.data.length; i++) {
                     Game.updateSnake(packet.data[i].id, packet.data[i].body);
                 }
                 break ;
             case  'join' :
                 for  ( var  j = 0; j < packet.data.length; j++) {
                     Game.addSnake(packet.data[j].id, packet.data[j].color);
                 }
                 break ;
             case  'leave' :
                 Game.removeSnake(packet.id);
                 break ;
             case  'dead' :
                 Console.log( 'Info: Your snake is dead, bad luck!' );
                 Game.direction =  'none' ;
                 break ;
             case  'kill' :
                 Console.log( 'Info: Head shot!' );
                 break ;
         }
     };
});
 
var  Console = {};
 
Console.log = ( function (message) {
     var  console = document.getElementById( 'console' );
     var  p = document.createElement( 'p' );
     p.style.wordWrap =  'break-word' ;
     p.innerHTML = message;
     console.appendChild(p);
     while  (console.childNodes.length > 25) {
         console.removeChild(console.firstChild);
     }
     console.scrollTop = console.scrollHeight;
});
 
Game.initialize();
 
 
document.addEventListener( "DOMContentLoaded" function () {
     // Remove elements with "noscript" class - <noscript> is not allowed in XHTML
     var  noscripts = document.getElementsByClassName( "noscript" );
     for  ( var  i = 0; i < noscripts.length; i++) {
         noscripts[i].parentNode.removeChild(noscripts[i]);
     }
},  false );


结论

通过阅读一些官方文档的代码,学习人家的编码风格,细节。比如线程安全方面。js的面向对象编写,很优雅。不像笔者遇到的经常看到的一个方法,一个方法式的嵌套调用,不考虑性能,就阅读起来就特别费劲。



本文转自 randy_shandong 51CTO博客,原文链接:http://blog.51cto.com/dba10g/1856234,如需转载请自行联系原作者

相关文章
|
3月前
|
XML 前端开发 Java
SpringMVC入门到实战------2、SpringMVC创建实例Hello SpringMVC(maven+tomcat)
这篇文章是SpringMVC框架的入门教程,详细指导了如何在IDEA中使用Maven和Tomcat创建SpringMVC工程,包括添加依赖、配置web.xml、编写控制器、创建配置文件、配置Tomcat服务器以及进行基本的测试,展示了一个简单的Hello SpringMVC示例。
SpringMVC入门到实战------2、SpringMVC创建实例Hello SpringMVC(maven+tomcat)
|
1月前
|
前端开发 JavaScript Python
Python Web应用中的WebSocket实战:前后端分离时代的实时数据交换
在前后端分离的Web应用开发模式中,如何实现前后端之间的实时数据交换成为了一个重要议题。传统的轮询或长轮询方式在实时性、资源消耗和服务器压力方面存在明显不足,而WebSocket技术的出现则为这一问题提供了优雅的解决方案。本文将通过实战案例,详细介绍如何在Python Web应用中运用WebSocket技术,实现前后端之间的实时数据交换。
79 0
|
2月前
|
监控 Java 应用服务中间件
部署tomcat部署实战案例
本文是关于Tomcat部署实战案例的教程,包括通过yum和二进制方式部署Tomcat的详细步骤,以及如何监控Tomcat服务。
210 84
部署tomcat部署实战案例
|
4月前
|
监控 前端开发 API
实战指南:使用Python Flask与WebSocket实现高效的前后端分离实时系统
【7月更文挑战第18天】构建实时Web应用,如聊天室,可借助Python的Flask和WebSocket。安装Flask及Flask-SocketIO库,创建Flask应用,处理WebSocket事件。前端模板通过Socket.IO库连接服务器,发送和接收消息。运行应用,实现实时通信。此示例展现了Flask结合WebSocket实现前后端实时交互的能力。
507 3
|
1月前
|
网络协议 Java 应用服务中间件
Tomcat中的WebSocket是如何实现的?
【10月更文挑战第7天】本文介绍了WebSocket在Tomcat中的实现,包括其全双工通信、单个TCP连接、协议升级和事件驱动的特点。通过Spring Boot项目整合WebSocket,展示了如何配置依赖、创建WebSocket处理类和配置类。详细解析了WebSocket的原理,包括ServerEndpointExporter的注册过程和请求处理流程。总结了WebSocket与HTTP请求处理的区别,并提供了进一步学习的资源。
Tomcat中的WebSocket是如何实现的?
|
5月前
|
Web App开发 移动开发 Java
基于tomcat运行HTML5 WebSocket echo例子
基于tomcat运行HTML5 WebSocket echo例子
47 2
|
5月前
|
JavaScript 网络协议 前端开发
【Nodejs】WebSocket 全面解析+实战演练——(Nodejs实现简易聊天室)
【Nodejs】WebSocket 全面解析+实战演练——(Nodejs实现简易聊天室)
591 0
|
3月前
|
缓存 Java 应用服务中间件
SpringMVC入门到实战------七、SpringMVC创建JSP页面的详细过程+配置模板+实现页面跳转+配置Tomcat。JSP和HTML配置模板的差异对比(二)
这篇文章详细介绍了在SpringMVC中创建JSP页面的全过程,包括项目的创建、配置、Tomcat的设置,以及如何实现页面跳转和配置模板解析器,最后还对比了JSP和HTML模板解析的差异。
SpringMVC入门到实战------七、SpringMVC创建JSP页面的详细过程+配置模板+实现页面跳转+配置Tomcat。JSP和HTML配置模板的差异对比(二)
|
4月前
|
前端开发 JavaScript UED
Python Web应用中的WebSocket实战:前后端分离时代的实时数据交换
【7月更文挑战第16天】在前后端分离的Web开发中,WebSocket解决了实时数据交换的问题。使用Python的Flask和Flask-SocketIO库,后端创建WebSocket服务,监听并广播消息。前端HTML通过JavaScript连接到服务器,发送并显示接收到的消息。WebSocket适用于实时通知、在线游戏等场景,提升应用的实时性和用户体验。通过实战案例,展示了如何实现这一功能。
315 2
|
4月前
|
网络协议 UED 开发者