通过前面3篇的阐述,相信可以构建一个简单的socket应用了。当然,也会遗漏了许多知识点,相信会在以后分享的实例中捎带说明下。
本文的主要是分析下tomcat官方自带的贪食蛇游戏。为什么选择分析这个项目呢。
贪食蛇游戏规则,人人明白,业务方面不需要过多解释(当然这款websocket版的游戏规则也有一定特色)。
游戏设计简单,一个对象足以完成游戏,但不涉及到一些复杂的逻辑算法。
通过游戏,有很好的代入感
1.游戏规则介绍
1.能够实现贪吃蛇自动向前移动,一旦贪食蛇选择了方向,贪食蛇就按所选方向开始运动,可以任意。移动方向为贪吃蛇当前行走方向。
2.游戏通过键盘的上下左右四个方向控制贪吃蛇当前行走方向。(没有可以吃的食物)。
3.支持对战功能,如果发生碰撞情况,后蛇会自杀,重置信息,重新来玩。
4.如果移动出画布外,从对立方向进入,移动方向不变。
界面是"群蛇乱舞”界面。
2.贪食蛇设计
贪食蛇状态快照
贪食蛇类图
贪食蛇:有几个重要属性。颜色,头(head),身体(tail),行动方向。
颜色:随机生成。
头&身体:决定蛇的长度,在画布中的位置。还有决定是否发生碰撞。有(x,y)坐标说明。
行动方向:东西南北四个方向。
重点说一下和websocket相关的信息。贪食蛇的session属性。
session主要负责贪食蛇状态信息的传播,将自己的颜色和位置信息传递到前端。
传播时机
状态变化要传播(kill,join,..)
位置变化要传播(包括方向,其实也是状态变化)
重置要传播(也是状态变化)
分析序列图得知,其实作为游戏的websocket的EndPoint,做的事情很简单。两件事
有新需求:创建贪食蛇,发送渲染命令(join)
响应客户端的命令(方向命令)
不难分析,游戏贪食蛇的移动,是应该有定时器驱动的,所有贪食蛇位置的变化,都是通过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,如需转载请自行联系原作者