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.
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?
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.