您好,登錄后才能下訂單哦!
websocket實戰(1) 入門
websocket實戰(2) 信息處理發送、接收和編碼
websocket實戰(3) 錯誤處理及配置管理
通過前面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,沒有更新,貪食蛇信息的更新不屬于聚合信息范疇)
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相關的方法。
//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<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)
沒有方向,不變化位置。
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)
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); }
判斷是否發生碰撞
判斷和其他,是否發生重疊。是否迎頭碰撞,還是頭尾碰撞。
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.
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的面向對象編寫,很優雅。不像筆者遇到的經常看到的一個方法,一個方法式的嵌套調用,不考慮性能,就閱讀起來就特別費勁。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。