Impact

This forum is read only and just serves as an archive. If you have any questions, please post them on github.com/phoboslab/impact

1 decade ago by ape

I'm having some trouble implementing the classic flyweight design pattern (or a variation of that pattern).

Basically here's what I'm trying to do, generally speaking: rather than spawn 100 entities, I spawn 1 entity (really, an "entity model"). On each game update I want to adjust a property of that entity 100 times. (In reality, those numbers are much greater , and in most cases, unknown).

In theory, I should be able to spawn 1 entity with its intrinsic properties (like entity.size, entity.type, etc), then update a pool of objects that point to that base entity, each with their own extrinsic properties (like position, animation, etc).

I'll show some code, then describe what I'm seeing:

ig.module(
  'game.entities.tree'
)
.requires(
  'impact.entity'
)
.defines(function(){
  /*
    FlyweightTree supplies the intrinsic properties, or
    properties each type of tree will possess. There
    are four possible types of tree, each type having
    a combination of a name and color. Like "Oak" and "yellow".

    We also want each the default properties of an ig.Entity
    which is why we extend the Entity module.
  */

  FlyweightTree = ig.Entity.extend({
    size: {x: 16, y:16},
    animSheet: new ig.AnimationSheet( 'media/tree.png', 16, 16),

    init: function(x, y, settings) {
      this.parent(x, y, settings);

      /*
        Add 16 possible animations - 4 colors, each with
        a frame showing a single digit between 1 and 4
      */

      var lc     = 0;
      var cell   = 0;
      var colors = ["green", "red", "yellow", "blue"];

      for (var c = 0; c < colors.length; c++) {
        color = colors[c];
        for (lc = 0; lc < 4; lc++) {
          this.addAnim(color + '_' + (lc+1), 1, [cell]);
          cell += 1;
        };
      };

      this.name  = settings.name;
      this.color = settings.color;
    }
  });

  /*
    The factory is a singleton object that allows us to
    either create a FlyweightTree object if one doesn't exist,
    or find the existing one.

  */
  var FlyweightFactory = (function() {
    var flyweights = {};
    return {
      get: function(name, color) {
        // check to see if we have the object in the pool yet
        if (!flyweights[name + color]) {
          // if we're spawning a new object, give it a random position, just for fun
          var randX = Math.floor(Math.random() * (ig.system.width));
          var randY = Math.floor(Math.random() * (ig.system.height));

          // add it to the pool of FlyweightTree objects
          flyweights[name+color] = ig.game.spawnEntity(FlyweightTree, randX, randY, {name: name, color: color});
        }

        // return the object from the pool
        return flyweights[name+color];
      },

      getCount: function() {
        // a helper that'll make iterating over the pool of FlyweightTree's easier
        var count = 0;
        for (var f in flyweights) count++;
        return count;
      }
    }
  })();

  /*
    The TreeCollection manages our pool of Tree objects. It's the main
    interface to our Tree objects for the game.
  */
  TreeCollection = function() {
    var trees = {};
    var count = 0;
    return {
      add: function(name, color, leaves, uid, pos) {
        trees[uid] = new Tree(name, color, leaves, uid, pos);
        count++;
      },
      get: function(uid) {
        return trees[uid];
      },
      getCount: function() {
        return count;
      }
    }
  };

  /*
    A Tree object is a combination of the FlyweightTree object, which
    contains the shared, or instrinsic, details of our tree, as well
    as the unique, or extrinsic, details of our tree.

    So while the tree might be a yellow Oak tree, it also has a unique
    position, and a unique number of leaves, and a unique ID.
  */


  var Tree = function(name, color, leaves, uid, pos) {
    this.flyweight = FlyweightFactory.get(name, color); // establish a reference to our FlyweightTree
    this.name      = this.flyweight.name;               // shortcuts to the name and color
    this.color     = this.flyweight.color;
    this.leaves    = leaves;                            // apply unique properties
    this.uid       = uid;
    this.pos       = pos;

    /*
      tree#update will be called by the main main#update.
      In theory, it should apply any new properties, then draw our entity
      using the anim for this tree.
    */
    this.update = function() {
      var anim = this.flyweight.anims[this.color + '_' + this.leaves];

      this.flyweight.currentAnim = anim;
      this.flyweight.pos         = this.pos;
      this.flyweight.update();
    }

    this.draw = function() {
      this.flyweight.draw();
    }
  }

  ig.global.TreeCollection = TreeCollection;
});


