How to use plugins in Phaser with Typescript

Sure!
This is my scene class.

// You can write more code here

/* START OF COMPILED CODE */

import Phaser, { Time } from "phaser";
import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin.js";
import {
  CustomShapes,
  Dialog,
  Space,
} from "phaser3-rex-plugins/templates/ui/ui-components";

const COLOR_PRIMARY = 0x4e342e;
const COLOR_LIGHT = 0x7b5e57;
const COLOR_DARK = 0x260e04;

var content = `Phaser is a fast, free, and fun open source HTML5 game framework that offers WebGL and Canvas rendering across desktop and mobile web browsers. Games can be compiled to iOS, Android and native apps by using 3rd party tools. You can use JavaScript or TypeScript for development.`;

const GetValue = Phaser.Utils.Objects.GetValue;
var createTextBox = function (scene: any, x: any, y: any, config: any) {
  var wrapWidth = GetValue(config, "wrapWidth", 0);
  var fixedWidth = GetValue(config, "fixedWidth", 0);
  var fixedHeight = GetValue(config, "fixedHeight", 0);
  var textBox = scene.rexUI.add
    .textBox({
      x: x,
      y: y,
      rtl: true,
      background: scene.rexUI.add
        .roundRectangle(0, 0, 2, 2, 20, COLOR_PRIMARY)
        .setStrokeStyle(2, COLOR_LIGHT),

      icon: scene.rexUI.add.roundRectangle(0, 0, 2, 2, 20, COLOR_DARK),

      // text: getBuiltInText(scene, wrapWidth, fixedWidth, fixedHeight),
      text: getBBcodeText(scene, wrapWidth, fixedWidth, fixedHeight),

      action: scene.add
        .image(0, 0, "nextPage")
        .setTint(COLOR_LIGHT)
        .setVisible(false),

      space: {
        left: 20,
        right: 20,
        top: 20,
        bottom: 20,
        icon: 10,
        text: 10,
      },
    })
    .setOrigin(0)
    .layout();

  textBox
    .setInteractive()
    .on(
      "pointerdown",
      function () {
        var icon = textBox.getElement("action").setVisible(false);
        textBox.resetChildVisibleState(icon);
        if (textBox.isTyping) {
          //icon.stop(true);
          textBox.stop(true);
        } 
        // else if (textBox.isLastPage){
        //   textBox.destroy();
        // }
        else {
          textBox.typeNextPage();
        }
      },
      textBox
    )
    .on(
      "pageend",
      function () {
        if (textBox.isLastPage) {
          //textBox.setVisible(false);
          textBox.destroy();
          return;
        }

        var icon = textBox.getElement("action").setVisible(true);
        textBox.resetChildVisibleState(icon);
        icon.y -= 30;
        var tween = scene.tweens.add({
          targets: icon,
          y: "+=30", // '+=100'
          ease: "Bounce", // 'Cubic', 'Elastic', 'Bounce', 'Back'
          duration: 500,
          repeat: 0, // -1: infinity
          yoyo: false,
        });
      },
      textBox
    );
  //.on('type', function () {
  //})

  return textBox;
};

var getBBcodeText = function (
  scene: any,
  wrapWidth: any,
  fixedWidth: any,
  fixedHeight: any
) {
  return scene.rexUI.add.BBCodeText(0, 0, "", {
    fixedWidth: fixedWidth,
    fixedHeight: fixedHeight,
    rtl: true,
    fontFamily: "Almarai",
    fontSize: "28px",
    wrap: {
      mode: "word",
      width: wrapWidth,
    },
    maxLines: 3,
  });
};

export default class Lab extends Phaser.Scene {
  rexUI!: UIPlugin;
  constructor() {
    super("Lab");
    /* START-USER-CTR-CODE */
    // Write your code here.
    /* END-USER-CTR-CODE */
  }

  editorCreate(): void {
    // bG_challenge1
    this.add.image(960, 540, "BG_challenge1");

    this.events.emit("scene-awake");
  }

  /* START-USER-CODE */

  // Write your code here

  preload() {
    //this.load.html("nameform", "assets/loginForm.html");
  }

