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,如需转载请自行联系原作者

相关文章
|
7月前
|
Java 应用服务中间件 Apache
企业实战(2) 项目环境搭建之Tomcat部署
企业实战(2) 项目环境搭建之Tomcat部署
|
23小时前
|
运维 Java 应用服务中间件
Tomcat详解(七)——Tomcat使用https配置实战
Tomcat详解(七)——Tomcat使用https配置实战
42 4
|
23小时前
|
前端开发 网络协议 Java
WebSocket理论和实战
WebSocket详解与实战操作
|
23小时前
|
传感器 监控 网络协议
WebSocket 实战:构建高效的实时应用
WebSocket 实战:构建高效的实时应用
WebSocket 实战:构建高效的实时应用
|
10月前
|
存储 数据安全/隐私保护
Netty实战(十三)WebSocket协议(一)
WebSocket 协议是完全重新设计的协议,旨在为 Web 上的双向数据传输问题提供一个切实可行的解决方案,使得客户端和服务器之间可以在任意时刻传输消息,因此,这也就要求它们异步地处理消息回执。
171 0
|
7月前
|
移动开发 前端开发 网络协议
WebSocket从入门到实战
WebSocket从入门到实战
182 0
|
9月前
|
域名解析 运维 负载均衡
【运维知识进阶篇】Tomcat集群实战之部署zrlog博客(Tomcat服务安装+静态资源挂载NFS+Nginx负载均衡+HTTPS证书+Redis会话保持)
【运维知识进阶篇】Tomcat集群实战之部署zrlog博客(Tomcat服务安装+静态资源挂载NFS+Nginx负载均衡+HTTPS证书+Redis会话保持)
262 1
|
10月前
|
数据安全/隐私保护
Netty实战(十四)WebSocket协议(二)
我们之前说过为了将 ChannelHandler 安装到 ChannelPipeline 中,需要扩展了ChannelInitializer,并实现 initChannel()方法
114 0
|
10月前
|
Java 应用服务中间件 Docker
Docker从入门到精通——实战Dockerfile构建Tomcat镜像
Docker从入门到精通——实战Dockerfile构建Tomcat镜像
158 0
|
12月前
|
Java 应用服务中间件 Docker
Docker Review - dockerfile 实战_使用dockerfile制作tomcat镜像
Docker Review - dockerfile 实战_使用dockerfile制作tomcat镜像
101 0