Tutorial
Heading
Feb 23, 2022

Canvas (2D UI) Manager in Decentraland

How to create different "Pop-Ups" in Decentraland using a single Canvas

The mission

How to create different "Pop-Ups" in Decentraland using a single Canvas. This involves developing a system that enables easy control over, preventing them overlap or imbalance.

Resources

Set up the Decentraland scene

We'll asume that you have some basic knowledge of creating a basic Decentraland game in TypeScript and understanding how its engine and entities function. If you need more information about Decentraland, please refer to the following documentation.

We will use the basic Decentraland scene as an example and add cubes to it as launchers for our interface.

Box as activator for the UI

// create the entity
const cubeClic = new Entity()
  
// add a transform to the entity
cubeClic.addComponent(new Transform({ position: new Vector3(1, 1, 1) }))
  
// add a shape to the entity
cubeClic.addComponent(new BoxShape())
  
// add the entity to the engine
engine.addEntity(cubeClic)


// add click component to the box
cubeClick.addComponent(
    new OnPointerDown(
      (e) => {
        //We will insert here the function "Show UI POP-UP"
        log("Box clicked", e)
      },
      {
        button: ActionButton.PRIMARY,
        showFeedback: true,
        hoverText: "Open UI",
      }
    )
  )

Now that we have an activator, we can create our canvas. To demonstrate one of the benefits of having a Manager, we will display a simple welcome pop-up that we will hide. Then, when clicking on the box, we will show a second pop-up that will create a monitoring interface. When it reaches 10, it will hide and show us another pop-up indicating completion. For all of this, we will only use a single canvas, allowing us to conserve resources and control the flow of these messages in a simple manner.

Create the HUD Manager

HUD class for initialize all UI classes and control the canvas:

import { UIMessagge as MessageTest, UICounter} from "./uiTest"

var hud: HUD 

//Create 1 HUD,  
export function getHUD(){
    if (!hud) {
      spawnHUD()
    }
    return hud
}

function spawnHUD(){
  hud = new HUD()
}
//Declare all UIs we will use here
export class HUD {  
  canvas: UICanvas
  uiCounter: UICounter
  messageTest: MessageTest

  constructor(){
    this.canvas = new UICanvas()
    this.canvas.visible = true
    this.uiCounter = new UICounter (this.canvas)
    this.messageTest= new MessageTest (this.canvas)
  }

  hideAll(){
      this.canvas.visible=false
  }
}

Add these lines to the beginning of game.ts:

import { getHUD, HUD } from "./hud"

//Init HUD manager
const hud_test= getHUD()

Add the “show” function to the OnPointerDown component:

(e) => {
        //We will insert here the function "Show UI POP-UP"
        hud_test.messageTest.show(true)
        log("Box clicked", e)
      },
      

Create custom messege UI

A simple pop-up with two buttons:

export class UIMessagge {
  imageBackground: UIImage;
  container: UIContainerRect;
  textBody: UIText;
  textComfirm: UIText;
  textCancel: UIText;

  confirmButton: UIImage;
  cancelButton: UIImage;

  //Callback to use when user presses confirm function
  confirmFunction: Function = () => {};
  cancelFunction: Function = () => {};