  create() {
    this.editorCreate();

    var content = `يجب أن أنتهي من جمع هذا الروبوت بأسرع وقت! هناك اثنا عشرة قطعة متناثرة في هذا المختبر، و يجب عليّ إيجادها بأسرع وقت.`;

    this.rexUI.add
      .sizer({
        x: 920,
        y: 800,
        width: 1280,
        height: 720,
        orientation: "x",
        rtl: true,
        space: { left: 10, right: 10, top: 10, bottom: 25, item: 10 },
      })
      // .addBackground(
      //   new SpeechBubble(this, COLOR_PRIMARY, 0xffffff)
      //   //this.add.image(960, 540, "Image from iOS")
      // )
      .add(
        this.add.image(960, 540, "scientist"),
        {
          proportion: 1,
          align: "right-bottom",
        }
        // this.rexUI.add.roundRectangle(0, 0, 0, 0, 20, COLOR_LIGHT),
        // {
        //     proportion: 0,
        //     align: 'bottom'
        // }
      )
      .add(    createTextBox(this, 100, 400, {
        wrapWidth: 500,
        fixedWidth: 500,
        fixedHeight: 65,
      }).start(content, 50))
      // .add(
      //   this.rexUI.wrapExpandText(
      //     this.add.text(0, 0, content, {
      //       fontFamily: "Cairo",
      //       fontSize: "32px",
      //       color: "#000000",
      //       rtl: true,
      //       testString: "يجب أن أنتهي من جمع هذا الروبوت بأسرع وقت!",
      //     })
      //   ),
      //   {
      //     proportion: 1,
      //     align: "center",
      //     //expand: true
      //   }
      // )
      // .add(
      //     this.rexUI.add.roundRectangle(0, 0, 0, 0, 20, COLOR_LIGHT),
      //     {
      //         proportion: 0,
      //         align: 'bottom'
      //     }
      // )
      .layout();
    //.drawBounds(this.add.graphics(), 0xff0000);
  }

  /* END-USER-CODE */
}

/* END OF COMPILED CODE */

// You can write more code here

This is my game config

import Phaser from "phaser";
import preloadPackUrl from "../static/assets/preload-asset-pack.json";
import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin.js";
import Level from "./scenes/Level";
import Preload from "./scenes/Preload";
import Lab from "./scenes/Lab";

class Boot extends Phaser.Scene {
  constructor() {
    super("Boot");
  }

  preload() {
    this.load.pack("pack", preloadPackUrl);
  }

  create() {
    this.scene.start("Preload");

  }
}

const game = new Phaser.Game({
  width: 1920,
  height: 1080,
  backgroundColor: "#2f2f2f",
  parent: "game",
  scale: {
    mode: Phaser.Scale.ScaleModes.FIT,
    autoCenter: Phaser.Scale.Center.CENTER_BOTH,
  },
  dom: {
    createContainer: true,
  },
  scene: [Boot, Preload, Level, Lab],
  plugins: {
    scene: [
      {
        key: "rexUI",
        plugin: UIPlugin,
        mapping: "rexUI",
      },
    ],
  },
});

game.scene.start("Boot");

Under ‘pointerdown’ event:

            if (textBox.isTyping) {
                textBox.stop(true);
            }
            else if (textBox.isLastPage) {
                textBox.destroy();
            }
            else {
                textBox.typeNextPage();
            }

It will destroy textbox if

  • textbox is not typing, and
  • textbox is shown at last page

Thus, clicking textbox when typing, it will

  1. show all lines of current page
  2. clicking again, then
    • show next page if not at last page
    • destroy if at last page

Remember that removing textbox.destroy() under ‘pageend’ event.

That’s exactly what I had but it isn’t working.
It only shows one page of Arabic text then immediately destroys it when clicked.

Sorry for all the trouble!

How does Sizer controls the position of its elements? I tried changing the position of each element to make them fit together but I couldn’t.
I even tried adding “Space” but that didn’t work.

I want the text box to be a little bit down (almost aligned the center of the character) and I want the text and the arrow icon to be inside the box.

