3.) MMO From Scratch - Plant

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

    3.) MMO From Scratch - Plant


    RaGEZONE Recommends

    RaGEZONE Recommends

    Previous (The AI)
    Table of Contents (Introduction)
    Next (The Client)

    Plants in Surface Tension are stupid, and so are pretty easy to make. The requirements don't specify any growth stages for plants. We don't need to worry about light sources, or water. All we need to do with plants is have them develop seeds and drop seeds. A seed is a rough copy of the plant. We'll assume several breeds of plants will inherit the Plant class when coding the copy. So if a tree inherits the Plant class, when a tree drops a seed, the same species of tree will spawn from that seed.

    So a Plant needs 2 feedback loops- DevelopSeed and DropSeed. If we don't require a development cycle (ie: force some amount of time required to create seeds), then plants will just copy themselves every cycle- and that results in a mess of plants very quickly.

    In the last tutorial I didn't go into detail in how a feedback loop works. Every feedback loop is required to follow an abstract interface. A feedback loop must take the AI as the argument, and must have a cycle method. If inheriting the brain, which everything should (even if plants don't actually have brains), a feedback loop may use the memory and decision-making engine. In real life, plants don't have a memory to determine if it's time to develop a seed- plants have a finite amount of places seeds may grow, and if there's room for a seed, one develops. I decided to use memories, because this is a computer program and I found it easy enough to use memories for storing information.

    So, I'll go into detail with the code in this one- unlike the last tutorial.

    We've determined a plant will need to take some parameters, so we'll start the code like this.

    Code:
    var Brain = require('./brain.js');
    function Plant(params) {
        var self = this;
        self.brain = new Brain();
    }
    We now have a plant with a brain. If that's not strange enough, let's add a feedback loop to this plant. We should always store the feedback loops outside of the class they are for, as we'll be creating thousands of plants in this game, and they can all share the same functions for feedback loops. I like to put my requires at the top of the file, the feedback loops just under that, and the class that takes the name of the file (plant.js) after the feedback loops. Finally, I'll export that class after it is declared.

    Code:
    // requirements
    // feedback loops
    // class
    // module.exports = class
    The AI will instantiate a new instance of every feedback loop (which is inexpensive in JavaScript). Therefore, we'll capitalize the first letter of the name in that function.
    Here's what the code should look like now:
    Code:
    var Brain = require('./brain.js');
    function DevelopSeed(ai) {
        var self = this;
        self.cycle = function () {
    
        }
    }
    function Plant(params) {
        var self = this;
        self.brain = new Brain();
        self.brain.createFeedback(DevelopSeed);
    }
    module.exports = Plant;
    Right now that feedback loop isn't doing anything. Also, the params passed to the Plant aren't accessible to the feedback loop. We can add the params to the plant's memory- but remember there are 2 sides of the brain, so we need to decide if we want to assign the same memory twice, or sync those memories. Some herbs are required to drop a single seed, and since the brain creates 2 DevelopSeed feedbacks (left and right brain), we need to sync the memories so it's possible to develop a single seed. We should sync the memories before every cycle, and to do that we need to abstract the self.brain.cycle. Let's create a Plant.cycle method, too.

    Code:
    var Brain = require('./brain.js');
    function DevelopSeed(ai) {
        var self = this;
        self.cycle = function () {
    
        }
    }
    function Plant(params) {
        var self = this;
        var memories;
        function synchronize() {
            self.brain.left.sync('memories.maxSeeds');
           self.brain.left.sync('memories.seedTime');
        }
        self.brain = new Brain();
        memories = self.brain.left.memories;
        memories.maxSeeds = params.maxSeeds || 2;
        memories.seedTime = params.seedTime || 60000;
        synchronize();
        self.brain.createFeedback(DevelopSeed);
        self.cycle = function () {
            synchronize();
            self.brain.cycle();
        }
    }
    module.exports = Plant;
    Now let's test this code:
    Code:
    var Plant = require('./plant.js');
    var p = new Plant({});
    p.cycle();
    console.log(p.brain.left.memories, p.brain.right.memories);
    The result is this:
    Code:
    {
      maxSeeds: 2, 
      seedTime: 60000, 
      lastAction: 'do nothing'
    } 
    {
      maxSeeds: 2, 
      seedTime: 60000, 
      lastAction: 'do nothing' 
    }
    This is good, 2 identical memories. Note that between cycles these memories may become out of sync- and that's usually ideal. If desired, memories may be synchronized during a cycle, too- using ai.sync(). The way we determine if a param is undefined is faulty. If we supply 0 to max seeds, it will default to 2. That's not ideal. We should create a function to make params optional.

    Code:
    function Plant(params) {
        var self = this;
        var memories;
        function optional(name, defaultValue) {
            return params[name] !== void 0 ? params[name] : defaultValue;
        }
        function synchronize() {
            self.brain.left.sync('memories.maxSeeds');
            self.brain.left.sync('memories.seedTime');
        }
        self.brain = new Brain();
        memories = self.brain.left.memories;
        memories.maxSeeds = optional('maxSeeds', 2);
        memories.seedTime = optional('seedTime', 60000);
        synchronize();
        self.brain.createFeedback(DevelopSeed);
        self.cycle = function () {
            synchronize();
            return self.brain.cycle();
        }
    }
    Now if we test, we can set maxSeeds to 0 and it will work as expected.

    Now let's work on that DevelopSeed feedback loop, using the memories we have created. We'll create an endTime based on the current time and the seedTime, and we also need a memory of how many seeds a plant has already, so not to exceed the maxSeeds.

    Code:
        ai.memories.seeds = 0;
        ai.memories.endTime = Date.now() + ai.memories.seedTime;

    We'll create a function to use as a decision to create a seed- this function should just do- it shouldn't have any conditional logic. We'll use conditional logic to determine whether to add a decision or not later.

    Code:
        function createSeed() {
            ai.memories.seeds += 1;
            ai.memories.endTime = Date.now() + ai.memories.seedTime;
        }
    Now in the cycle, we'll determine if we should add a decision, and set the weight for that decision based on how long the seed has been ready to develop.

    Code:
        self.cycle = function () {
            var now = Date.now();
            var weight = now - ai.memories.endTime;
            if (weight > 0 &&
                ai.memories.maxSeeds > ai.memories.seeds
            ) {
                ai.addDecision({
                    weight: weight,
                    action: createSeed,
                    title: "create seed"
                });
            }
        }
    Altogether,the DevelopSeed feedback loop should look like this:
    Code:
    function DevelopSeed(ai) {
        var self = this;
        ai.memories.seeds = 0;
        ai.memories.endTime = Date.now() + ai.memories.seedTime;
        function createSeed() {
            ai.memories.seeds += 1;
            ai.memories.endTime = Date.now() + ai.memories.seedTime;
        }
        self.cycle = function () {
            var now = Date.now();
            var weight = now - ai.memories.endTime;
            if (weight > 0 &&
                ai.memories.maxSeeds > ai.memories.seeds
            ) {
                ai.addDecision({
                    weight: weight,
                    action: createSeed,
                    title: "create seed"
                });
            }
        }
    }
    Now, to test this, we have to run the cycles in an interval. I'm using a unix terminal to test this, so I'm clearing the console every cycle using process.stdout.write('\033c'); If you're using windows, you can use a bunch of empty lines, or some other trick to clear the console screen, instead.

    Here is the test code:
    Code:
    var Plant = require('./plant.js');
    var p = new Plant({
        maxSeeds: 5, 
        seedTime: 5000
    });
    var leftSeeds = 0;
    var rightSeeds = 0;
    var interval = setInterval(function () {
        process.stdout.write('\033c');
        var result = p.cycle();
        if (p.brain.left.memories.lastAction === 'create seed' && p.brain.left.decisions.length > 1) {
            leftSeeds += 1
        }
        if (p.brain.right.memories.lastAction === 'create seed' && p.brain.right.decisions.length > 1) {
            rightSeeds += 1;
        }
        console.log("Left Decisions:", p.brain.left.decisions);
        console.log("Right Decisions:", p.brain.right.decisions);
        console.log("Seeds create (left):", leftSeeds);
        console.log("Seeds create (right):", rightSeeds);
        console.log("Total seeds:", leftSeeds + rightSeeds);
    }, 1000);
    In this test, I'm interested in what decisions there are to choose from,how many seeds each side of the brain has, and how many seeds there are total. The seed time is 5 seconds in this test, and the maxSeeds is set to 5. When I run this code, it will give me 10 seeds in about 5 seconds. The way this works now makes it impossible for a plant to develop only a single seed. We'll change that in the DevelopSeed code by synchronizing the amount of seeds a plant has. I'm okay with the plants developing 2 seeds at a time, so I won't multiply the seedTime by 2. It really is taking 5 seconds to develop a seed, but plants are developing 2 seeds at once. It's also possible to use a single side of the brain for plants instead of both sides, but I like it this way. Later we may change the Plant code to allow n developments at a time- perhaps requiring a Fibonacci number to mimic real-world plants. But, that's the future.

    So,the solution is 1 extra line in the createSeed function. Here is the entire plant.js file so far: (latest change in bold)
    Code:
    var Brain = require('./brain.js');
    function DevelopSeed(ai) {
        var self = this;
        ai.memories.seeds = 0;
        ai.memories.endTime = Date.now() + ai.memories.seedTime;
        function createSeed() {
            ai.memories.seeds += 1;
            ai.memories.endTime = Date.now() + ai.memories.seedTime;
            ai.sync('memories.seeds');
        }
        self.cycle = function () {
            var now = Date.now();
            var weight = now - ai.memories.endTime;
            if (weight > 0 &&
                ai.memories.maxSeeds > ai.memories.seeds
            ) {
                ai.addDecision({
                    weight: weight,
                    action: createSeed,
                    title: "create seed"
                });
            }
        }
    }
    function Plant(params) {
        var self = this;
        var memories;
        function optional(name, defaultValue) {
            return params[name] !== void 0 ? params[name] : defaultValue;
        }
        function synchronize() {
            self.brain.left.sync('memories.maxSeeds');
            self.brain.left.sync('memories.seedTime');
        }
        self.brain = new Brain();
        memories = self.brain.left.memories;
        memories.maxSeeds = optional('maxSeeds', 2);
        memories.seedTime = optional('seedTime', 60000);
        synchronize();
        self.brain.createFeedback(DevelopSeed);
        self.cycle = function () {
            synchronize();
            return self.brain.cycle();
        }
    }
    module.exports = Plant;
    It's great that plants can develop seeds, but to reproduce they must drop seeds onto the world that grow into copies of themselves. To keep it simple, I'll just have the plants drop copies of themselves, rather than seeds. Players may collect seeds by destroying plants that contain seeds. But the plants will consume seeds when they reproduce, and simply drop a plant somewhere in the world.

    Now comes a problem. Where is the plant in this world? Right now the plant has no idea where it is, or what size it is. So we need to add 3 parameters. The place, size, and a function for creating offspring. These 3 params should be synchronized memories. We should also create a feedback loop for DropSeed.

    Here's the updated Plant code:
    Code:
    function Plant(params) {
        var self = this;
        var memories;
        function optional(name, defaultValue) {
            return params[name] !== void 0 ? params[name] : defaultValue;
        }
        function synchronize() {
            self.brain.left.sync('memories.maxSeeds');
            self.brain.left.sync('memories.seedTime');
            self.brain.left.sync('memories.place');
            self.brain.left.sync('memories.size');
            self.brain.left.sync('memories.offspring');
        }
        function makeChild (x, y) {
            var childParams = Object.create(params);
            childParams.place = [x, y];
            return new Plant(childParams);
        }
        self.brain = new Brain();
        memories = self.brain.left.memories;
        memories.maxSeeds = optional('maxSeeds', 2);
        memories.seedTime = optional('seedTime', 60000);
        memories.place = optional('place', [0, 0]);
        memories.size = optional('size', [25, 25]);
        memories.offspring = optional('offspring', makeChild);
        synchronize();
        self.brain.createFeedback(DevelopSeed);
        self.brain.createFeedback(DropSeed);
        self.cycle = function () {
            synchronize();
            return self.brain.cycle();
        }
    }
    The offspring function may be overwritten by things that inherit Plant. That's useful if we don't necessarily want the offspring to be exact copies. Perhaps we want the offspring to be a bit different or allow for mutations? Anyway, for now we'll just use exact copies. Everything in the copied plant is the same except for the place. It would be silly to reproduce in the same exact place in the world, we want instead for the offspring to be somewhere next to it's parent. Before we get in too far over our heads, let's write some code for the DropSeed feedback loop.
    Code:
    function DropSeed(ai) {
        var self = this;
        var weight = 5;
        function drop () {
            var x = 0;
            var y = 0;
            ai.memories.seeds -= 1;
            ai.sync('memories.seeds');
            // tODO: Determine x, y for child
            return ai.memories.offspring(x, y)
        }
        self.cycle = function () {
            if (ai.memories.seeds > 0) {
                ai.addDecision({
                    weight: weight,
                    action: drop,
                    title: "drop seed"
                });
            }
        }
    }
    I mentioned before that when we drop a seed, we need to sync the seeds memory. I did that now before I forget to later. I also added a TODO to let me know I need to set x and y to something very different than 0, 0. So, we have some data to help us place a seed next to this plant. We have the place, and the size. But we should place the seed in a random direction- I'll choose one of eight different possibilities [n, nw, w, sw, s, se, e, ne].

    For simplicity, I'll just create an array with 8 pre-determined placements, then in the drop function I'll get a random number between 0-7, and use that number to pick the place out of the array.
    Code:
    function DropSeed(ai) {
        var self = this;
        var weight = 5;
        var place = ai.memories.place;
        var size = ai.memories.size;
        var seedPlaces = [
            [place[0] - size[0], place[1] - size[1]],
            [place[0] - size[0], place[1]],
            [place[0] - size[0], place[1] + size[1]],
            [place[0], place[1] - size[1]],
            //[place[0], place[1]], // Should not put child at same place as parent
            [place[0], place[1] + size[1]],
            [place[0] + size[0], place[1] - size[1]],
            [place[0] + size[0], place[1]],
            [place[0] + size[0], place[1] + size[1]]
        ];
        function drop () {
            var randPlace = seedPlaces[Math.floor(Math.random() * 8)];
            ai.memories.seeds -= 1;
            ai.sync('memories.seeds');
            return ai.memories.offspring(randPlace[0], randPlace[1]);
        }
        self.cycle = function () {
            if (ai.memories.seeds > 0) {
                ai.addDecision({
                    weight: weight,
                    action: drop,
                    title: "drop seed"
                });
            }
        }
    }
    Now that the DropSeed feedback loop is complete, let's test.
    Code:
    var Plant = require('./plant.js');
    var plants = [new Plant({
        maxSeeds: 5, 
        seedTime: 5000
    })];
    var interval = setInterval(function () {
        process.stdout.write('\033c');
        plants.forEach(function (plant) {
            var child = plant.cycle();
            if (child instanceof Plant) {
                plants.push(child);
            }
            console.log(plant.brain.left.memories.place);
        });
        console.log("Total plants:", plants.length);
    }, 1000);
    If we run this code, we get a whole bunch of plants rather quickly. Much of the plants are spawned in places where other plants already exist! But how to fix that?

    Well, we can either give the plants more knowledge of the world around them, or we can simply let the plant think it's reproducing, but prevent that from happening in the test code. I'm going to choose the later this time. So, I'll create another array containing serialized places where plants exist, and if the child plant is at a place that already exists, then it won't be added to the world.

    Here is the updated test, changes in bold:
    Code:
    var Plant = require('./plant.js');
    var plants = [new Plant({
        maxSeeds: 5, 
        seedTime: 5000,
        place: [500, 500],
        size: [25, 25]
    })];
    var plantPlaces = ['500,500'];
    function serializePlace(plant) {
        return plant.brain.left.memories.place[0] + ',' + plant.brain.left.memories.place[1];
    }
    var interval = setInterval(function () {
        process.stdout.write('\033c');
        plants.forEach(function (plant) {
            var child = plant.cycle();
            if (child instanceof Plant && 
                (plantPlaces.indexOf(serializePlace(child)) === -1)
            ) {
                plants.push(child);
                plantPlaces.push(serializePlace(child));
            }
            console.log(plant.brain.left.memories.place);
        });
        console.log("Total plants:", plants.length);
    }, 1000);
    Now when we test, we get a much more manageable outcome. But, since this test made the seedTime only 5 seconds, we still get a lot of plants after running this for only a few minutes...
    Code:
    [ 500, 500 ]
    [ 500, 475 ]
    [ 475, 500 ]
    [ 500, 525 ]
    [ 525, 525 ]
    [ 525, 475 ]
    [ 525, 450 ]
    [ 450, 500 ]
    [ 475, 475 ]
    ....
    [ 1025, 425 ]
    [ 1050, 500 ]
    [ 800, 25 ]
    [ 875, 25 ]
    [ 1025, 600 ]
    [ 275, 900 ]
    [ 1025, 650 ]
    [ 450, 975 ]
    Total plants: 1275
    The good news, is that none of the plants are in the same place, and we didn't have to loop to find that out.

    In later tutorials, we'll start saving plants to the database, and sending those plants to the client. But wait! We haven't even started on the client yet!

    Making games is hard- but I'll cover every bit of my journey in this tutorial. It will take a long time, so only the most committed will follow the entire thing.

    Previous (The AI)
    Table of Contents (Introduction)
    Next (The Client)
    Last edited by s-p-n; 29-01-16 at 09:50 PM.





Advertisement