Ui Blocks-A lightweight alternate to containers

Phaser Containers Alternative

As a developer for a gaming company that has a lot of feedback from its users I understand that people can get very passionate about the features that they’re used to having. I’m afraid I was a victim of that passion about a week ago when I saw this notice from Richard Davey(creator of Phaser).

This is just a heads-up for those who track issues that support for nested Containers will be removed from a forthcoming version of Phaser (not 3.12, but probably 3.13).

The ability to create and use Containers will remain, but you will no longer be able to add a Container as a child of another. Source

I understand and support this decision, but it did leave me with a problem to solve.

Groups and Containers in Phaser 3

Coming from a background of flash I was used to creating MovieClips and adding other objects inside those clips.  I especially use this for user interface components. In Phaser 2 I use groups quite a bit to create UI.

To understand the problem, we need to understand a bit about groups and containers. Lately, I’ve been seeing a lot of confusion about groups and containers. It is especially confusing for those of us that are coming from Phaser 2. Groups in Phaser 3 do not work the same as in Phaser 2.

A common question I’ve seen is ” why can I not add a group to a group?”. Let’s have a look at what happens when you add a child to a container or group.

How Groups Work

Here is some simple code that adds 4 images to a scene. We then add 2 images to each of the group and the container.

create() {
        this.icon1 = this.add.image(100, 100, "icon1");
        this.icon2 = this.add.image(200, 200, "icon2");
        this.icon3 = this.add.image(300, 300, "icon3");
        this.icon4 = this.add.image(400, 400, "icon4");
        //
        //
        //
        this.con = this.add.container();
        this.group = this.add.group();
        //
        //
        this.con.add(this.icon1);
        this.con.add(this.icon2);
        //
        //
        this.group.add(this.icon3);
        this.group.add(this.icon4);
    }

Now if we log out the scene to the console this is the result

As you can see even though we have added the images, icon3 and 4 to the group, they are still on the display list of the scene. The group isn’t anywhere to be seen on the list at all! In Phaser 3 groups are used to organize game objects. You may group objects together for physics collisions, or simply to have an easy way to iterate through the children.  In Phaser 3  Sprites, images, texts are always children of the scene.  That is except for containers.

How Containers Work

Containers have their own display list. If you add a game object to a container it goes on the container’s list. Here is the output of the container.

As you can see, icon1 and icon2 are on the list of the container. So when you change something on the container, those changes will affect all the children on the list.

So what’s the problem?

Because we can add a container to a container and then that container can contain an infinite number of containers it begins to affect the performance of the code.  In the words of Richard Davey:

Containers can be nested. This means you can insert one Container inside another and branch children off of that. We do not recommend this as the deeper the chain goes, the more expensive every single look-up becomes, as each child traverses the tree back to the root every time it renders.

This is what apparently has caused enough problems to eliminate container nesting.

My Workaround

 I do not intend this to serve as a replacement for containers everywhere in every situation. However, a lot of developers I’ve talked to are using containers to build UI, and it is the only way that I am currently using containers.

As you can see from the output above phaser containers have bodies this means that physics can be applied to a container. They also have a lot of other things in there such as transform, angle and blend mode that I don’t need for my UI.  All I need to do is to find a set of elements that can be positioned relative to another set of coordinates. I’ve also added the functionality to set visibility or not. These features can be expanded but for now, these basics will suffice.

For example to create a text button I combine a text field and an image inside a container. say that I want to make a message box, that would require making another container. Then I would have to put the first container(the button) inside the second container. 

This solution solves that problem. It does have its limitations but it’s what I need and I hope that you find it helpful. 

The UIBlock Class

The UIBlock class leaves the children on the stage the same way that groups do. I also used a linked list instead of for next Loops to speed up the iteration through the children to update their positions. Everything else is just basic maths. 