this.conversation = this.rexUI.add
      .sizer({
        x: 920,
        y: 800,
        width: 1000,
        height: 600,
        orientation: "x",
        rtl: true,
        space: { left: 10, right: 10, top: 10, bottom: 25, item: 10 },
      })
      .add(
        this.add.image(960, 540, "Character_Scientist"),
        {
          proportion: 1,
          align: "right-bottom",
        }
      )
      .add(
        createTextBox(this, 800, 1000, {
          wrapWidth: 750,
          fixedWidth: 1096,
          fixedHeight: 332,
        }).start(content, 50),
        {
          align: "center-center",
        }
      )
      .layout()
      .drawBounds(this.add.graphics(), 0xff0000);
const GetValue = Phaser.Utils.Objects.GetValue;
var createTextBox = function (scene: any, x: any, y: any, config: any) {
  var wrapWidth = GetValue(config, "wrapWidth", 0);
  var fixedWidth = GetValue(config, "fixedWidth", 0);
  var fixedHeight = GetValue(config, "fixedHeight", 0);
  var textBox = scene.rexUI.add
    .textBox({
      x: x,
      y: y,
      rtl: true,
      background: scene.add.image(0, 0, "UI_textbox"),
      // background: scene.rexUI.add
      //   .roundRectangle(0, 0, 2, 2, 20, COLOR_PRIMARY)
      //   .setStrokeStyle(2, COLOR_LIGHT),

      //icon: scene.rexUI.add.roundRectangle(0, 0, 2, 2, 20, COLOR_DARK),

      // text: getBuiltInText(scene, wrapWidth, fixedWidth, fixedHeight),
      text: getBBcodeText(scene, wrapWidth, fixedWidth, fixedHeight),

      action: scene.add
        .image(0, 0, "UI_textbox_next")
        //.setTint(COLOR_LIGHT)
        .setVisible(false),

      space: {
        left: 20,
        right: 20,
        top: 20,
        bottom: 20,
        icon: 10,
        text: 10,
      },
    })
    .setOrigin(0)
    .layout();

  textBox
    .setInteractive()
    .on(
      "pointerdown",
      function () {
        var icon = textBox.getElement("action").setVisible(false);
        textBox.resetChildVisibleState(icon);
        if (textBox.isTyping) {
          //icon.stop(true);
          textBox.stop(true);
        } else if (textBox.isLastPage) {
          done = true;
          textBox.destroy();
        } else {
          textBox.typeNextPage();
        }
      },
      textBox
    )
    .on(
      "pageend",
      function () {
        if (textBox.isLastPage) {
          //textBox.setVisible(false);
          //textBox.destroy();
          return;
        }

        var icon = textBox.getElement("action").setVisible(true);
        textBox.resetChildVisibleState(icon);
        icon.y -= 30;
        var tween = scene.tweens.add({
          targets: icon,
          y: "+=30", // '+=100'
          ease: "Bounce", // 'Cubic', 'Elastic', 'Bounce', 'Back'
          duration: 500,
          repeat: 0, // -1: infinity
          yoyo: false,
        });
      },
      textBox
    );
  //.on('type', function () {
  //})

  return textBox;
};

var getBBcodeText = function (
  scene: any,
  wrapWidth: any,
  fixedWidth: any,
  fixedHeight: any
) {
  return scene.rexUI.add.BBCodeText(0, 0, "", {
    fixedWidth: fixedWidth,
    fixedHeight: fixedHeight,
    rtl: true,
    fontFamily: "Almarai",
    fontSize: "28px",
    color: "fff000",
    wrap: {
      mode: "word",
      width: wrapWidth,
    },
    maxLines: 2,
  });
};

Please add a simplest, runnable test code for this case.

const COLOR_PRIMARY = 0x4e342e;
const COLOR_LIGHT = 0x7b5e57;
const COLOR_DARK = 0x260e04;

class Demo extends Phaser.Scene {
    constructor() {
        super({
            key: 'examples'
        })
    }

