7.) MMO From Scratch - Other Players

Results 1 to 1 of 1
  1. #1
    :-) s-p-n is offline
    DeveloperRank
    Jun 2007 Join Date
    Next DoorLocation
    2,146Posts

    7.) MMO From Scratch - Other Players


    RaGEZONE Recommends

    RaGEZONE Recommends

    Previous (Players 2)
    Table of Contents (Introduction)
    Next (Slire Herb)

    Git the files here

    So far we have an HTML5 thing where a character can walk around, and the server saves the character's position in the server. To a player's perspective, this isn't interesting at all yet. In all fairness, this tutorial barely makes this project a little bit more interesting.. Barely...

    In any case, it's the first step to a multiplayer game. We must have multiple players who can actually see each other moving around the screen. So let's do that now.

    In /private/channels/others.js we listen for that 'player-update' event we set up in tutorial 5.

    All we do is make sure the player in this update is not the player in the actual session, and emit that event. We don't simply want to broadcast this (which would emit the event to all other clients, because we want to verify that the user is actually logged in and playing the game (state 4). Recall that a client may be in any 1 of 4 states.

    Anyway, here's the code:
    Code:
    module.exports = function (m, session) {
        var socket = session.socket;
        m.event.on('player-update', function (player) {
            if (session.user && session.user.username !== player.username) {
                //console.log('player updated:', player);
                socket.emit('others-update', {username: player.username, game: player.game});
            }
        });
    }
    Pretty simple. That's all the server needs to do for other players at this point. Now let's move onto the client.

    We know we're going to need a dictionary of all the other players, the moveTime is 100 just like with the client's player, and we can default the lastMove (the time of the last move) to 0. Also, we should make a tweenTime and set it to 0.9 like we did for the single player.

    /public/js/objects/others.js
    Code:
    function Others(main) {
        "use strict";
        var self = this;
        var others = Object.create(null);
        var moveTime = 100;
        var tweenTime = 0.9;
        var lastMove = 0;    self.preload = function () {};
        self.create = function () {};
        self.update = function () {};
        self.render = function () {};
    }
    We don't need to preload the sprite, because we already did that in /public/js/objects/player.js. We don't really need to create anything- as the client doesn't know of other players until after it is already in state 4- which is long after the create code has been run. So all we need to worry about is the update tick.

    Let's create some helper functions.

    I decided it was necessary to have a helper that would cycle through each player, check if that player is ready, and if so execute a callback that accepts a player as the argument. Since there's not a simple way to get the username, we can pass that as a second, optionally accepted argument.
    Code:
        function eachPlayer(fn) {
            var username, player;
            for (username in others) {
                if (!others[username].ready) {
                    continue;
                }
                fn(others[username], username);
            }
        }
    The second helper function is for actually moving the other player. In the last tutorial we determined direction of movement by listening for keyboard events. The keyboard dictated which direction our code moved the player. This time we're given the destination position, and we're forced to reverse engineer the direction of movement based on the current position and the destination. It's actually easily done with a few comparisons. Before we start moving the sprite, we should assure the sprite is at the same position of the game data. It should be at worst, very close- so we'll just use a quick, jerky snap to fix that (a simple assignment- no tween). After we figure out the direction, we start animating the sprite, and set the stillFrame to the correct direction so that when the sprite stops walking, it stands still facing the same direction. Finally, we set up a tween for the sprite and the username text.
    Code:
        function movePlayer(player, destination) {
            console.log("moving to: ", destination);
            player.sprite.x = player.game.x;
            player.sprite.y = player.game.y;
            if (destination.x < player.sprite.x) {
                if (player.sprite.scale.x < 0) {
                    player.sprite.scale.x *= -1;
                }
                player.sprite.animations.play('left');
                player.stillFrame = 3;
            } else if (destination.x > player.sprite.x) {
                if (player.sprite.scale.x > 0) {
                    player.sprite.scale.x *= -1;
                }
                player.sprite.animations.play('right');
                player.stillFrame = 3;
            } else if (destination.y < player.sprite.y) {
                player.sprite.animations.play('up');
                player.stillFrame = 6;
            } else if (destination.y > player.sprite.y) {
                player.sprite.animations.play('down');
                player.stillFrame = 0;
            }
            main.game.add.tween(player.sprite).
                to(destination, moveTime * tweenTime, 'Linear').
                start();
            main.game.add.tween(player.text).
                to({
                    x: destination.x - 2,
                    y: destination.y - 60
                }, moveTime * tweenTime, 'Linear').
                start();
        }
    Now, we'll make one last helper function for cycling through all of the other players, and moving them. For this, we're going to adopt a strategy called entity interpolation. I'll take a moment to explain what that is.

    Network Latency Correction Strategies
    Let's stop and think about this for a second. There are several strategies we could use to make it appear like things are happening real-time, when in fact they are not. I'll discuss Dead Reckoning first.
    1. Dead Reckoning is basically client-side prediction. Say we're shooting an arrow on client A. The server can tell client B and A the time the arrow was shot on the server, and the speed that arrow is moving. Let's assume for dead reckoning that players don't move- they are stationary. It's possible for both clients to make corrections to the velocity of the arrow in order for it to hit the exact same target at more/less the exact same time on each client- assuming that both of the clients may receive the arrow packet long enough before the arrow should actually hit a destination. The reason dead reckoning may work, is because the arrow moves in a more/less straight line. The arrow won't all of a sudden change direction like a heat-seeking missile (and to be fair, if that direction change is small or predictable, dead reckoning could still work). Even if we don't give the clients a destination, we can use the dead reckoning strategy to make both clients see the same game after only one or two game-state updates. One problem though- players can and do change direction immediately. So dead reckoning won't work for this application.
    2. Entity Interpolation involves showing each player a representation of every other player in the past. To do this, we need to make a queue on the client for each other player. We'll be receiving packets from the server for each other player, and every time we receive a packet we'll add the game-state to a queue for that player. During the game update, we'll take an entry out of that queue, and move that player. We'll cycle through all of the players (roughly) every 100ms. If the queue gets too large, though, we should make a quick correction. This will result in jerky movement for players with a high ping- but in a PVP game, we don't want any player to see any other player over 500ms behind. I'll even lower that to 300ms. This is a number we can adjust statically or dynamically later on- but right now- the queue length maximum is 3. It should be noted that entity interpolation results in every player having a different view of the game. They see themselves in the present, and everyone else slightly in the past.

    So. Our last helper function will cycle through each of the players, check if there is a game-state in the queue, and if so, check if that length is too large and pop from & reset the queue if it is. If the queue is not too large, we'll just take the first state out of that queue. In either case, we have a destination- so we pass that to movePlayer() and after that we set player.game to the destination to account for the tween either not finishing in time or giving us some whacky floating point for an end-result. If there is nothing in the queue, we should stop the animations and assign the sprite's frame to the still frame.
    Code:
        function cyclePlayerQueues() {
            var destination;
            eachPlayer(function (player) {
                if (player.queue.length > 0) {
                    console.log('queue size:', player.queue.length);
                    if (player.queue.length > maxQueueLength) {
                        destination = player.queue.pop();
                        player.queue = [];
                    } else {
                        destination = player.queue.shift();
                    }
                    movePlayer(player, destination);
                    player.game = destination;
                } else {
                    player.sprite.animations.stop();
                    player.sprite.frame = player.stillFrame;
                }
            });
        }
    Now that we have all of those helper functions, our game update logic should be really simple. We simply set up a condition to make the update happen only every moveTime (100ms), and then cycle through the players. Preload, create, and render simply need to be there to fulfill the interface requirements.
    Code:
        self.preload = function () {};
        self.create = function () {};
        self.update = function () {
            if (lastMove + moveTime < Date.now()) {
                lastMove = Date.now();
                cyclePlayerQueues();
            }
        };
        self.render = function () {};
    Last but most certainly not least, we need to receive data from the server. If the player we receive data on doesn't exist in our dictionary yet, we need to initialize that player. We need to set up the sprite, set up the text above their head, and give them some game-state data- like stillFrame, game, queue, and ready.

    After that, it's pretty safe to assume that the player is in the dictionary, so we'll add the game-state we received into the queue. And that's it!

    Code:
        comms.on('others-update', function (data) {
            console.log("got other update", data);
            if (!(data.username in others)) {
                others[data.username] = {};
                // Set up player sprite
                others[data.username].sprite = main.game.add.sprite(
                    data.game.x, 
                    data.game.y, 
                    'player'
                );
                others[data.username].sprite.anchor.setTo(0.5, 0.9);
                others[data.username].sprite.animations.add(
                    'down', 
                    [0, 1, 0, 2], 
                    10, 
                    true
                );
                others[data.username].sprite.animations.add(
                    'left', 
                    [3, 4, 3, 5], 
                    10, 
                    true
                );
                others[data.username].sprite.animations.add(
                    'right', 
                    [3, 4, 3, 5], 
                    10, 
                    true
                );
                others[data.username].sprite.animations.add(
                    'up', 
                    [6, 7, 6, 8], 
                    10, 
                    true
                );
    
                // Set up text above player
                others[data.username].text = main.game.add.text(
                    data.game.x - 2, 
                    data.game.y - 60, 
                    data.username
                );
                others[data.username].text.anchor.setTo(0.5);
                others[data.username].text.align = 'center';
                others[data.username].text.font = 'Arial Black';
                others[data.username].text.fontSize = 16;
                others[data.username].text.stroke = '#000000';
                others[data.username].text.strokeThickness = 3;
                others[data.username].text.fill = '#FFFFFF';
    
                // Set up miscellaneous data for player.
                others[data.username].stillFrame = 0;
                others[data.username].game = data.game;
                others[data.username].queue = [];
                others[data.username].ready = true;
            }
            others[data.username].queue.push(data.game);
        });
    Almost forgot, here's the entire code for /public/js/objects/others.js
    Code:
    function Others(main) {
        "use strict";
        var self = this;
        var others = Object.create(null);
        var moveTime = 100;
        var tweenTime = 0.9;
        var lastMove = 0;
        var maxQueueLength = 3;
        function eachPlayer(fn) {
            var username, player;
            for (username in others) {
                if (!others[username].ready) {
                    continue;
                }
                fn(others[username], username);
            }
        }
        function movePlayer(player, destination) {
            console.log("moving to: ", destination);
            player.sprite.x = player.game.x;
            player.sprite.y = player.game.y;
            if (destination.x < player.sprite.x) {
                if (player.sprite.scale.x < 0) {
                    player.sprite.scale.x *= -1;
                }
                player.sprite.animations.play('left');
                player.stillFrame = 3;
            } else if (destination.x > player.sprite.x) {
                if (player.sprite.scale.x > 0) {
                    player.sprite.scale.x *= -1;
                }
                player.sprite.animations.play('right');
                player.stillFrame = 3;
            } else if (destination.y < player.sprite.y) {
                player.sprite.animations.play('up');
                player.stillFrame = 6;
            } else if (destination.y > player.sprite.y) {
                player.sprite.animations.play('down');
                player.stillFrame = 0;
            }
            main.game.add.tween(player.sprite).
                to(destination, moveTime * tweenTime, 'Linear').
                start();
            main.game.add.tween(player.text).
                to({
                    x: destination.x - 2,
                    y: destination.y - 60
                }, moveTime * tweenTime, 'Linear').
                start();
        }
        function cyclePlayerQueues() {
            var destination;
            eachPlayer(function (player) {
                if (player.queue.length > 0) {
                    console.log('queue size:', player.queue.length);
                    if (player.queue.length > maxQueueLength) {
                        destination = player.queue.pop();
                        player.queue = [];
                    } else {
                        destination = player.queue.shift();
                    }
                    movePlayer(player, destination);
                    player.game = destination;
                } else {
                    player.sprite.animations.stop();
                    player.sprite.frame = player.stillFrame;
                }
            });
        }
        self.preload = function () {};
        self.create = function () {};
        self.update = function () {
            if (lastMove + moveTime < Date.now()) {
                lastMove = Date.now();
                cyclePlayerQueues();
            }
        };
        self.render = function () {};
        comms.on('others-update', function (data) {
            console.log("got other update", data);
            if (!(data.username in others)) {
                others[data.username] = {};
                // Set up player sprite
                others[data.username].sprite = main.game.add.sprite(
                    data.game.x, 
                    data.game.y, 
                    'player'
                );
                others[data.username].sprite.anchor.setTo(0.5, 0.9);
                others[data.username].sprite.animations.add(
                    'down', 
                    [0, 1, 0, 2], 
                    10, 
                    true
                );
                others[data.username].sprite.animations.add(
                    'left', 
                    [3, 4, 3, 5], 
                    10, 
                    true
                );
                others[data.username].sprite.animations.add(
                    'right', 
                    [3, 4, 3, 5], 
                    10, 
                    true
                );
                others[data.username].sprite.animations.add(
                    'up', 
                    [6, 7, 6, 8], 
                    10, 
                    true
                );
    
                // Set up text above player
                others[data.username].text = main.game.add.text(
                    data.game.x - 2, 
                    data.game.y - 60, 
                    data.username
                );
                others[data.username].text.anchor.setTo(0.5);
                others[data.username].text.align = 'center';
                others[data.username].text.font = 'Arial Black';
                others[data.username].text.fontSize = 16;
                others[data.username].text.stroke = '#000000';
                others[data.username].text.strokeThickness = 3;
                others[data.username].text.fill = '#FFFFFF';
    
                // Set up miscellaneous data for player.
                others[data.username].stillFrame = 0;
                others[data.username].game = data.game;
                others[data.username].queue = [];
                others[data.username].ready = true;
            }
            others[data.username].queue.push(data.game);
        });
    }
    Now players can see each other walking around the game. Whew. It took us a while to get to this point, but this is a big step and sets the direction we're heading towards. Is that the right direction? I'm not sure, but now we're committed. We can fudge some things in the future, but from here-on-out I reserve the right to make bad decisions before I know they are bad decisions. For example, using keyboard events instead of mouse clicks for movement. This strategy sucks for TCP- and we have to use TCP for this game because that's the only choice web sockets give us! I chose keyboard events for movement because this is a PVP game, and I think using a controller/keyboard requires much more skill in combat on the human side of things, than point-&-click does. It may not be the easiest to work out technically, but I'm not making a game for computers- I'm making the game for humans. As a hacker, I'll do my best to make the computer do what I want it to do, whether the computer wants to or not.

    Previous (Players 2)
    Table of Contents (Introduction)
    Next (Slire Herb)
    Last edited by s-p-n; 03-02-16 at 09:43 PM.





Advertisement