  constructor(parentUI: UIShape) {
    var parent: UIShape;
    parent = parentUI as UIShape;
    this.container = new UIContainerRect(parent);
    this.container.visible = false;
    this.container.vAlign = "center";
    this.container.hAlign = "center";
    this.container.width = 2012*0.25
    this.container.height = 2354*0.10
    this.container.positionX = "0%";
    this.container.positionY = "0%";
    this.container.color = new Color4(0, 0, 0, 0);
    this.container.isPointerBlocker = true;

    //Background Long
    this.imageBackground = new UIImage(this.container, new Texture("assets/panel-large.png"));
    this.imageBackground.name = "textBox";
    this.imageBackground.sourceWidth = 2012;
    this.imageBackground.sourceHeight = 2354;
    this.imageBackground.width = 2012 * 0.25;
    this.imageBackground.height = 2354 * 0.10;
    this.imageBackground.vAlign = "center";
    this.imageBackground.hAlign = "center";
    this.imageBackground.positionX = "0%";
    this.imageBackground.positionY = 0;
    this.imageBackground.visible = true;

    //Text Body
    this.textBody = new UIText(this.container);
    this.textBody.textWrapping = true;
    this.textBody.width = 320;
    this.textBody.value ="Welcome to the best tutorial for UI Canvas in DECENTRALAND!\n\nChoose if you want to show counter UI or not";
    this.textBody.hAlign = "center";
    this.textBody.vAlign = "center";
    this.textBody.hTextAlign = "left";
    this.textBody.positionX = 0;
    this.textBody.positionY = 20;
    this.textBody.color = Color4.Black();
    this.textBody.fontSize = 14;
    this.textBody.visible = true;
    this.textBody.font = new Font(Fonts.LiberationSans);

    const confirmButtonImage = new Texture("assets/UI_Button_Prev.png");
    this.confirmButton = new UIImage(this.container, confirmButtonImage);
    this.confirmButton.name = "confirmButton";
    this.confirmButton.sourceWidth = 427;
    this.confirmButton.sourceHeight = 157;
    this.confirmButton.width = 106;
    this.confirmButton.height = 39;
    this.confirmButton.hAlign = "center";
    this.confirmButton.vAlign = "center";
    this.confirmButton.positionX = -100;
    this.confirmButton.positionY = -60;
    this.confirmButton.isPointerBlocker = true;
    this.confirmButton.visible = true;
    this.confirmButton.onClick = new OnPointerDown(() => {
      //Void Confirm Function, we will set this function at game.ts
      this.confirmFunction();
      this.show(false);
    });

    const cancelButtonImage = new Texture("assets/UI_Button_Next.png");
    this.cancelButton = new UIImage(this.container, cancelButtonImage);
    this.cancelButton.name = "cancelbutton";
    this.confirmButton.sourceWidth = 427;
    this.confirmButton.sourceHeight = 157;
    this.confirmButton.width = 106;
    this.confirmButton.height = 39;
    this.cancelButton.hAlign = "center";
    this.cancelButton.vAlign = "center";
    this.cancelButton.positionX = 100;
    this.cancelButton.positionY = -60;
    this.cancelButton.isPointerBlocker = true;
    this.cancelButton.visible = true;
    this.cancelButton.onClick = new OnPointerDown(() => {
			//Void Cancel Function, we will set these function at game.ts
      this.cancelFunction();
      this.show(false);
    });

    //Text Button Cancel
    this.textCancel = new UIText(this.container);
    this.textCancel.value = "Yes!";
    this.textCancel.hAlign = "center";
    this.textCancel.vAlign = "center";
    this.textCancel.hTextAlign = "left";
    this.textCancel.positionX = -60; 
    this.textCancel.positionY = -45; 
    this.textCancel.color = Color4.Black();
    this.textCancel.fontSize = 15;
    this.textCancel.visible = true;
    this.textCancel.font = new Font(Fonts.LiberationSans);
    this.textCancel.isPointerBlocker= false

    //Text Button Comfirm
    this.textComfirm = new UIText(this.container);
    this.textComfirm.value = "No!";
    this.textComfirm.hAlign = "center";
    this.textComfirm.vAlign = "center";
    this.textComfirm.hTextAlign = "left";
    this.textComfirm.positionX = 140;
    this.textComfirm.positionY = -45;
    this.textComfirm.color = Color4.Black();
    this.textComfirm.fontSize = 15;
    this.textComfirm.visible = true;
    this.textComfirm.font = new Font(Fonts.LiberationSans);
    this.textComfirm.isPointerBlocker= false
  }

  //Functions:
  show(bVisible: boolean) {
    this.container.visible = bVisible;
  }
	//Change Text:
  setBodyText(text: string) {
    this.textBody.value = text;
  }

}

Create custom counter UI

Simple pop-up with a counter:

export class UICounter {
  container: UIContainerRect;
  textBody: UIText;
  textCounter: UIText;

  countValue:number = 0

