8.) MMO From Scratch - Slire Herb

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

    8.) MMO From Scratch - Slire Herb


    RaGEZONE Recommends

    RaGEZONE Recommends

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

    Git the files here

    The time has come to add the plant we created in the first set of tutorials to our game stage!

    The very first thing I'm going to do though, is enter a mongo shell and add an herb manually. An herb, in respect to the database, will have 3 properties:
    1. id: ObjectId generated by mongo.
    2. name: A name dictating the class of herb.
    3. place: Array(2) with the structure: [x, y].


    So, I start a mongod instance first. After that, I can start a mongo shell by executing this command in the terminal:
    Code:
    mongo surfaceTension
    Where 'surfaceTension' is the name of the db.

    Once I'm in, I'll run this little insert statement:
    Code:
    db.herbs.insert({name: 'slire', place: [250, 250]});
    Then if I run a db.herbs.find(), I will see it in there. In MongoDB, if a collection doesn't exist, one will be created when you attempt to insert a document.

    Now, we'll create a file, /private/extensions/objects/ai/herb.js.
    We'll require the plant.js file, then we'll create the Herb function. The params passed to Herb should include a name and a place. We'll set self.name to params.name, and the Plant code should take care of the place for us. We're not going to think of different classes of herbs yet, so we'll just use the default plant params. Oh, and we'll set the offspring param to create a new herb instead of a new plant. Then, we'll call Plant with the params. Buuut, instead of assigning plant to something like self.plant, let's inject the Herb's this context when calling plant, Since we're doing that, we should also hijack the Plant's prototype, but after that set the constructor back to Herb. That's a lot to take in, just examine the code below instead:
    Code:
    var Plant = require('./plant.js');
    function Herb(params) {
        "use strict";
        var self = this;
        self.name = params.name;
        var plantParams = Object.create(params);
        plantParams.offspring = function (x, y) {
            var childParams = Object.create(params);
            childParams.place = [x, y];
            return new Herb(childParams);
        };
        Plant.call(self, plantParams);
    }
    Herb.prototype = Object.create(Plant.prototype);
    Herb.prototype.constructor = Herb;
    module.exports = Herb;
    I'll warn you all now: we're going to be jumping around a lot of files to get this working. Many of these files have similar or exactly the same file-name, but different paths. So pay attention to the path so you might become a little less confused. Please comment if you are confused, so I can attempt to fix tutorials for future readers.

    Hopefully you can follow the above code, because now I'm going to jump straight over to a new file: /private/extensions/objects/herbs.js

    .If you remember the test code we ran for plants back in Part 3, we're going to use some of the ideas from that in this file. One major difference, is we're going to start synchronizing herbs with the database. With that said, we're going to need to couple the Herbs function with the server's main variable. We're going to use main.db, and a new namespace: main.game. In /server.js, I add 'game' to the main variable. It is a regular empty object {}. You'll see later that in /private/extensions/objects.js, I create main.game.objects, that is an object {herbs: []}.

    I foresee changes to this structure in the future, but for brevity these are the private variables we're going to need:
    Code:
        var self = this;
        var herbPlaces = [];
        var herbs = [];
        var interval = void 0;
        var intervalTime = 1000;
    1. herbPlaces - a list of serialized places that herbs are at.
    2. herbs - a list of Herb instances.
    3. interval - a variable that will point to the result of setInterval(..).
    4. intervalTime - the time (ms) passed to setInterval.

    For the first variable, we should create a helper function like we did in the test from part 3 for serializing herb places.
    Code:
        function serializePlace(herb) {
            return herb.brain.left.memories.place[0] + 
                ',' + 
                herb.brain.left.memories.place[1];
        }
    Now let's focus on some database helper functions. We might want to make these public methods, actually.. Okay we will. Let's create a self.db object with 2 methods: each, and add.

    self.db.each will cycle through all of the herbs in the database, and take 2 callbacks. The first is executed for each herb, and is passed the document from the db. The second is executed when there are no more herbs to cycle through. If there's an error finding herbs, which there shouldn't be, then we'll log it to the console.

    self.db.add will take an Herb instance and a callback function, add the herb's name & place to the database, and when the transaction is complete, will call the callback with a possible error and the document from the db. We'll just send the callback the default mongojs arguments for now.

    Code:
        self.db = {
            each: function (fn, done) {
                main.db.herbs.find().forEach(function (err, doc) {
                    if (!doc || err) {
                        // out of documents, or error.
                        if (!err) {
                            done();
                        } else {
                            console.log("Herb Find Error!");
                            console.log(err);
                        }
                        return;
                    }
                    fn(doc);
                });
            },
            add: function (herb, fn) {
                main.db.herbs.insert({
                    name: herb.name,
                    place: herb.brain.left.memories.place
                }, fn);
            }
        };
    We have the power to stop an interval once it's created, so let's export that power to the Herbs instance with a stop method:
    Code:
        self.stop = function () {
            clearInterval(interval);
        }
    With a stop method, comes a start method. We'll basically rip the code for this directly from the test code in part 3 that we used for plants. We'll just change a few things so it works with herbs and a database. We set the private interval variable to this setInterval, and we use the intervalTime for the time so it's easy to change later if needed. We'll loop through the private herbs variable, set a local child variable to the herb instance's cycle. We'll then check if the child is an instance of an Herb, and if it is, we'll check if the place it was spawned in isn't already taken. If both of those conditions are fulfilled, we'll add the child to the db. Once it is added to the db successfully, we'll add the herb's serialized place to the array, and the herb instance to the herbs array.

    Now we need a callback... We need to tell the server, "Hey! a new herb was created!" But we're faced with a problem- We don't have a socket to send to.. I mean, we shouldn't really couple the Herbs function to a socket, anyway. How about we require a callback to send this information to. So, when people call Herbs, they need to pass it main, and a callback for when herbs are created. We'll call this callback, self.cycleCallback- because it's called when the cycle has something to say.. idk, maybe a bad choice of names but that's what I called it :).

    If we get a db error, we should tell the server console that we did- you never know, we might get a db error here.

    Here's our self.start method:
    Code:
        self.start = function () {
            interval = setInterval(function () {
                herbs.forEach(function (herb) {
                    var child = herb.cycle();
                    if (child instanceof Herb && 
                        (herbPlaces.indexOf(serializePlace(child)) === -1)
                    ) {
                        self.db.add(child, function (err, doc) {
                            if (!err) {
                                herbPlaces.push(serializePlace(child));
                                herbs.push(child);
                                self.cycleCallback(doc);
                            } else {
                                console.log("Herb Insertion Error!");
                                console.log(err);
                            }
                        });
                    }
                });
            }, intervalTime);
        };
    Now we're done with methods and helper functions. Let's run some construction code. We need to assign self.cycleCallback to the argument, and we need to get all of the herbs from the DB and assign them to the herbs array, and their places to the herbPlaces array. We should probably call self.cycleCallback with each herb, too, as, I mean we created some herbs, right? Then once we're done getting each of the herbs from the db, we should call self.start()- and now the wheels are turning, baby!
    Code:
        self.cycleCallback = childCallback;
        // Construct the herbs list from db:
        self.db.each(function (doc) {
            var herb = new Herb(doc);
            herbPlaces.push(serializePlace(herb));
            herbs.push(herb);
            self.cycleCallback(doc);
        }, function () {
            self.start();
        });
    And the entire code for /private/extensions/objects/herbs.js:
    Code:
    var Herb = require('./ai/herb.js');
    function Herbs(main, childCallback) {
        "use strict";
        var self = this;
        var herbPlaces = [];
        var herbs = [];
        var interval = void 0;
        var intervalTime = 1000;
        function serializePlace(herb) {
            return herb.brain.left.memories.place[0] + 
                ',' + 
                herb.brain.left.memories.place[1];
        }
        self.db = {
            each: function (fn, done) {
                main.db.herbs.find().forEach(function (err, doc) {
                    if (!doc || err) {
                        // out of documents, or error.
                        if (!err) {
                            done();
                        } else {
                            console.log("Herb Find Error!");
                            console.log(err);
                        }
                        return;
                    }
                    fn(doc);
                });
            },
            add: function (herb, fn) {
                main.db.herbs.insert({
                    name: herb.name,
                    place: herb.brain.left.memories.place
                }, fn);
            }
        };
        self.stop = function () {
            clearInterval(interval);
        }
        self.start = function () {
            interval = setInterval(function () {
                herbs.forEach(function (herb) {
                    var child = herb.cycle();
                    if (child instanceof Herb && 
                        (herbPlaces.indexOf(serializePlace(child)) === -1)
                    ) {
                        self.db.add(child, function (err, doc) {
                            if (!err) {
                                herbPlaces.push(serializePlace(child));
                                herbs.push(child);
                                self.cycleCallback(doc);
                            } else {
                                console.log("Herb Insertion Error!");
                                console.log(err);
                            }
                        });
                    }
                });
            }, intervalTime);
        };
        self.cycleCallback = childCallback;
        // Construct the herbs list from db:
        self.db.each(function (doc) {
            var herb = new Herb(doc);
            herbPlaces.push(serializePlace(herb));
            herbs.push(herb);
            self.cycleCallback(doc);
        }, function () {
            self.start();
        });
    }
    module.exports = Herbs;
    Actually, the wheels aren't turning yet. We need to instantiate Herbs. So I made an extension specifically for game objects. I consider something a game object if it will be on the client's game stage, and it isn't a map tile. Technically, players would be game objects, too, but.. as far as the server is concerned, players are considered different- at least for this stage in development.

    Since this is an extension, recall we need to set it up like this:
    Code:
    module.exports = function (m) {
    
    }
    We need to require the Herbs function. We can do that at the top.
    Code:
    var Herbs = require('./objects/herbs.js');
    module.exports = function (m) {
        
    }
    We need to set m.game.objects to {herbs: []}. This time herbs won't be an array of Herb instances- it will be an array of client-ready herbs. So each item in this array will be formatted like:
    Code:
    {
        name: "slire",
        place: [250, 250]
    }
    After we create herbs list, it will be available to socket channels! That's great news if we intend to send clients a bulk list of herbs.

    Now we can instantiate the Herbs function, passing it 'm' and a callback that takes an herb as the argument. In that callback, we'll push the herb onto m.game.objects.herbs. We will also emit an event on main, titled 'herb-created'.

    Take a look at the code for /private/extensions/objects.js:
    Code:
    var Herbs = require('./objects/herbs.js');
    module.exports = function (m) {
        m.game.objects = {herbs: []};
        var herbs = new Herbs(m, function (herb) {
            m.game.objects.herbs.push(herb);
            m.event.emit('herb-created', herb);
            console.log("herb created:", herb);
        });
    }
    We're almost done with the server part! Now, we need to figure out a way to package up these herbs, and send them to the clients. To do that, let's create a channel for herbs! /private/channels/herbs.js

    I finally got around to setting a session.state variable. In /private/extensions/socket.js, I defaulted the session.state to 1. In /private/channels/player.js, inside the initPlayer helper function, I set session.state to 4 and also emitted a session event called 'game-ready'. So, whenever we require the player to be in state 4, we can just check session.state now. Likewise, we can listen on 'game-ready' to send initialization packets as soon as the player enters state 4.

    So, when we get a main event, 'herb-created', we'll check if the player is at state 4, and if so, send them the new herb that was created. When we get the session event, 'game-ready', we'll send the player a bulk list of all the herbs on the server. Spoiler for future tutorials: This won't scale. For now, we'll plan on optimizing later.

    So here is the code for sending herbs to the masses:
    Code:
    module.exports = function (m, session) {
        "use strict";
        var socket = session.socket;
        m.event.on('herb-created', function (herb) {
            if (session.state === 4) {
                socket.emit('herb-created', herb);
            }
        });
        session.event.on('game-ready', function (ready) {
            if (ready) {
                socket.emit('herbs-init', m.game.objects.herbs);
            }
        });
    };
    Now, we send the client 2 kinds of packets. A single herb is sent in 'herb-created', and all the herbs are sent using 'herbs-init'.

    Now we can code the client, and we should be done.

    So, create a file /public/js/objects/herbs.js​.
    We need to preload the image for slire herbs, and name it 'slire'.
    We're not concerned with create, update, or render- so we'll just put blank functions there. This is becoming a bad pattern.... We'll have to figure out a way to remove all these frivolous functions in the near future.
    Anyway, we need two comms listeners- one for 'herbs-init', that will make all of the herbs sprites, and thus put them on the game stage. And one for 'herb-created', that will make a single herb sprite, thus putting it on the game stage.

    Here's the code:
    Code:
    function Herbs (main) {
        "use strict";
        var self = this;
        self.preload = function () {
            main.game.load.image('slire', './assets/game/slire.png');
        };
        self.create = function () {};
        self.update = function () {};
        self.render = function () {};
        comms.on('herbs-init', function (herbs) {
            console.log("got herbs init:");
            console.log(herbs);
            herbs.forEach(function (herb) {
                main.game.add.sprite(herb.place[0], herb.place[1], herb.name);
            });
        });
        comms.on('herb-created', function (herb) {
            console.log("herb created");
            console.log(herb);
            main.game.add.sprite(herb.place[0], herb.place[1], herb.name);
        });
    }
    We need to edit /public/index.html and include this JS file with a <script> tag.
    We need to edit /public/js/main.js and add this plugin to the list of plugins.

    We're done! If you run this code, you should see one herb- the one we created manually. The reason we created one herb manually, is so it can reproduce and make more herbs. If you let the server run for a few minutes, you'll start to see a pasture of herbs appear before your very eyes.

    You'll also notice that the draw order is all messed up- herbs always display on top of players, and this is unnatural. In the next tutorial we'll add all of the game objects to a Phaser group. In Phaser, sprites are drawn to the stage in the same order we create them, or if the sprites are all part of a group, the order they are in the group. So in the next tutorial, we'll have to sort these sprites on the client so the game visually makes sense.

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





Advertisement