In my game.init I build a collection of Trees like so:

this.trees = new TreeCollection();

var randX = function() {return Math.floor(Math.random() * (ig.system.width))};
var randY = function() {return Math.floor(Math.random() * (ig.system.height))};

this.trees.add("Oak", "yellow", 1, 1, {x: randX(), y: randY()});
this.trees.add("Pine", "green", 2, 2, {x: randX(), y: randY()});
this.trees.add("Oak", "yellow", 3, 3, {x: randX(), y: randY()});
this.trees.add("Pine", "green", 4, 4, {x: randX(), y: randY()});

Then in my game.update I do this:

for (var i = 0; i < this.trees.getCount(); i++) {
  this.trees.get(i+1).update();
};

And in game.draw :

this.parent();
for (var i = 0; i < this.trees.getCount(); i++) {
  this.trees.get(i+1).draw();
};

When run, what I see are the animations for "Oak", "Yellow", with 3 leaves, and "Pine", "green", with 4 leaves. Essentially the last of each FlyweightTree object.

Any tips?

1 decade ago by lTyl

When run, what I see are the animations for "Oak", "Yellow", with 3 leaves, and "Pine", "green", with 4 leaves. Essentially the last of each FlyweightTree object.

Any tips?


        // return the object from the pool
        return flyweights[name+color];

Instead of return name + color, have each property added to the tree collection pool have it's own identifier key:

this.trees.add("YellowOak1Leaf", "Oak", "yellow", 1, 1, {x: randX(), y: randY()});

You are doing this already, but the identifier is not unique, so you are only getting the first result JS sees sent to you. In your example, you have 4 properties added, but only two identifiers (name + color: "OakYellow" and "PineGreen") - so you are only get two responses.

1 decade ago by ape

@ITyl Thanks for responding. The problem with giving each of the trees a unique identifier the way you describe is that it sidesteps the Flyweight pattern.

For example, this creates 2 Entity objects, when I really only want 1:

this.trees.add("YellowOak1Leaf", "Oak", "yellow", 1, 1, {x: randX(), y: randY()});
this.trees.add("YellowOak2Leaf", "Oak", "yellow", 1, 1, {x: randX(), y: randY()});

This creates only 1 Entity object, but multiple trees in the TreeCollection object:

this.trees.add("Oak", "yellow", 1, some_unique_id1, {x: randX(), y: randY()});
this.trees.add("Oak", "yellow", 2, some_unique_id2, {x: randX(), y: randY()});

The idea is to then do an update and a draw for each of the trees in the TreeCollection - those trees aren't actually Entity objects - they're unique instances that each refer to a single Entity object.

I've being following along with a debugger and it's clear to me that the Entity's update and draw functions are being called for each of the TreeCollection objects - but I'm losing track of where and why only two frames are drawing.

1 decade ago by ape

I figured it out.

In the main update I was updating each tree object in the tree collection, one after another.

Then I was doing the same thing in the main draw method.

The main draw method happens after the main update method, so only the last of the updates I did on the list of tree objects "counted".

The fix was dead simple:

for (var i = 0; i < this.trees.getCount(); i++) {
  this.trees.get(i+1).update();
  this.trees.get(i+1).draw();
};

Works like a charm. Now it's time to see if I can really get the performance gains I'm hoping for using the pattern.

1 decade ago by ape

I'll throw together a more organized test shortly, but the first results are promising. No scaling, a canvas size of 640x640, tile size of 16x16.

On a fairly fast machine (latest Macbook Pro) in Chrome I'm moving 5,000 "trees" a few random pixels per update and getting 30fps. At 10,000 trees, I'm at around 20fps. At 20,000 trees, it starts to crawl at 7fps.

4 trees shows a heap snapshot of 2.3MB in size.
10,000 tree shows a heap snapshot of 4.0MB.

I suspect if I were just using entities, I'd be lucky if I could even spawn 10,000 entities, let alone be able to take a heap snapshot.

1 decade ago by lTyl

Looks promising and glad you got it working!
Page 1 of 1
« first « previous next › last »