    preload() { 
        this.load.scenePlugin({
            key: 'rexuiplugin',
            url: 'https://raw.githubusercontent.com/rexrainbow/phaser3-rex-notes/master/dist/rexuiplugin.min.js',
            sceneKey: 'rexUI'
        });
 
        this.load.image('nextPage', 'https://raw.githubusercontent.com/rexrainbow/phaser3-rex-notes/master/assets/images/arrow-down-left.png');
      
         this.load.image('textBox', 'https://ibb.co/R9z3Cvb');
        
        this.load.image('scientist', 'https://ibb.co/GFnPQsz');
    }

    create() {
var content = `لبيب آلي ذكي جدًا! لقد صنعته خصيصًا ليساعد في القيام بالمهام اليومية في المنزل. بقي علي جمع القطع الأخيرة. هناك اثنا عشرة قطعة متناثرة في أنحاء المختبر، لنبحث عنها و ننهي بناء لبيب.`;

    this.conversation = this.rexUI.add
      .sizer({
        x: 920,
        y: 800,
        width: 1000,
        height: 600,
        orientation: "x",
        rtl: true,
        space: { left: 10, right: 10, top: 10, bottom: 25, item: 10 },
      })

      .add(
        this.add.image(960, 540, "scientist"),
        {
          proportion: 1,
          align: "right-bottom",
        }

      )
      .add(
        createTextBox(this, 800, 1000, {
          wrapWidth: 1050,
          fixedWidth: 1096,
          fixedHeight: 332,
        }).start(content, 50),
        {
          align: "center-center",
        }
      )
      .layout()
      .drawBounds(this.add.graphics(), 0xff0000);
    }

    update() {}
}


const GetValue = Phaser.Utils.Objects.GetValue;
var createTextBox = function (scene: any, x: any, y: any, config: any) {
  var wrapWidth = GetValue(config, "wrapWidth", 0);
  var fixedWidth = GetValue(config, "fixedWidth", 0);
  var fixedHeight = GetValue(config, "fixedHeight", 0);
  var textBox = scene.rexUI.add
    .textBox({
      x: x,
      y: y,
      rtl: true,
      background: scene.add.image(0, 0, "textbox"),
      text: getBBcodeText(scene, wrapWidth, fixedWidth, fixedHeight),

      action: scene.add
        .image(0, 0, "nextPage")
        //.setTint(COLOR_LIGHT)
        .setVisible(false),

      space: {
        left: 100,
        right: 100,
        top: 150,
        bottom: 20,
        icon: 10,
        text: 10,
      },
    })
    .setOrigin(0)
    .layout();

  textBox
    .setInteractive()
    .on(
      "pointerdown",
      function () {
        var icon = textBox.getElement("action").setVisible(false);
        textBox.resetChildVisibleState(icon);
        if (textBox.isTyping) {
          //icon.stop(true);
          textBox.stop(true);
        } else if (textBox.isLastPage) {
          done = true;
          textBox.destroy();
        } else {
          textBox.typeNextPage();
        }
      },
      textBox
    )
    .on(
      "pageend",
      function () {
        if (textBox.isLastPage) {
          //textBox.setVisible(false);
          //textBox.destroy();
          return;
        }

        var icon = textBox.getElement("action").setVisible(true);
        textBox.resetChildVisibleState(icon);
        icon.y -= 30;
        var tween = scene.tweens.add({
          targets: icon,
          y: "+=30", // '+=100'
          ease: "Bounce", // 'Cubic', 'Elastic', 'Bounce', 'Back'
          duration: 500,
          repeat: 0, // -1: infinity
          yoyo: false,
        });
      },
      textBox
    );
  //.on('type', function () {
  //})

  return textBox;
};

var getBBcodeText = function (
  scene: any,
  wrapWidth: any,
  fixedWidth: any,
  fixedHeight: any
) {
  return scene.rexUI.add.BBCodeText(0, 0, "", {
    fixedWidth: fixedWidth,
    fixedHeight: fixedHeight,
    rtl: true,
    fontFamily: "Almarai",
    fontSize: "28px",
    color: "fff000",
    wrap: {
      mode: "word",
      width: wrapWidth,
    },
    maxLines: 2,
  });
};

var config = {
    type: Phaser.AUTO,
    parent: 'phaser-example',
    width: 800,
    height: 600,
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH,
    }, 
    scene: Demo
};

var game = new Phaser.Game(config);