  constructor(parentUI: UIShape) {
    var parent: UIShape;
    parent = parentUI as UIShape;
    //Container DeCentraland, propieadades
    this.container = new UIContainerRect(parent);
    this.container.visible = false;
    this.container.vAlign = "center";
    this.container.hAlign = "center";
    this.container.width = 413 * 0.6;
    this.container.height = 215 * 0.6;
    this.container.positionX = "41%";
    this.container.positionY = "38%";
    this.container.isPointerBlocker = true;
    //container.color = Color4.Gray()

    //Background
    const backgroundUI = new Texture("assets/panel-large.png");
    const backgroundUIimage = new UIImage(this.container, backgroundUI);
    backgroundUIimage.name = "UI_Bubble_TC.png";
    backgroundUIimage.sourceWidth = 826;
    backgroundUIimage.sourceHeight = 528;
    backgroundUIimage.width = 413 * 0.6;
    backgroundUIimage.height = 252 * 0.6;
    backgroundUIimage.hAlign = "center";
    backgroundUIimage.vAlign = "center";
    backgroundUIimage.positionX = 0;
    backgroundUIimage.positionY = 0;
    backgroundUIimage.isPointerBlocker = false;

    //Text Timer Quest
    this.textBody = new UIText(this.container);
    this.textBody.value = "Number of clics on Red Box:";
    this.textBody.hAlign = "center";
    this.textBody.vAlign = "center";
    this.textBody.hTextAlign = "center";
    this.textBody.positionX = 0;
    this.textBody.positionY = 30;
    this.textBody.color = Color4.White();
    this.textBody.fontSize = 14;
    this.textBody.visible = true;
    this.textBody.font = new Font(Fonts.LiberationSans);

    //Text Timer Quest
    this.textCounter = new UIText(this.container);
    this.textCounter.value = "0";
    this.textCounter.hAlign = "center";
    this.textCounter.vAlign = "center";
    this.textCounter.hTextAlign = "center";
    this.textCounter.positionX = 0;
    this.textCounter.positionY = 0;
    this.textCounter.color = Color4.White();
    this.textCounter.fontSize = 16;
    this.textCounter.visible = true;
    this.textCounter.font = new Font(Fonts.LiberationSans);
  }
  //Functions
  show(bVisible: boolean) {
    this.container.visible = bVisible;
    //Reset if show false
    if (bVisible==false){
      this.countValue=0
      this.textCounter.value= this.countValue.toString()
    }
  }

  //Inc Counter
  incCount(value?:number){
    if (value) {
      //Inc a value if it exists 
      this.textCounter.value= (this.countValue+value).toString()
    }else{
      //Inc 1 without value
      this.countValue++
      this.textCounter.value=this.countValue.toString()
    }
  }
  
}

Set functions for UI

To complete the process, we need to populate the empty functions that we created in our two UIs. Through the HUD Manager, we’ll have greater control over their status:

//Set confirmFunction to show counter
hud_test.messageTest.confirmFunction=()=>{
  hud_test.uiCounter.show(true);
  log("Clic Yes!");
}
//Set cancelFunction to show counter
hud_test.messageTest.cancelFunction=()=>{
  log("Clic No!");
  hud_test.uiCounter.show(false)
}

We will also require a second cube as an actuator for our counter:

//Material Red
const mRed = new Material()
mRed.albedoColor = Color3.Red()

//Create a new cube for inc Count
const cubeIncCount = new Entity();
cubeIncCount.addComponent(new Transform({ position: new Vector3(6, 1, 6) }));
cubeIncCount.addComponent(new BoxShape());
cubeIncCount.addComponent(mRed)
engine.addEntity(cubeIncCount);
cubeIncCount.addComponent(
  new OnPointerDown(
    () => {
      hud_test.uiCounter.incCount()
      log("Counter Box clicked");
    },
    {
      button: ActionButton.PRIMARY,
      showFeedback: true,
      hoverText: "Inc counter UI",
    }
  )
);

As a result of all this, we will achieve a simple UI that displays a message asking whether or not we want to show a counter. All of this will be managed  from a single canvas.

Results

As a result, we have obtained 4 different UI states:

Status 0
Status 1

Status 2
Status 3

Status 0 again

Conclusion

To maintain order among the different UIs that may appear in a scene and to display them through a single canvas, it is necessary to create an intermediate class, such as the HUD class in our case.

Moreover, this class will enable us to interact with all of them simultaneously or independently, offering several advantages when developing a complex UI.

Code
Decentraland
Lianir
Coder
Tutorial
How to import Decentraland SkyboxEditor into Unity
Tutorial
Doing a MANA transaction in a Decentraland scene
Tutorial
Canvas (2D UI) Manager in Decentraland