设为首页 收藏本站
查看: 524|回复: 0

[经验分享] websocket实战(4) websocket版贪食蛇游戏(tomcat官方自带)

[复制链接]

尚未签到

发表于 2018-12-2 07:48:15 | 显示全部楼层 |阅读模式
  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 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 < 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 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.id, packet.data.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 -  is not allowed in XHTML
    var noscripts = document.getElementsByClassName("noscript");
    for (var i = 0; i < noscripts.length; i++) {
        noscripts.parentNode.removeChild(noscripts);
    }
}, false);  

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





运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-642161-1-1.html 上篇帖子: Metasploit溢出Tomcat管理台默认口令漏洞 下篇帖子: 解除Tomcat后台管理上传部署war包大小限制
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表