The position and space settings in your given test code are strange.

  • Scene size is 800x600
  • Topmost ui at 920x800 : position is out of scene
  • Text size is 1096x332 : width is too big

Width of image ‘scientist’ at right side will be resized, the ratio of this image might not be kept.
And the background image ‘textBox’ will be resized too. The same, the ratio of this image might not be kept.
It is better to use ninepatch for background image.

You can test ui elements from bottom to top.

Sorry! My bad!

The scene size should be: 1920 * 1080

I’ll try ninepatch and see.

Thank you!

The background image has a transparent border around image, therefore the typing-text will show outside of display-image (actually, text is at transparent border)

The red rectangle is the whole size of image.

The space setting in textBox can be used to shift text game object.
You might use round rectangle at first, to see the result of space setting.

Okay… Thank you!

Another question,
If I have two characters that are talking to each other, and I need the second character’s text box to appear only after the first character is done speaking, how would I do that?
Do I declare two sizers objects?

Two separated text-box might be easier.

But I want the character image to appear beside the text box, like this:

Can we do this with the text-box object?

Do you mean that putting two images at left and right side of text-box? Yes you can use Sizer to do that.

@rexrainbow Re-opening this. I have a question regarding the original post in this thread related to RexUIPlugin with Typescript: “Property ‘rexUI’ does not exist on type ‘Scene’” - the issue is in the third code block of these blocks. I’ve tried following everything discussed in the thread above related to this issue.

Versions

  • Phaser: 3.55.2
  • Typescript: 4.6.4
  • Package Manager: Webpack v4.31.0

Code

game.ts:

import 'phaser'
import ExplorationScene from './js/scenes/exploration'
import UIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin.js'

var game = null
const config = {
    width: 800,
    height: 600,
    type: Phaser.AUTO,
    parent: 'game',
    scene: [
        ExplorationScene,
    ],
    scale: {
        mode: Phaser.Scale.FIT,
        autoCenter: Phaser.Scale.CENTER_BOTH,
        orientation: Phaser.Scale.Orientation.LANDSCAPE,
    },
    physics: {
        default: 'arcade',
        arcade: {
            debug: false,
            gravity: {
                y: 0
            }
        }
    },
    plugins: {
        scene: [
            {
                key: 'rexUI',
                plugin: UIPlugin,
                mapping: 'rexUI'
            },
        ]
    },
}

export class Game extends Phaser.Game {
    constructor(config: object) {
      super(config)
    }
}

window.addEventListener('load', () => {
    game = new Game(config)
})

exploration.ts (Scene):

import UIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin.js'
import Npc from '../sprites/npc'

class ExplorationScene extends Phaser.Scene {
    public rexUI: UIPlugin

    constructor() {
        super('exploration-scene')
    }

    preload() {}

    create() {
        let npcs = ['npc'].map(npcToLoad => {
			return new Npc(this, 400, 300, '', npcToLoad)
		})
	    NpcsController.setNpcs(npcs)
     }
}

npc.ts (where RexUI is used):

import 'phaser'

class Npc extends Phaser.Physics.Arcade.Sprite {
    public scene: Phaser.Scene

    constructor(scene: Phaser.Scene, x: number, y: number, texture: string, name: string) {
        super(scene, x, y, texture)
        this.scene = scene
        this.scene.add.existing(this)
        this.scene.physics.add.existing(this)
        this.addTooltip()
    }

    addTooltip() {
        var that = this
		const COLOR_PRIMARY = 0x4e342e
		const COLOR_LIGHT = 0x7b5e57
		const COLOR_DARK = 0x260e04
		var content = `[area=Phaser]Phaser[/area] is a [area=fast]fast[/area], free, and fun open source HTML5 game framework that offers WebGL and Canvas rendering across desktop and mobile web browsers. Games can be compiled to iOS, Android and native apps by using 3rd party tools. You can use JavaScript or TypeScript for development.`

		this.scene.input.setTopOnly(false)
		PlayerController.getPlayer().setCanMove(false)

        const GetValue = Phaser.Utils.Objects.GetValue

        var createTextBox = function(scene: Phaser.Scene, x: number, y: number, config: any) {
            var wrapWidth = GetValue(config, 'wrapWidth', 0)
            var fixedWidth = GetValue(config, 'fixedWidth', 0)
            var fixedHeight = GetValue(config, 'fixedHeight', 0)
            // ISSUE HERE: "Property 'rexUI' does not exist on type 'Scene'
            var textBox = scene.rexUI.add.textBox({
                ...
            })
        }
        ...
    }
    ...
}

