Welcome!

Join our community of MMO enthusiasts and game developers! By registering, you'll gain access to discussions on the latest developments in MMO server files and collaborate with like-minded individuals. Join us today and unlock the potential of MMO server development!

Join Today!

5.) MMO From Scratch - Players

Joined
Jun 8, 2007
Messages
1,985
Reaction score
490
Previous (The Client)
Table of Contents (Introduction)
Next (Players 2)

I already briefly went over the login/content serving system in a previous tutorial. If you aren't familiar with that, I suggest going through that before moving on.

I'm not going into the code for login/register here. If you want to skim through the code on github, I'd encourage that. Just know that when a user registers, the data stored is an object formatted like this:
Code:
{
   username: <string>,
   password: <hashString>,
   key: <hashString || boolean:False>,
   game: <object> {x: <number>, y: <number>, ...}
}

At this point, I'm only going to set up 4 commands for walking - up, down, left, right. The client will send these commands to the server, and the server will determine if it those commands are reasonable enough to consider legitimate, and if so, execute those commands and broadcast the game state to the player who sent those commands. The server broadcasts changes to game-state to every player.

So, I'll start with a file: /private/channels/player.js.

Here are some commands we might want to execute:
Code:
[COLOR=#008000][B]var[/B][/COLOR] exec [COLOR=#666666]=[/COLOR] {
    up[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (player) {
        player.game.y [COLOR=#666666]-=[/COLOR] [COLOR=#666666]7.5[/COLOR];
    },
    down[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (player) {
        player.game.y [COLOR=#666666]+=[/COLOR] [COLOR=#666666]7.5[/COLOR];
    },
    left[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (player) {
        player.game.x [COLOR=#666666]-=[/COLOR] [COLOR=#666666]7.5[/COLOR];
    },
    right[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (player) {
        player.game.x [COLOR=#666666]+=[/COLOR] [COLOR=#666666]7.5[/COLOR];
    }
};

Whenever a command is executed, Server is going to make assumptions after some simple verification that the command will execute successfully. Server will simultaneously update the database, and broadcast the changed state to all of the players. If for whatever reason Server was wrong or the database didn't accept the change, Server will broadcast another state to the clients. Note that any state the clients receive is in the past. This is one of the most challenging points we have to work with in regards to multiplayer game development over the internet.

So let's get started with this player channel. The player can be in one of four different states-
  1. not logged in and game not ready (just entered site)
  2. not logged in but game ready (logged out of game)
  3. logged in but game not ready (just hit the log-in button and was successfully logged in, game initializing)
  4. logged in and game ready (playing the game)
We want the transitions to/from these states to be as seamless as possible. Here's how we're going to respond to these different states:
  1. Display a login form.
  2. Remove session (server- done automatically when socket connection lost), clear cookie (client), refresh web-browser (also clears session by losing socket connection- transition to state 1).
  3. Initialize game (client) and send gameReady message to server, server responds with initial game state specific for the player. Transition to state 4.
  4. Periodically send game state updates to the player, and listen for commands from the player (server). Listen to UI events like mouse/keyboard/gamepad from player, send commands to server (client).

So, first and foremost, to handle the the content-sending process on the server, we'll send the login form right away if there's no sessionID cookie. Also, we'll listen on the logged_in event, and if we get a true- indicating a successful login, we'll send the game. If we get false- indicating an unsuccessful login attempt, we'll send the login form again.

Here is /private/channels/game.js:
Code:
[COLOR=#008000][B]var[/B][/COLOR] content [COLOR=#666666]=[/COLOR] {
    login[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]null[/B][/COLOR],
    game[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]null[/B][/COLOR]
}

module.exports [COLOR=#666666]=[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (m, session) {
    [COLOR=#008000][B]var[/B][/COLOR] socket [COLOR=#666666]=[/COLOR] session.socket;
    [COLOR=#008000][B]function[/B][/COLOR] sendLoginForm() {
        socket.emit([COLOR=#BA2121]"content"[/COLOR], {
            selector[COLOR=#666666]:[/COLOR] [COLOR=#BA2121]"#canvas"[/COLOR],
            html[COLOR=#666666]:[/COLOR] content.login [COLOR=#666666]||[/COLOR] (content.login [COLOR=#666666]=[/COLOR] m.fs.readFileSync(m.root [COLOR=#666666]+[/COLOR] [COLOR=#BA2121]'/protected/login.html'[/COLOR], [COLOR=#BA2121]'utf8'[/COLOR]))
        });
    }
    [COLOR=#008000][B]function[/B][/COLOR] sendGame() {
        socket.emit([COLOR=#BA2121]"content"[/COLOR], {
            selector[COLOR=#666666]:[/COLOR] [COLOR=#BA2121]"#canvas"[/COLOR],
            html[COLOR=#666666]:[/COLOR] content.game [COLOR=#666666]||[/COLOR] (content.game [COLOR=#666666]=[/COLOR] m.fs.readFileSync(m.root [COLOR=#666666]+[/COLOR] [COLOR=#BA2121]'/protected/game.html'[/COLOR], [COLOR=#BA2121]'utf8'[/COLOR]))
        });
    }
    [COLOR=#008000][B]if[/B][/COLOR] ([COLOR=#666666]![/COLOR]socket.request.cookies.sessionID) {
        sendLoginForm();
    }

    session.event.on([COLOR=#BA2121]"logged_in"[/COLOR], [COLOR=#008000][B]function[/B][/COLOR] (success) {
        [COLOR=#008000][B]if[/B][/COLOR] (success [COLOR=#666666]===[/COLOR] [COLOR=#008000][B]true[/B][/COLOR]) {
            sendGame();
        } [COLOR=#008000][B]else[/B][/COLOR] {
            sendLoginForm();
        }
    });
}

That should fulfill the server's responsibility in handling states 1 through 3- at least as far as content is concerned. The login/register channels handle the rest of those 3 responsibilities.

I'll hold-off on handling the logout functionality- but that's almost entirely handled on the client. The server should handle step 2 pretty much automatically as long as the client does it's job. I'm not concerned if a cheater winds up in a fucked up game-state by changing the client code- I can't really prevent that. I just want to be sure it won't give the cheater any advantage.

I want to concentrate on State 4 for the rest of this tutorial.

Before we can do anything, we must set up a database. To do that, create a folder named /data and execute the following command in a terminal to start the database. The first time you run this, it will take a while as MongoDB must initialize the database. Please use MongoDB 3.x if you're following along with this tutorial.
Code:
mongod --dbpath data
For best results, have mongo running before starting the node.js server.

If you were successful starting your MongoDB instance, this should be the last message you see:
Code:
[initandlisten] waiting for connections on port [B]27017[/B]
If your port is intentionally different than the default port 27017, you'll have to change some code in /private/extensions/db.js for your connection to be established.

Once you have the database set up, you should be able to run the server and create a test account.

You should get a message when logging in for the first time saying,
Code:
[CENTER][COLOR=#FF0000][B]Unable to find user, <username>. [/B][/COLOR][/CENTER]
[COLOR=#0000ff][U][SIZE=3][CENTER][B]Create it?[/B][/CENTER]
[/SIZE][/U][/COLOR]
Clicking the link will immediately create the user, and you can log-in with that username/password in the future. After clicking that link, the server will send /protected/game.html which has the following code:
Code:
[COLOR=#008000][B]<script>[/B][/COLOR]
[COLOR=#008000][B]if[/B][/COLOR] ([COLOR=#008000]window[/COLOR].gameObj [COLOR=#666666]===[/COLOR] [COLOR=#008000][B]void[/B][/COLOR] [COLOR=#666666]0[/COLOR]) {
    [COLOR=#008000]window[/COLOR].gameObj [COLOR=#666666]=[/COLOR] {};
} [COLOR=#008000][B]else[/B][/COLOR] {
    [COLOR=#008000]window[/COLOR].location [COLOR=#666666]=[/COLOR] [COLOR=#BA2121]"/"[/COLOR];
}
console.log(gameObj);
initializeGame(gameObj);
[COLOR=#008000][B]</script>[/B][/COLOR]
The above code checks if the gameState is already active, if it is, it refreshes the browser to prevent duplicate game instances running at the same time. This situation may arise from the server restarting, which I've observed result in strange things happening. A different solution could be made very complex by stubbornly not refreshing the browser, but I find this to be the easiest catch-all solution.

Back to the player!

Here is the code from /private/channels/player.js:
Code:
[COLOR=#008000][B]var[/B][/COLOR] exec [COLOR=#666666]=[/COLOR] {
    up[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (player) {
        player.game.y [COLOR=#666666]-=[/COLOR] [COLOR=#666666]7.5[/COLOR];
    },
    down[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (player) {
        player.game.y [COLOR=#666666]+=[/COLOR] [COLOR=#666666]7.5[/COLOR];
    },
    left[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (player) {
        player.game.x [COLOR=#666666]-=[/COLOR] [COLOR=#666666]7.5[/COLOR];
    },
    right[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (player) {
        player.game.x [COLOR=#666666]+=[/COLOR] [COLOR=#666666]7.5[/COLOR];
    }
};

module.exports [COLOR=#666666]=[/COLOR] [COLOR=#008000][B]function[/B][/COLOR] (m, session) {
    [COLOR=#008000][B]var[/B][/COLOR] socket [COLOR=#666666]=[/COLOR] session.socket;
    [COLOR=#008000][B]var[/B][/COLOR] player;
    [COLOR=#008000][B]var[/B][/COLOR] intervalTime [COLOR=#666666]=[/COLOR] [COLOR=#666666]100[/COLOR];
    [COLOR=#008000][B]var[/B][/COLOR] lastCmdTime [COLOR=#666666]=[/COLOR] [COLOR=#666666]0[/COLOR];

    [COLOR=#008000][B]function[/B][/COLOR] updatePlayer () {
        m.db.users.update({
            [COLOR=#BA2121]'username'[/COLOR][COLOR=#666666]:[/COLOR] player.username
        }, {
            $set[COLOR=#666666]:[/COLOR] {game[COLOR=#666666]:[/COLOR] player.game}
        }, {multi[COLOR=#666666]:[/COLOR] [COLOR=#008000][B]false[/B][/COLOR]});
        m.event.emit([COLOR=#BA2121]'player-update'[/COLOR], player);
    }

    [COLOR=#008000][B]function[/B][/COLOR] initPlayer () {
        [COLOR=#008000][B]var[/B][/COLOR] userId, other;
        socket.emit([COLOR=#BA2121]'player'[/COLOR], {username[COLOR=#666666]:[/COLOR] player.username, game[COLOR=#666666]:[/COLOR] player.game});
        [COLOR=#008000][B]for[/B][/COLOR] (userId [COLOR=#008000][B]in[/B][/COLOR] m.session) {
            [COLOR=#008000][B]if[/B][/COLOR] (userId [COLOR=#666666]===[/COLOR] session.id [COLOR=#666666]||[/COLOR] m.session[userId].user [COLOR=#666666]===[/COLOR] [COLOR=#008000][B]void[/B][/COLOR] [COLOR=#666666]0[/COLOR]) {
                [COLOR=#008000][B]continue[/B][/COLOR];
            }
            other [COLOR=#666666]=[/COLOR] m.session[userId].user;
            socket.emit([COLOR=#BA2121]'others-update'[/COLOR], {username[COLOR=#666666]:[/COLOR] other.username, game[COLOR=#666666]:[/COLOR] other.game});
        }
    }
    session.event.on([COLOR=#BA2121]'logged_in'[/COLOR], [COLOR=#008000][B]function[/B][/COLOR] (result) {
        [COLOR=#008000][B]if[/B][/COLOR] (result [COLOR=#666666]===[/COLOR] [COLOR=#008000][B]true[/B][/COLOR]) {
            player [COLOR=#666666]=[/COLOR] session.user;
        }
    });

    socket.on([COLOR=#BA2121]'game-ready'[/COLOR], [COLOR=#008000][B]function[/B][/COLOR] () {
        [COLOR=#008000][B]if[/B][/COLOR] (player) {
            initPlayer();
        } [COLOR=#008000][B]else[/B][/COLOR] {
            session.event.on([COLOR=#BA2121]'logged_in'[/COLOR], [COLOR=#008000][B]function[/B][/COLOR] (result) {
                [COLOR=#008000][B]if[/B][/COLOR] (result [COLOR=#666666]===[/COLOR] [COLOR=#008000][B]true[/B][/COLOR]) {
                    initPlayer();
                }
            });
        }
    });
    socket.on([COLOR=#BA2121]'player-input'[/COLOR], [COLOR=#008000][B]function[/B][/COLOR] (data) {
        [COLOR=#008000][B]var[/B][/COLOR] now [COLOR=#666666]=[/COLOR] [COLOR=#008000]Date[/COLOR].now();
        [COLOR=#008000][B]var[/B][/COLOR] lag [COLOR=#666666]=[/COLOR] now [COLOR=#666666]-[/COLOR] data.time;
        [COLOR=#008000][B]var[/B][/COLOR] cmd [COLOR=#666666]=[/COLOR] data.action;

        console.log([COLOR=#BA2121]'cmd:'[/COLOR], cmd, [COLOR=#BA2121]'lag:'[/COLOR], lag, [COLOR=#BA2121]'step:'[/COLOR], data.step);
        [COLOR=#008000][B]if[/B][/COLOR] (cmd [COLOR=#008000][B]in[/B][/COLOR] exec) {
            [COLOR=#008000][B]if[/B][/COLOR] ((lastCmdTime [COLOR=#666666]+[/COLOR] intervalTime) [COLOR=#666666]>[/COLOR] now) {
                console.log([COLOR=#BA2121]"Client moving too fast!"[/COLOR], lastCmdTime, now);
            } [COLOR=#008000][B]else[/B][/COLOR] [COLOR=#008000][B]if[/B][/COLOR] ([COLOR=#008000]isNaN[/COLOR](lag) [COLOR=#666666]||[/COLOR] lag [COLOR=#666666]<[/COLOR] [COLOR=#666666]0[/COLOR]) {
                console.log([COLOR=#BA2121]"Client supplied invalid lag data!"[/COLOR], data.lag);
            [COLOR=#008000][B]else[/B][/COLOR] {
                exec[cmd](player);
            }
            lastCmdTime [COLOR=#666666]=[/COLOR] now;
            
            socket.emit([COLOR=#BA2121]'player-move'[/COLOR], {game[COLOR=#666666]:[/COLOR] player.game, time[COLOR=#666666]:[/COLOR] now [COLOR=#666666]-[/COLOR] lag, step[COLOR=#666666]:[/COLOR] data.step});
            updatePlayer();
        } [COLOR=#008000][B]else[/B][/COLOR] [COLOR=#008000][B]if[/B][/COLOR] (cmd [COLOR=#666666]===[/COLOR] [COLOR=#BA2121]'still'[/COLOR]) {
            [COLOR=#408080][I]//m.event.emit('player-update', player);[/I][/COLOR]
        }
    });
}

There's a lot going on here, I'll go through it step-by-step.

First, we declare some variables.

  • socket - the client's socket connection data. It belongs to one client only.
  • player - the player for this socket.
  • intervalTime - The minimum amount of time there should be between client commands.
  • lastCmdTime - The last time a command was received.

We have 2 helper functions, the first is updatePlayer. updatePlayer will update the DB with the user's game-state, and without waiting for confirmation, emit a 'player-update' event on the main variable. In laymen's terms, updatePlayer will not wait for the database to say the command was successful- it will just shout out and tell all the other players that this player's game-state has changed, and give them that changed data. This is optimistic to say the least, but we get a more responsive game this way- and the client's commands are usually executed successfully, anyway.

Then we have initPlayer. This function will tell the player's client who (s)he is. It will also tell the player about other connected players.

After those 2 helper functions, we have 3 socket listeners.

The first listens for 'logged_in' for this session. Once this client successfully logs in, we assign the player variable documented above, to the logged in user.

The second listens for the client to shout, 'game-ready'. When the client does that, we check if the player exists, and if not, we listen on for the server to shout 'logged_in' for this session. After either case, we invoke initPlayer().

The third listens for the client to shout 'player-input'. Spoiler: This is the fun part.

We start with 3 variables:
  • now - the current time.
  • lag - the time the client sent the request subtracted by the current time.
  • cmd - the command the client wants Server to execute.
We're in development, so we log everything the client sends us.
If the command sent from the client is a command Server recognizes, we go further, otherwise we ignore this request.
If the time between commands is invalid, Server doesn't execute the command,
If the lag variable is invalid, Server doesn't execute the command.
Otherwise, server does execute the command, and the game-state is changed.
After those checks, Server sets the lastCmdTime to the current time, and tells this client the current game-state, and then executes updatePlayer(). The client uses the step it gave us to determine if it needs to fix the player's game-state or not. In the end, we should be able to, on the client, use this system to implement some lag compensation.

Now, before I go further I want to point out a bug in the above steps. That is the "Client moving too fast!", when in fact the client is acting legitimate. The 100ms delay on the client is flawed if, a bunch of commands are stacked up in the pipeline during a delay in internet connectivity, and the server receives several commands at once which were sent 100ms apart. We'll probably visit this code in the future to attempt and fix this bug, but to prevent cheating, we'll just let it be as-is.

In the next tutorial, we'll visit the player on the client- and we'll be able to see our character moving around the world.

Previous (The Client)
Table of Contents (Introduction)
Next (Players 2)
 
Last edited:
Back
Top