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

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

本文的主要是分析下tomcat官方自带的贪食蛇游戏。为什么选择分析这个项目呢。贪食蛇游戏规则,人人明白,业务方面不需要过多解释(当然这款websocket版的游戏规则也有一定特色)。

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

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

1.游戏规则介绍

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

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

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

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

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

d8c77cd28c130bf75c4549acfaa239a1.png

2.贪食蛇设计

贪食蛇状态快照

86f47cf6852abe699d4a8c059dcfae9f.png

贪食蛇类图

820a27fa7e6b57c6db0c26211187ccf0.png

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

颜色:随机生成。

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

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

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

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

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

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

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

85524f339b2f0a90271530199fda369e.png

分析序列图得知,其实作为游戏的websocket的EndPoint,做的事情很简单。两件事有新需求:创建贪食蛇,发送渲染命令(join)

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

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

1.贪食蛇聚合信息维护(CRD,没有更新,贪食蛇信息的更新不属于聚合信息范畴)protected static synchronized void addSnake(Snake snake) {

if (snakes.size() == 0) {

startTimer();

}

snakes.put(Integer.valueOf(snake.getId()), snake);

}

protected static Collection 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相关的方法。//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()protected static void tick() {

StringBuilder sb = new StringBuilder();

for (Iterator 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)

没有方向,不变化位置。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 snakes)public synchronized void update(Collection 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 

nextLocation.x = SnakeAnnotation.PLAYFIELD_WIDTH;

}

if (nextLocation.y 

nextLocation.y = SnakeAnnotation.PLAYFIELD_HEIGHT;

}

if (direction != Direction.NONE) {

tail.addFirst(head);

if (tail.size() > length) {

tail.removeLast();//这一步很关键,实现动态位置变化,否则蛇就无限增长了

}

head = nextLocation;

}

//处理蛇是否发生碰撞

handleCollisions(snakes);

}

判断是否发生碰撞

判断和其他,是否发生重叠。是否迎头碰撞,还是头尾碰撞。private void handleCollisions(Collection 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.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 

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 

Game.updateSnake(packet.data[i].id, packet.data[i].body);

}

break;

case 'join':

for (var j = 0; 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 - 

 is not allowed in XHTML

var noscripts = document.getElementsByClassName("noscript");

for (var i = 0; i 

noscripts[i].parentNode.removeChild(noscripts[i]);

}

}, false);

结论

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

为什么需要websocket? 传统的实时交互的游戏,或服务器主动发送消息的行为(如推送服务),如果想做在微信上,可能你会使用轮询的方式进行,不过这太消耗资源,大量的请求也加重了服务器的负担,而且延迟问题比较严重。如果是自己开发的app,为了解决这些问题,很多团队会自建socket,使用tcp长链接、自定协议的方式与服务器进行相对实时的数据交互。有能力的团队,采用这种方式自然没什么大问题。不过小团队可能就要花费很多时间去调试,要解决很多难题,这个在成本上就划不来。 H5引入了webSocket来解决网页端的长链接问题,而微信小程序也支持websocket。这是一个非常重要的特性,所以本系列的文章会专门拿出一篇来讨论websocketwebSocket本质上也是TCP连接,它提供全双工的数据传输。一方面可以避免轮询带来的连接频繁建立与断开的性能损耗,另一方面数据可以是比较实时的进行双向传输(因为是长链接),而且WebSocket允许跨域通信(这里有个潜在的跨域安全的问题,得靠服务端来解决)。目前除IE外的浏览器已经对webSocket支持得很好了,微信小程序再推一把之后,它会变得更加流行。 我们来设计一个新的demo,一个比较有趣的小游戏,多人扫雷,准确地讲,多人挖黄金。 游戏规则是这样的:把雷换成金子,挖到金子加一分,每人轮流一次(A挖完轮到B,B挖完A才能再点击),点中金子就算你的,也不会炸,游戏继续,直到把场上所有的金子都挖完游戏才结束。跟扫雷一样,数字也是表示周边有几个金子,然后用户根据场上已经翻出来的数字来猜哪一格可能有金子。 这种交互的游戏难点在于,用户的点击操作都要传到服务器上,而且服务器要实时的推送到其它玩家的应用上。另外用户自己也要接收对方操作时实时传过来的数据,这样才不至于重复点中同一个格子。简单讲,就是你要上报操作给服务器,而服务器也要实时给你推消息。为了简化整个模型,我们规定玩家必须轮流来点击,玩家A点完后,才能轮到玩家B,玩家B操作完,玩家A才能点。 我们分几步来实现这个功能。 一、实现思路 1、第一步,我们要先生成扫雷的地图场景 这个算法比较简单,简述一下。随机取某行某列就可以定位一个格子,标记成金子(-1表示金子)。mimeCnt表示要生成的金子的数量,用同样的方式循环标记mimeCnt个随机格子。生成完后,再用一个循环去扫描这些-1的格子,把它周边的格子都加1,当然必须是非金子的格子才加1。代码放在这里。 其中increaseArround用来把这格金子周边的格子都加1,实现也比较简单: 执行genMimeArr(),随机生成结果如下: -1表示金子。看了下貌似没什么问题。接下去,我们就要接入webSocket了。 (这个是js本的,其实生成地图场景的工作是在后台生成,这个js本只是一个演示,不过算法是一样的。) 2、我们需要一个支持webSocket的服务端 本例子中,我们使用python的tornado框架来实现(tornado提供了tornado.websocket模块)。当然读者也可以使用socket.io,专为webSocket设计的js语言的服务端,用起来非常简单,它也对不支持webSocket的浏览器提供了兼容(flash或comet实现)。 笔者本人比较喜欢使用tornado,做了几年后台开发,使用最多的框架之一的就是它,NIO模型,而且非常轻量级,同样的rps,java可能需要700-800M的内存,tornado只要30-40M,所以在一台4G内存的机子上可以跑上百个tornado服务,而java,对不起,只能跑3个虚拟机。微服务的时代,这一点对小公司很重要。当然如果读者本人对java比较熟悉的话,也可以选择netty框架尝试一下。 webSocket用tornado的另一个好处是,它可以在同一个服务(端口)上同时支持webSocket及http两种协议。tornado的官方demo代码中展示了怎么实现同时使用两种协议。在本游戏中,可以这么用:用户进入首页,用http协议去拉取当前的房间号及数据。因为首页是打开最多的,进了首页的用户不一定会玩游戏。所以首页还没必要建立webSocket链接,webSocket链接主要用来解决频繁请求及推送的操作。首页只有一个请求操作。选了房间号后,进去下一个游戏页面再开始建立webSocket链接。 3、客户端 使用微信小程序开发工具,直接连接是会报域名安全错误的,因为工具内部做了限制,对安全域名才会允许连接。所以同样的,这里我们也继续改下工具的源码,把相关的行改掉就行修改方式如下: 找到asdebug.js的这一行,把它改成: if(false)即可。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值