中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

websocket實戰(4) websocket版貪食蛇游戲(tomcat官方自帶)

發布時間:2020-07-23 03:15:49 來源:網絡 閱讀:4993 作者:randy_shandong 欄目:開發技術

websocket實戰(1) 入門

websocket實戰(2) 信息處理發送、接收和編碼

websocket實戰(3) 錯誤處理及配置管理

通過前面3篇的闡述,相信可以構建一個簡單的socket應用了。當然,也會遺漏了許多知識點,相信會在以后分享的實例中捎帶說明下。

本文的主要是分析下tomcat官方自帶的貪食蛇游戲。為什么選擇分析這個項目呢。

  1. 貪食蛇游戲規則,人人明白,業務方面不需要過多解釋(當然這款websocket版的游戲規則也有一定特色)。

  2. 游戲設計簡單,一個對象足以完成游戲,但不涉及到一些復雜的邏輯算法。

  3. 通過游戲,有很好的代入感

1.游戲規則介紹

1.能夠實現貪吃蛇自動向前移動,一旦貪食蛇選擇了方向,貪食蛇就按所選方向開始運動,可以任意。移動方向為貪吃蛇當前行走方向。

2.游戲通過鍵盤的上下左右四個方向控制貪吃蛇當前行走方向。(沒有可以吃的食物)。

3.支持對戰功能,如果發生碰撞情況,后蛇會自殺,重置信息,重新來玩。

4.如果移動出畫布外,從對立方向進入,移動方向不變。

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

websocket實戰(4) websocket版貪食蛇游戲(tomcat官方自帶)

2.貪食蛇設計

貪食蛇狀態快照

websocket實戰(4) websocket版貪食蛇游戲(tomcat官方自帶)

貪食蛇類圖

websocket實戰(4) websocket版貪食蛇游戲(tomcat官方自帶)

貪食蛇:有幾個重要屬性。顏色,頭(head),身體(tail),行動方向。

顏色:隨機生成。

頭&身體:決定蛇的長度,在畫布中的位置。還有決定是否發生碰撞。有(x,y)坐標說明。

行動方向:東西南北四個方向。

重點說一下和websocket相關的信息。貪食蛇的session屬性。

session主要負責貪食蛇狀態信息的傳播,將自己的顏色和位置信息傳遞到前端。

傳播時機

  1. 狀態變化要傳播(kill,join,..)

  2. 位置變化要傳播(包括方向,其實也是狀態變化)

  3. 重置要傳播(也是狀態變化)

websocket實戰(4) websocket版貪食蛇游戲(tomcat官方自帶)

分析序列圖得知,其實作為游戲的websocket的EndPoint,做的事情很簡單。兩件事

  1. 有新需求:創建貪食蛇,發送渲染命令(join)

  2. 響應客戶端的命令(方向命令)

不難分析,游戲貪食蛇的移動,是應該有定時器驅動的,所有貪食蛇位置的變化,都是通過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的面向對象編寫,很優雅。不像筆者遇到的經常看到的一個方法,一個方法式的嵌套調用,不考慮性能,就閱讀起來就特別費勁。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

美姑县| 平武县| 慈溪市| 东乡县| 山东省| 依安县| 永仁县| 湘阴县| 平山县| 合川市| 麦盖提县| 连平县| 花莲县| 贵港市| 改则县| 泗水县| 桃江县| 寿阳县| 卢氏县| 凤翔县| 萨迦县| 沙湾县| 达尔| 加查县| 巫溪县| 上高县| 林甸县| 务川| 同心县| 金沙县| 象州县| 铁力市| 个旧市| 淮南市| 沧源| 上饶市| 宾川县| 邵东县| 泽普县| 龙井市| 海口市|