6.) MMO From Scratch - Players Continued

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

    6.) MMO From Scratch - Players Continued


    RaGEZONE Recommends

    RaGEZONE Recommends

    Previous (Players)
    Table of Contents (Introduction)
    Next (Players 3)

    Git the files for this tutorial here.


    Now let's work on the client side of players.

    First, let's get a single player moving around the world, and then we'll work on other players.

    Before we do anything with the players, we need to tell the server that this client is ready. The client is ready once the canvas is created and all of the game object's have run their create methods.

    In /public/js/main.js, we need to emit a 'game-ready' event to the server.
    So let's change that code to this (change in bold):
    Code:
        function create() {
            plugins.forEach(function (plugin) {
                plugin.create();
            });
            comms.emit('game-ready', true);
        }

    So, we need a file: /public/js/objects/player.js

    Let's give ourselves a checklist of what needs to be done.
    • Read user-input from user.
    • Get initial data from server about this player's game-state.
    • Ability to send server commands based on user-input.
    • Preload spritesheet for player.
    • Create player sprite, prepare animations, and put user's username above their head.
    • Based on user input, move the player around the world.
    • Correct user's position based on server response.
    • Use lag-compensation to make animations and position corrections appear smooth.


    We're going to end up with 2 different perspectives of where the user is in the world. We have the most recently received server's perspective, and the sprite's perspective. We're going to use the client's step in relation to the server's step to make the movement appear fairly smooth.

    We'll create a helper function to read the keyboard's state, so we can (in the update tick) check if a key is being held down. I chose to use WASD as well as the arrow (cursor) keys.

    Code:
        function Keys(game) {
            var keyList = {
                up: game.input.keyboard.addKey(Phaser.Keyboard.W),
                down: game.input.keyboard.addKey(Phaser.Keyboard.S),
                left: game.input.keyboard.addKey(Phaser.Keyboard.A),
                right: game.input.keyboard.addKey(Phaser.Keyboard.D),
                space: game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR)
            };
            var cursors = game.input.keyboard.createCursorKeys();
            this.isDown = function isDown(key) {
                if (key in keyList && keyList[key].isDown) {
                    return true;
                }
                if (key in cursors && cursors[key].isDown) {
                    return true;
                }
                return false;
            };
        }


    We'll initialize this key-state machine in the Player.create method. Before we can even worry about keystrokes, we need to preload the spritesheet, and create the player sprite. We should also create the text that appears the sprite's head in the Player.create method. The player should be facing a direction when they are spawned. Rather than have the server be aware of which direction the player is facing, we'll just let each client figure that out, and spawn players facing the screen every time the game is first initialized.

    Code:
        self.preload = function () {
            main.game.load.spritesheet('player', '/assets/game/dude_sprite.png', 25, 50);
        };
        self.create = function () {
            // Set up Player
            self.player = main.game.add.sprite(250, 250, 'player');
            self.player.anchor.setTo(.5, .9);
            self.player.animations.add('down', [0, 1, 0, 2], 10, true);
            self.player.animations.add('left', [3, 4, 3, 5], 10, true);
            self.player.animations.add('right', [3, 4, 3, 5], 10, true);
            self.player.animations.add('up', [6, 7, 6, 8], 10, true);
    
            // Set the still-frame- the direction the player should be standing- to facing the screen.
            self.stillFrame = 0;
    
            // Set up text above player
            self.text = main.game.add.text(250, 190, 'Loading..');
            self.text.anchor.set(0.5);
            self.text.align = 'center';
            self.text.font = 'Arial Black';
            self.text.fontSize = 16;
            self.text.stroke = '#000000';
            self.text.strokeThickness = 3;
            self.text.fill = '#FFFFFF';
    
            // Set up listeners for keyboard input
            self.key = new Keys(main.game);
    
            // Make camera follow this player
            main.game.camera.follow(self.player);
        };


    Please refer to the Phaser.io documentation for questions regarding sprites/text/camera. Basically, we create a player sprite, we set the anchor to the center of the player, somewhere near their feet (this is important later to sort visible objects on the screen properly, and for collision detection). We then add an animation for walking in all 4 directions.

    We set the stillFrame to 0- that is the graphic standing still, facing the client's screen, looking right at the human player.

    We then set up the text. We set the text to say 'Loading..' until the server responds with the username. Remember, we have no data from the server yet of where the player should actually be in the world, or what their name is, or what they are wearing or anything. We set the anchor for the text, so we can center it over the player more easily. We set alignment to center, the style of font, the size, the stroke color, the stroke thickness, and the fill color of the text should all be fairly obvious.

    We initialize the key-state machine, and we tell the game.camera to follow the player we just created.

    Now let's think of what a server command should do.. It's actually really simple, we can make a function that takes a command, and then tells the server the step, time, and action we want it to take. We need a private 'step' variable, we can make that in the Player scope.. Just a regular variable.. 3 lines should do the work for us.

    Code:
        function serverCommand(command) {
            step += 1;
            var data = {time: Date.now(), action: command, step: step};
            comms.emit('player-input', data);
        }


    So, in the update function we're going to have to do nothing until we get the player's data. Once we do get the player's data, we're going to have to update the position of that player. Since we're going to use tweens to move the player around, we'll have to have a newPos object to store the changes, and use a tween to move the player to that new position later. We're also going to need the current time, so we'll make a variable named 'now' for that information.
    Code:
        self.update = function () {
            var now = Date.now();
            var newPos = {x: self.player.x, y: self.player.y};
            if (self.playerData === void 0) {
                return;
            }
            if (self.playerData.username !== self.text.text) {
                self.text.text = self.playerData.username;
                newPos = self.playerData.game;
                main.game.camera.focusOnXY(self.playerData.game.x, self.playerData.game.y);
            }

    We're checking if the playerData username is different then the text above the player's head. The only time this should be different is the first time we run this code after getting the data from the server. Or if a cheater changes it for some reason. In any case, it's a good time to set the basic things up. We set the text to the username, set up a newPos, and focus the game camera on the player's position from the server.

    Now, we're going to need a lastMove and a moveTime- as we're going to be moving the player ever 100ms. Those need to be scoped to the Player function, not the update method. Also, we don't want to increment the step variable forever- that's a memory leak waiting to happen. So we'll set the step variable to 0 anytime the serverStep matches the client's step. That's also a good time to force the client's sprite position to the server's position. Since we're in development, we should probably log the steps on the client and server, just so we know that our code is working properly.

    Code:
            if (lastMove + moveTime < now) {
                lastMove = now;
                if (step === serverStep) {
                    step = 0;
                    newPos = self.playerData.game;
                } else if (step !== 0) {
                    console.log("steps:", step, serverStep);
                }
                //TODO: Move player to newPos.
            }


    Our sprite only has one sideways direction, if you look at it. And it is moving left. If we want to move the sprite to the right, we simply flip the sprite on the y axis. In Phaser that's easy- especially since we set the anchor on the x-axis to 0.5 (center). We also want to play an animation anytime a sprite is walking. Finally, we want to set the stillFrame to the direction the player is walking now. That way when they stop walking, they will be facing the same direction.

    Code:
                if (self.key.isDown('left')) {
                    // Move to left
                    if (self.player.scale.x < 0) {
                        self.player.scale.x *= -1;
                    }
                    newPos.x -= horrSpeed;
                    serverCommand('left');
                    self.player.animations.play('left');
                    self.stillFrame = 3;
                } else if (self.key.isDown('right')) {
                    // Move to right
                    if (self.player.scale.x > 0) {
                        self.player.scale.x *= -1;
                    }
                    newPos.x += horrSpeed;
                    serverCommand('right');
                    self.player.animations.play('right');
                    self.stillFrame = 3;
                }


    Moving up/down is even easier- because we don't have to worry about flipping the sprite to change directions. Other than that, it's the same.

    Code:
                } else if (self.key.isDown('up')) {
                    // Move up
                    newPos.y -= vertSpeed;
                    serverCommand('up');
                    self.player.animations.play('up');
                    self.stillFrame = 6;
                } else if (self.key.isDown('down')) {
                    // Move down
                    newPos.y += vertSpeed;
                    serverCommand('down');
                    self.player.animations.play('down');
                    self.stillFrame = 0;
                }


    Finally, if none of those keys are down, we need to stop the animation and set the sprite's frame to the still frame. After all that is done, we'll run the tweens for the player's sprite and the text above the sprite's head.

    Code:
                } else {
                    // Player not moving
                    self.player.animations.stop();
                    self.player.frame = self.stillFrame;
                }
                main.game.add.tween(self.player).
                    to(newPos, moveTime * tweenTime, 'Linear').
                    start();
                main.game.add.tween(self.text).
                    to({
                        x: newPos.x - 2, 
                        y: newPos.y - 60
                    }, moveTime * tweenTime, 'Linear').
                    start();


    We need to make the tweens a little bit shorter than the moveTime, so we set tweenTime to a decimal between 0 and 1. The higher it is, the more smooth the animations, but the more jerky and noticeable it is when the authoritative server steps in to make a change. Right now I'm trying .9- it seems to work well, but we'll see what happens when we add artificial lag.

    After the Player.update() code, we should put some communication listeners. The initial player channel the server sends us simply called 'player', and all mid-game updates are sent via 'player-move'.

    The first one should just assign the data from the server to self.playerData.
    The second one should do more/less the same thing, but we only want to change the game data. We're not doing anything with the lag yet, but it's useful to log this so we know it's happening. Also in the second one, we need to update the serverStep variable.

    Code:
        comms.on('player', function (data) {
            self.playerData = data;
        });
        comms.on('player-move', function (data) {
            console.log('player-move:', data.game);
            var lag = Date.now() - data.time;
            self.playerData.game = data.game;
            console.log("lag:", lag);
            serverStep = data.step;
        });


    And that's it. Here is the entire /public/js/objects/player.js file:
    Code:
    function Player (main) {
        "use strict";
        var self = this;
        var lastMove = 0;
        var moveTime = 100;
        var horrSpeed = 7.5;
        var vertSpeed = 7.5;
        var step = 0;
        var serverStep = 0;
        var tweenTime = .9;
        self.playerData;
        function Keys(game) {
            var keyList = {
                up: game.input.keyboard.addKey(Phaser.Keyboard.W),
                down: game.input.keyboard.addKey(Phaser.Keyboard.S),
                left: game.input.keyboard.addKey(Phaser.Keyboard.A),
                right: game.input.keyboard.addKey(Phaser.Keyboard.D),
                space: game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR)
            };
            var cursors = game.input.keyboard.createCursorKeys();
            this.isDown = function isDown(key) {
                if (key in keyList && keyList[key].isDown) {
                    return true;
                }
                if (key in cursors && cursors[key].isDown) {
                    return true;
                }
                return false;
            };
        }
        function serverCommand(command) {
            step += 1;
            var data = {time: Date.now(), action: command, step: step};
            comms.emit('player-input', data);
        }
        self.preload = function () {
            main.game.load.spritesheet('player', '/assets/game/dude_sprite.png', 25, 50);
        };
        self.create = function () {
            // Set up Player
            self.player = main.game.add.sprite(250, 250, 'player');
            self.player.anchor.setTo(.5, .9);
            self.player.animations.add('down', [0, 1, 0, 2], 10, true);
            self.player.animations.add('left', [3, 4, 3, 5], 10, true);
            self.player.animations.add('right', [3, 4, 3, 5], 10, true);
            self.player.animations.add('up', [6, 7, 6, 8], 10, true);
    
            // Set the still-frame- the direction the player should be standing- to facing the screen.
            self.stillFrame = 0;
    
            // Set up text above player
            self.text = main.game.add.text(250, 190, 'Loading..');
            self.text.anchor.set(0.5);
            self.text.align = 'center';
            self.text.font = 'Arial Black';
            self.text.fontSize = 16;
            self.text.stroke = '#000000';
            self.text.strokeThickness = 3;
            self.text.fill = '#FFFFFF';
    
            // Set up listeners for keyboard input
            self.key = new Keys(main.game);
    
            // Make camera follow this player
            main.game.camera.follow(self.player);
        };
        self.update = function () {
            var now = Date.now();
            var newPos = {x: self.player.x, y: self.player.y};
            if (self.playerData === void 0) {
                return;
            }
            if (self.playerData.username !== self.text.text) {
                self.text.text = self.playerData.username;
                newPos = self.playerData.game;
                main.game.camera.focusOnXY(self.playerData.game.x, self.playerData.game.y);
            }
            if (lastMove + moveTime < now) {
                lastMove = now;
                if (step === serverStep) {
                    step = 0;
                    newPos = self.playerData.game;
                } else if (step !== 0) {
                    console.log("steps:", step, serverStep);
                }
                if (self.key.isDown('left')) {
                    // Move to left
                    if (self.player.scale.x < 0) {
                        self.player.scale.x *= -1;
                    }
                    newPos.x -= horrSpeed;
                    serverCommand('left');
                    self.player.animations.play('left');
                    self.stillFrame = 3;
                } else if (self.key.isDown('right')) {
                    // Move to right
                    if (self.player.scale.x > 0) {
                        self.player.scale.x *= -1;
                    }
                    newPos.x += horrSpeed;
                    serverCommand('right');
                    self.player.animations.play('right');
                    self.stillFrame = 3;
                } else if (self.key.isDown('up')) {
                    // Move up
                    newPos.y -= vertSpeed;
                    serverCommand('up');
                    self.player.animations.play('up');
                    self.stillFrame = 6;
                } else if (self.key.isDown('down')) {
                    // Move down
                    newPos.y += vertSpeed;
                    serverCommand('down');
                    self.player.animations.play('down');
                    self.stillFrame = 0;
                } else {
                    // Player not moving
                    self.player.animations.stop();
                    self.player.frame = self.stillFrame;
                }
                main.game.add.tween(self.player).
                    to(newPos, moveTime * tweenTime, 'Linear').
                    start();
                main.game.add.tween(self.text).
                    to({
                        x: newPos.x - 2, 
                        y: newPos.y - 60
                    }, moveTime * tweenTime, 'Linear').
                    start();
            }
        };
        self.render = function () {};
        comms.on('player', function (data) {
            self.playerData = data;
        });
        comms.on('player-move', function (data) {
            console.log('player-move:', data.game);
            var lag = Date.now() - data.time;
            self.playerData.game = data.game;
            console.log("lag:", lag);
            serverStep = data.step;
        });
    }


    And we have a player walking around :)

    Next, we'll get other players walking around! Oh, by the way, if you want to simulate lag, put the code in serverCommand() inside of a setTimeout(). Then, every time the client sends the server data it will be whatever ms behind you decide to set. Setting a random number between 0 and 1000 is lot's of fun.

    Code:
        function serverCommand(command) {
            var t = Math.floor(Math.random() * 1000);
            var data;
            step += 1;
            data = {time: Date.now(), action: command, step: step};
            setTimeout(function () {
                comms.emit('player-input', data);
            }, t);
        }


    Setting t to something sane, like 500, or even 2500- will be like artificial lag. Setting t to random like above, will cause the packets to be sent out of order, and since we're assuming sanity, our server/client isn't setup for that environment- and our programs simply can't handle it the way they are coded. In practice, the packets will never be continuously sent out of order and at different time intervals like the above test-case, so we won't plan on that happening on our server/client. But, it is fun to see what happens ;)

    For a realistic test, try setting t to 500, and view the console.log results- the animations are fairly responsive and smooth. We'll test like this later when we test multiplayer in the next tutorial.

    Previous (Players)
    Table of Contents (Introduction)
    Next (Players 3)
    Last edited by s-p-n; 03-02-16 at 04:50 PM.





Advertisement