In exploration.ts, it has public rexUI: UIPlugin, but this line does not exist in npc.ts. Might try to add this line in npc.ts.

Hi,

In npc.ts, I’ve added import RexUIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin.js' at the top and public rexUI: RexUIPlugin within class Npc extends Phaser.Physics.Arcade.Sprite {.

It’s now able to find RexUI, but I have the following error now within npc.ts at:
var textBox = this.rexUI.add.textBox({...})

Cannot read properties of undefined (reading ‘add’)

In npc.ts, console.log(this.rexUI) gives me undefined.

In exploration.ts, console.log(this.rexUI) gives me:

Since rexUI is a “scene” plugin as the docs say, in npc.ts, I’ve also tried var textBox = this.scene.rexUI.add.textBox({...}), but if I do that, it says rexUI does not exist on type Scene even though I’m passing through the exploration.ts scene to npc.ts.


I guess my core question is: how can I make use of a “scene” plugin, such as rexUI, within a non-scene class, such as Phaser.GameObjects.Container or Phaser.Physics.Arcade.Sprite?

I’m assuming the rexUI needs to be part of a scene class: so if I’m passing a scene class instance to the constructor of a non-scene class instance, and within those non-scene class instances I set this.scene = scene (where scene is the scene instance class), shouldn’t I be able to access rexUI from the non-scene instance with this.scene.rexUI...? I’m assuming within the non-scene class, that I cannot do this.rexUI even if I added import RexUIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin.js' to the top of the non-scene class file, and declared it with rexUI: RexUIPlugin, right?


My code now is:

game.ts

import 'phaser'
import ExplorationScene from './js/scenes/exploration'
import RexUIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin.js'

var game = null
const config = {
    ...
    scene: [
        ExplorationScene,
    ],
    ...
    plugins: {
        scene: [
            {
                key: 'rexUI',
                plugin: RexUIPlugin,
                mapping: 'rexUI'
            },
        ]
    },
}

exploration.ts (Scene):

import RexUIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin.js'
import Npc from '../sprites/npc'

class ExplorationScene extends Phaser.Scene {
    rexUI: RexUIPlugin

    constructor() {
        super('exploration-scene')
    }

    preload() {}

    create() {
        var npc = new Npc(this, 400, 300)
     }
}

npc.ts (where RexUI is used):

import 'phaser'
import RexUIPlugin from 'phaser3-rex-plugins/templates/ui/ui-plugin.js'

class Npc extends Phaser.Physics.Arcade.Sprite {
    rexUI: RexUIPlugin
    public scene: Phaser.Scene

    constructor(scene: Phaser.Scene, x: number, y: number) {
        super(scene, x, y)
        this.scene = scene
        this.scene.add.existing(this)
        ...
        this.addTooltip()
    }

    addTooltip() {
        var that = this
	    var content = `[area=Phaser]Phaser[/area] is a [area=fast]fast[/area], free,...`
        ...
        var createTextBox = function(scene: Phaser.Scene, x: number, y: number, config: any) {
            // ISSUE HERE: "Property 'rexUI' does not exist on type 'Scene'"
            var textBox = that.scene.rexUI.add.textBox({
                ...
            })
            // ISSUE HERE: "No property 'add' on undefined"
            var textBox = that.rexUI.add.textBox({
                ...
            })
        }
        ...
    }
    ...
}

Might try to import Game object class directly, since you can use import... from. For example

import { TextBox } from 'phaser3-rex-plugins/templates/ui/ui-components.js';

...

addTooltip() {
    var textBox = new TextBox(scene, config);
    scene.add.existing(textBox);
}

In this way, you don’t have to install rexUI plugin either declare rexUI: RexUIPlugin anymore.