class UIBlock {
    constructor() {
        //init private variables
        this._x = 0;
        this._y = 0;
        //
        //
        //keep track of this block's previous position
        this._oldX = 0;
        this._oldY = 0;
        //
        //
        this._visible = true;
        //
        //
        //needs to be set by developer
        this._displayWidth = 0;
        this._displayHeight = 0;
        //
        //
        //an array of the children
        this.children = [];
        //current child count
        //used for indexing
        this.childIndex = -1;
        //
        //used to identify this as a UIBlock to another UIBlock
        this.isPosBlock = true;
    }
    set x(val) {
        //record the current x into oldX
        this._oldX = this._x;
        //
        //update the value
        this._x = val;
        //
        //update the children
        this.updatePositions();
    }
    set y(val) {
        //record the current y into oldY
        this._oldY = this._y;
        //
        //update the value
        this._y = val;
        //update the children
        this.updatePositions();
    }
    //getters
    get x() {
        return this._x;
    }
    get y() {
        return this._y;
    }
    //add a child
    add(child) {
        //up the index
        this.childIndex++;
        //make a note of the index inside the child
        child.childIndex = this.childIndex;
        //add to the array
        this.children.push(child);
        //build the linked list
        this.buildList();
    }
    removeChild(child) {
        //take the child off the array based on index
        this.children.splice(child.childIndex, 1);
        //
        //rebuild the linked list
        this.buildList();
        //rebuild the indexes
        var len = this.children.length;
        for (var i = 0; i < len; i++) {
            this.children[i].childIndex = i;
        }
        //set the childIndex to the length of the array
        this.childIndex = len;
    }
    buildList() {
        var len = this.children.length;
        if (len > 1) {
            for (var i = 1; i < len; i++) {
                //set the current child to the previous child's nextChild property
                this.children[i - 1].nextChild = this.children[i];
            }
        }
        this.children[len - 1].nextChild = null;
    }
    get displayWidth() {
        return this._displayWidth;
    }
    get displayHeight() {
        return this._displayHeight;
    }
    setSize(w, h) {
        this._displayWidth = w;
        this._displayHeight = h;
    }
    setXY(x, y) {
        this.x = x;
        this.y = y;
        this.updatePositions();
    }
    set visible(val) {
        if (this._visible != val) {
            this._visible = val;
            if (this.children.length > 0) {
                //send the first child to the updateChildVisible function
                this.updateChildVisible(this.children[0], val);
            }
        }
    }
    get visible() {
        return this._visible;
    }
    updateChildVisible(child, vis) {
        child.visible = vis;
        if (child.isPosBlock == true) {
            child.visible = vis;
        }
        if (child.nextChild != null) {
            //if the child has a nextChild call this function recursively 
            this.updateChildVisible(child.nextChild, vis);
        }
    }
    updateChildPos(child) {
        child.y = child.y - this._oldY + this._y;
        child.x = child.x - this._oldX + this._x;
        if (child.isPosBlock == true) {
            child.updatePositions();
        }
        if (child.nextChild != null) {
            //if the child has a nextChild call this function recursively 
            this.updateChildPos(child.nextChild);
        }
        //set the old values to the new
        this._oldX = this._x;
        this._oldY = this._y;
    }
    updatePositions() {
        if (this.children.length > 0) {
            //send the first child to the updateChildPos function
            this.updateChildPos(this.children[0]);
        }
    }
    getRelPos(child) {
        return {
            x: child.x - this.x,
            y: child.y - this.y
        }
    }
    
}

Usage

My preferred way of using this is to extend a class

class TextButton extends UIBlock {
    constructor(config) {
        super(config.scene);
        //add children
        
        }
}

var textButton=new TextButton({scene:this});

However, you can also use it this way

var back = this.add.image(0, 0, "box");
var back = this.add.image(0, 0, "box");
var button = this.add.image(0, 50, "button");
var text=this.add.text(0,-30,"Message Here",{color:'#ff0000'}).setOrigin(0.5,0.5);

block=new UIBlock();

block.add(back);
block.add(button);
block.add(text);

block.x=240;
block.y=300;

Result:

Limitations

These are the main features missing from the UIBlock:

  • physics
  • alpha
  • scaling
  • rotation

Alpha can easily be added by copying and editing the functions associated with visibility. I suggest scaling the children individually. It is possible that scaling can be implemented at a later time. Since this is being primarily used for UI, physics isn’t really needed. I  am happy to say that tweening will work as long as you stick to the properties that are in UIBlock, such as x and y.  I’ll most likely keep tweaking this to my needs and I feel that there is a lot to be added, but I wanted to release this for any other developers who find themselves in need of it.

 

 

 

 

2 thoughts on “Ui Blocks-A lightweight alternate to containers”

  1. What if I need to know parentContainer.x and parentContainer.y. What would be the best solution with UIBlocks?
    At some point I need to take out element out of the container/uiBlock and make it live his own life. For this I use parentcontainer.x and parentcontainer.y to calculate where to put the element. How this would be achieved with UI blocks?
    Should I store UIBlock property in some way as parentContainer is stored?

    1. In this code, the parent keeps track of the children, but you can easily pass the parent (this) when you add the child if you need that information.

      You can use removeChild(child) to take the child out of the parent.

Leave a Comment