NAV

Introduction

"When you wish to instruct, be brief" - Cicero

This is a summary of the knowledge I have gathered along my career and I think is essential to get started making games and content with PixiJS.

PixiJS is an HTML5 Creation Engine and can be used to create beautiful digital content with the fastest, most flexible 2D WebGL renderer.

We could spend pages and pages discussing what PixiJS is and isn't, or if you should or shouldn't use it over other engines, frameworks, or libraries but I refuse to do so. I assume that if you are here is because you already made your mind so I won't try to convince you.

Let's get straight to teaching!

Getting Started

Before we even start...

This tutorial won't teach you to code from scratch. I will assume you already know your way around code and object-oriented programming.
Git and some shell (console) experience will be good but not mandatory. If you know any object-oriented programming you will be just fine.

This ain't your grandma's Javascript...

If you came here expecting to write javascript between two <script> tags in your .html file, oh boy I have bad news for you.
Web development might have started with only a text editor and dumping code into a single .js file but in today's spooky fast world of javascript libraries, going commando ain't gonna cut it.
This... guide? course? tutorial? will use Typescript and it will be designed to work with pixi-hotwire, a boilerplate to get you started fast without the hassle of how to create and maintain a standard web project with a bundler. (If you have a bit more experience in web development, feel free to tinker and try any bundler of your liking.)

pixi-hotwire?

In case you realize you have no idea what a bundler is or why you need one, worry not! I've created a base template project called pixi-hotwire. It is the simplest boilerplate I could build. It goes from nothing to watching your code running with the least amount of configurations.
It is a just-add-npm solution that sets you up with typescript, pixi.js and parcel. It also will make a final build folder for you instead of you having to pluck manually files out of your project folder to get the uploadeable result of your hard work.

Typescript?

I'll be honest with you: I hate Javascript. I hate the meme of truthy and falsy values. I hate the implicit type coercion. I hate the dynamic context binding. I hate a lot of javascript... But I must endure, adapt, overcome.
Enter Typescript: Typescript will force your hand to keep your code as strictly typed as possible, and if you need to use type coercion and really javascripty code, you must consciously choose to make the data type as any.
As a nice bonus, you get intellisense on Visual Studio Code (which is Microsoft's fancy name for code auto-complete).

I will never scream (PIXI.)

I feel that doing import * as PIXI from "pixi.js" so that I can go PIXI.Sprite, PIXI.Container, PIXI.Loader, etc, is silly and makes me feel clumsy. I will use named imports: import { Sprite, Container, Loader } from "pixi.js" and that allows me to just use the class name without adding PIXI everywhere.
I could say it is better for three-shaking (the step where the bundler doesn't add to your code stuff that you never use) or that I fight the smurf naming convention... but at the end of the day I just like named imports better. Sorry, not sorry.

Enough talk, have at you!

Be patient. When the time is right, the code will be shown in this column.

Let's start by making sure you have all the tools and materials.

You will need:

A quick overview of what you will find inside pixi-hotwire:

Once you have cloned or downloaded pixi-hotwire you will need to grab all the dependencies (stored inside the package.json file), to do so you will need to use a shell (console), navigate to the project folder and use the command npm install to read all the dependencies and download them into the node_modules folder.

When the progress finishes you now have access to new stuff that begins with npm!

Run npm run start and open your web browser on the website http://localhost:1234.
localhost means "your own computer" and 1234 is the port where this web server is running. Nobody else will see your game running on their localhost. You will need to export it and upload it somewhere.
Try hitting F12 and finding the Javascript Console. That will be your best friend when needing to know why something is or isn't working. That is where console.log() will write and where you will see any errors that you might have made.

Finally some code!

Now, for the actual code inside the index.ts file

import { Application, Sprite } from 'pixi.js'

const app = new Application({
    view: document.getElementById("pixi-canvas") as HTMLCanvasElement,
    resolution: window.devicePixelRatio || 1,
    backgroundColor: 0x6495ed,
    width: 640,
    height: 480
});

const clampy: Sprite = Sprite.from("clampy.png");

clampy.anchor.set(0.5);

clampy.x = app.screen.width / 2;
clampy.y = app.screen.height / 2;

app.stage.addChild(clampy);

First, we see the import statement. As previously stated, I prefer named imports to just importing everything under a big all-caps PIXI object.

After that, we create an app instance of PixiJS Application. This object is a quick way to set up a renderer and a stage to drop stuff on screen and be ready to render. You can see that I call for an HTML element by id pixi-canvas, this element can be found on the index.html file. (If you know a bit of web development, that file will reference directly the typescript file, which is usually illegal. This works because that file it's used by parcel as the entry point. The final exported index.html file won't have any reference to any typescript file.)
As a parameter for the Application object, we give it some options. You can see and explore the full list in PixiJS official docs.

Then, I create a Sprite with the Sprite.from(...) method, this is a super powerful shortcut that can take as a parameter one of many things, among which we can find:

Can you guess what we used here?
Well, I hope you guessed option three, "the URL of an image file" because that is the correct option.
By saying "clampy.png" it means "find a file called clampy.png in the root of my asset folder". You will see that our asset folder is called static and if you check in there, indeed there it is, clampy.png!

Finally, I set the position of Clampy and add it to the screen by doing an addChild() to the app.stage.
You will learn soon enough what the stage and why is it called addChild() but for now it should suffice to know that app.stage is the name for "the entire screen" and everything that you want to show needs to be appended by using addChild().

Homework!

Oh boy, you thought you could get away?
Try these on for size!

Putting stuff on screen

(and being in control of said stuff)

The DisplayList

In my not-so-humble opinion, the best way to handle 2d graphics.

It all starts with an abstract class: The DisplayObject. Anything that can be shown on the screen must inherit from this abstract class at some point in his genealogy. When I want to refer to "anything that can be placed on the screen" I will use the generic term DisplayObject.
In PixiJS your bread and butter are going to be Container and Sprite. Sprites can show graphics (if you come from the getting started you've already seen it in action) and Containers are used to group sprites to move, rotate and scale them as a whole. You can add a Container to another Container and keep going as deep as you need your rabbit hole to go.
Imagine a cork bulletin board, some photos, and a box of thumbtacks. You could pin every photo directly to the board... or you could pin some photos to another and then move them all together by moving the photo you pinned every other photo to.
In this relationship, the Container is called the parent and the DisplayObjects that are attached to this parent are called children. Things are rendered back-to-front, which means that children will always be covering their parents. Between siblings (children with the same parent), the render order will start from the first added child and move to the last one, making the last child added show on top of all his siblings (unless you use the zOrder property of any display object. However, this property only works between siblings. A child will never be able to be behind his parent).
I know that it has list in the name, but it actually looks more like a tree. If you are a nerd like me and understand data structures, imagine a tree structure where each node can have any number of nodes attached that can have more nodes attached...
Finally, we have the root of everything, the granddaddy of them all, the greatest of grandfathers, and we shall call it the Stage. The stage is just a regular container that the Application class creates for us and feeds it to the Renderer to... well render it.

Have some code

import { Application, Sprite, Container } from 'pixi.js'

const app = new Application({
    view: document.getElementById("pixi-canvas") as HTMLCanvasElement,
    resolution: window.devicePixelRatio || 1,
    backgroundColor: 0x6495ed,
    width: 640,
    height: 480
});

const conty: Container = new Container();
conty.x = 200;
conty.y = 0;
app.stage.addChild(conty);

const clampy: Sprite = Sprite.from("clampy.png");
clampy.x = 100;
clampy.y = 100;
conty.addChild(clampy);

Ok, let's make a simple example to test Container and Sprite.

It should look fairly familiar, but let's go real quick over it...
We create conty the Container and add it to the app.stage. That means that it is added to the screen and should get rendered but it is empty...
So we create clampy the clamp Sprite like the last time but instead of adding it directly to the screen, we add it to conty the Container.
Now, see how we set Clampy's position to 100, 100 but if you run it, you will see that it is not showing up there but way more to the side... why?
Well, we added Clampy to conty and he has an x value of 200. That means that Clampy now has a global x value of 300!

Homework!

Ok, let's try some things and see what happens!

A quick overview of the Display Objects you can use out of the box.

Note: I will just give you a quick overview and a link to the official PixiJS API, if you want to know everything about an object you should got here.

Container

an example where bigConty is the papa of littleConty.

const bigConty: Container = new Container();
bigConty.scale.set(2); // You can use set and only one value to set x and y
bigConty.position.x = 100;
bigConty.y = 200; // this is a shortcut for .position.y and it also exists one for .position.x
app.stage.addChild(bigConty);

const littleConty: Container = new Container();
// position has a copy setter. It won't use your reference but copy the values from it!
littleConty.position = new Point(300,200);
bigConty.addChild(littleConty);

A bit of a silly example, it won't show anything on screen since containers don't have content by themselves.

The most basic class you will have. While it doesn't have any graphical representation on its own, you can use it to group other objects and affect them as a unit.
PixiJS Container API
Methods and properties that you will use most frequently:

Particle Container

There isn't much to them, it's like a regular container

const particleConty: ParticleContainer = new ParticleContainer();
// Pretty much everything that worked on a Container will work with a ParticleContainer.

A Particle Container is a special kind of container designed to go fast. To achieve this extra speed you sacrifice some functionality.
The rules for Particle Containers are:

While the name seems to indicate that Particle Containers should be used for Particles you can use them as super fast containers when you need to render a lot of objects.

Sprite

The simple example from the getting started

const clampy: Sprite = Sprite.from("clampy.png");

clampy.anchor.set(0.5);

// setting it to "the middle of the screen
clampy.x = app.screen.width / 2;
clampy.y = app.screen.height / 2;

app.stage.addChild(clampy);

Here we use again the shortcuts for position.

The simplest way to show a bitmap on your screen. It inherits from Container so all the properties from above apply here too!
PixiJS Sprite API
Methods and properties that you will use most frequently:

Graphics

There is SO much stuff you can do with graphics... Let's just make a circle at 100,100

const graphy: Graphics = new Graphics();

// we give instructions in order. begin fill, line style, draw circle, end filling
graphy.beginFill(0xFF00FF);
graphy.lineStyle(10, 0x00FF00);
graphy.drawCircle(0, 0, 25); // See how I set the drawing at 0,0? NOT AT 100, 100!
graphy.endFill();

app.stage.addChild(graphy); //I can add it before setting position, nothing bad will happen.

// Here we set it at 100,100
graphy.x = 100;
graphy.y = 100;

I can't stress this enough: Do draw your graphics relative to their own origin and then move the object. Don't try to draw it directly on the screen position you want

This class allows you to make primitive drawings like rectangles, circles, and lines. It is really useful when you need masks, hitboxes, or want a simple graphic without needing a bitmap file. It also inherits from Container.
PixiJS Graphics API
Methods and properties that you will use most frequently:

Text

Check PixiJS Textstyle Editor to make the textstyle easily.

const styly: TextStyle = new TextStyle({
    align: "center",
    fill: "#754c24",
    fontSize: 42
});
const texty: Text = new Text('私に気づいて先輩!', styly); // Text supports unicode!
texty.text = "This is expensive to change, please do not abuse";

app.stage.addChild(texty);

(Japanese text is optional, I used it just to show Unicode support)

Oh boy, we could have an entire chapter dedicated to text but for now, just the basics.
Text has AMAZING support for Unicode characters (as long as your chosen font supports it) and it is pretty consistent on how it looks across browsers.
PixiJS Text API
Tips:

BitmapText

PixiJS Textstyle Editor can be used to make the object for the BitmapFont.from(...) thingy.

// If you need to know, this is the expensive part. This creates the font atlas
BitmapFont.from("comic 32", {
    fill: "#ffffff", // White, will be colored later
    fontFamily: "Comic Sans MS",
    fontSize: 32
})

// Remember, this font only has letters and numbers. No commas or any other symbol.
const bitmapTexty: BitmapText = new BitmapText("I love baking, my family, and my friends",
    {
        fontName: "comic 32",
        fontSize: 32, // Making it too big or too small will look bad
        tint: 0xFF0000 // Here we make it red.
    });

bitmapTexty.text = "This is cheap";
bitmapTexty.text = "Change it as much as you want";

app.stage.addChild(bitmapTexty);

Remember, symbols won't show by default. Your sentence might not mean the same without commas.

The faster but more limited brother to Text, BitmapText uses a BitmapFont to draw your text. That means that changing your text is just changing what sprites are shown on screen, which is really fast. Its downside is that if you need full Unicode support (Chinese, Japanese, Arabic, Russian, etc) you will end up with a font atlas so big that performance will start to go down again.
PixiJS BitmapText API
Tips:

Filters

Stunning effects with no effort!

PixiJS has a stunning collection of filters and effects you can apply either to only one DisplayObject or to any Container and it will apply to all its children! I won't cover all the filters (at the time of writing there are 37 of them!!) instead, I will show you how to use one of the pre-packaged filters and you will have to extrapolate the knowledge from there.
You can see a demo of the filters or go directly to Github to see what package to install.

Creating and using filters is so easy that I wasn't sure if I needed to make this part or not

// import the filters
// If you are using pixijs < 6 you might need to import `filters`
import { BlurFilter } from "pixi.js"; 

// Make your filter
const myBlurFilter = new BlurFilter();

// Add it to the `.filters` array of any DisplayObject
clampy.filters = [myBlurFilter];

This is a section that I almost didn't make because using filters is super simple but I made it anyway so you guys know there is a huge list of filters ready to use.

In essence, create a filter, add it to the filters array of your display object, and you are done!

Particles

Make it rain

Make sure you dropped your emitter.json somewhere inside your src folder

import * as particleSettings from "../emitter.json";


const particleContainer = new ParticleContainer();
app.stage.addChild(particleContainer);

const emitter = new particles.Emitter(particleContainer, Texture.from("particleTexture.png"), particleSettings);
emitter.autoUpdate = true; // If you keep it false, you have to update your particles yourself.
emitter.updateSpawnPos(200, 100);
emitter.emit = true;

For handling particles, PixiJS uses their own format and you need to install the library: npm install pixi-particles and you can create them using the Particle Designer

Once we have our particles looking good on the editor we download a .json file and we will feed the parsed object into the Emitter constructor along with a container for our particles (it can be any Container but if you have a lot of particles you might want a ParticleContainer), and the Texture (or an array of textures) you want your particles to use.

To make that .json into something usable we need to either load it with the Loader or just add it as part of our source code and import it.

Methods and properties that you will use most frequently:

Context

whose grandma is it anyway?

Hopefully an example will make it easier to see...

class A {
    private myName: string = "I am A";
    public method: Function;
}

class B {
    private myName: string = "I am B";

    public printName() {
        // Here... what does "this" means?!
        console.log(this.myName);
    }
}

const a = new A();
const b = new B();

// I assign a.method
a.method = b.printName;
a.method(); // This will print "I am A"
b.printName(); // This will print "I am B"

// This will create a new function where `b` is the `this` for that function
a.method = b.printName.bind(b);
a.method(); // This now prints "I am B"

They are calling the same method... and getting different results!?

Imagine you have a phone number that calls your grandma Esther. Let's say that this number is 555-1234. When you dial 555-1234 you call your grandma.
Now you dictate me the number, 555-1234 and I dial, it calls my grandma Rachel... Wait what?!
We saw a moment ago that to reference a method or member of a class we need to prepend the this. keyword. In javascript, the this object, will become "whoever owns the function" (the context of who and where called the function).
So when you need to pass a method as a parameter (let's call it b.printName), when that function parameter gets called, inside the code of that function, this is not b! To fix this, you need to tell the function to bind (attach) the this (context) object. This is done by adding .bind(b) to the function like so: b.printName.bind(b)
Back to the telephone example, for me to call your grandma Esther you give me 555-1234.bind(estherGrandson) and that makes it so that when I use the number, it understands that the original owner it's you and thus calls your grandma Esther.

This might make no sense now but you will start seeing a lot of .bind() in future chapters and this is the primer.

If you want a more formal explanation with fewer grandmas, you can try MDN webdocs

Splitting code

it's not me, it's you

We have been dumping all our code directly into index.ts and this is all fine and dandy for quick tests and learning stuff but if we are going to move forward we need to learn how to split up our codebase into different files.

A quick preview of Scenes

This is my Scene.ts file

import { Container, Sprite } from "pixi.js";

export class Scene extends Container {
    private readonly screenWidth: number;
    private readonly screenHeight: number;

    // We promoted clampy to a member of the class
    private clampy: Sprite;
    constructor(screenWidth: number, screenHeight: number) {
        super(); // Mandatory! This calls the superclass constructor.

        // see how members of the class need `this.`?
        this.screenWidth = screenWidth;
        this.screenHeight = screenHeight;

        // Now clampy is a class member, we will be able to use it in another methods!
        this.clampy = Sprite.from("clampy.png");

        this.clampy.anchor.set(0.5);
        this.clampy.x = this.screenWidth / 2;
        this.clampy.y = this.screenHeight / 2;
        this.addChild(this.clampy);
    }
}

In a future chapter I will provide an example of my scene management system but for now, let's start by making a class that inherits from Container and that will be the lifecycle of our examples and demos. This will allow us to have methods that come from an object instead of being global functions.
Start by creating a new .ts file and create a class that extends Container. In my case, I created Scene.ts and created the Scene class. (I like to name my files with the same name as my class and to keep my classes starting with an uppercase).
See the export keyword? In this wacky modular world, other files can only see what you export.
And in the constructor of this new class, let's dump the code of our getting started.

And this is how my index.ts looks now

import { Application } from 'pixi.js'
import { Scene } from './scenes/Scene'; // This is the import statement

const app = new Application({
    view: document.getElementById("pixi-canvas") as HTMLCanvasElement,
    resolution: window.devicePixelRatio || 1,
    backgroundColor: 0x6495ed,
    width: 640,
    height: 480
});

// pass in the screen size to avoid "asking up"
const sceny: Scene = new Scene(app.screen.width, app.screen.height);

app.stage.addChild(sceny)

Finally, let's go back to the index.ts file and create and add to the screen a new Scene instance!
First, we will need to import it, Visual Studio Code usually can suggest you the import path, otherwise you just import it with curly braces and the relative or absolute path of the file.

From now on, the examples will show you "scenes" instead of raw code to plaster in your index.ts. You have been warned.

Animating stuff

shake it baby!

Let's sum it up quickly, we have 3 main ways of animating stuff:

There is another kind of animations called bone animation and while there are PixiJS plugins for loading two of the biggest formats out there, Spine and DragonBones, we won't be seeing them here.

AnimatedSprite

Don't freak out, we will use some javascripty stuff.

import { AnimatedSprite, Container, Texture } from "pixi.js";

export class Scene extends Container {
    constructor() {
        super();

        // This is an array of strings, we need an array of Texture
        const clampyFrames: Array<String> = [
          "clampy_sequence_01.png",
          "clampy_sequence_02.png",
          "clampy_sequence_03.png",
          "clampy_sequence_04.png"
        ];

        // `array.map()` creates an array from another array by doing something to each element.
        // `(stringy) => Texture.from(stringy)` means
        // "A function that takes a string and returns a Texture.from(that String)"
        const animatedClampy: AnimatedSprite = new AnimatedSprite(clampyFrames.map((stringy) => Texture.from(stringy)));
        // (if this javascript is too much, you can do a simple for loop and create a new array with Texture.from())

        this.addChild(animatedClampy); // we just add it to the scene

        // Now... what did we learn about assigning functions...
        animatedClampy.onFrameChange = this.onClampyFrameChange.bind(this);
    }

    private onClampyFrameChange(currentFrame): void {
        console.log("Clampy's current frame is", currentFrame);
    }
}

Clampy sequence assets don't exist. I lied to you. You will need your own assets.

Frame-by-frame animations go with 2d games like toe and dirt. Animated sprite has got your back. (This inherits from Sprite)
PixiJS AnimatedSprite API
Tips and stuff:

Ticker

Trigger warning: some math ahead

Try to read a bit into the math before jumping into this one

import { Container, Sprite, Ticker } from "pixi.js";

export class Scene extends Container {
    private readonly screenWidth: number;
    private readonly screenHeight: number;

    private clampy: Sprite;
    private clampyVelocity: number = 5;
    constructor(screenWidth: number, screenHeight: number) {
        super();

        this.screenWidth = screenWidth;
        this.screenHeight = screenHeight;

        this.clampy = Sprite.from("clampy.png");

        this.clampy.anchor.set(0.5);
        this.clampy.x = 0; // we start it at 0
        this.clampy.y = this.screenHeight / 2;
        this.addChild(this.clampy);

        // See the `, this` thingy there? That is another way of binding the context!
        Ticker.shared.add(this.update, this);

        // If you want, you can do it the bind way
        // Ticker.shared.add(this.update.bind(this)); 
    }

    private update(deltaTime: number): void {
        this.clampy.x = this.clampy.x + this.clampyVelocity * deltaTime;

        if (this.clampy.x > this.screenWidth) {
            // Woah there clampy, come back inside the screen!
            this.clampy.x = 0;
        }
    }
}

Ok, PixiJS Ticker is an object that will call a function every frame before rendering and tell you how much time has passed since the last frame.
But why is that useful? Well, when we know the velocity of something and we know for how long it has been moving we can predict where the object would be!

In a silly example: If I know you can walk one block in 5 minutes, I know that if 10 minutes have passed you should be two blocks away.

Now, the actual spooky math is:

New Position = Old Position + Velocity * Time Passed

We can use this math every single frame to move an object after we give it a velocity! (Sorry if this was very obvious for you.)

Now that we are on board on what we need to do, let's see how to use the PixiJS Ticker.
You can create one for yourself or just use the Ticker.shared instance that is already created for quick use. You just attach a function you want to be called every frame with the .add() and that's it! PixiJS Ticker API
Tips and Tricks:


Tweens

We will use my own creation, Tweedle.js you can get it by npm install tweedle.js

import { Tween, Group } from "tweedle.js";
import { Container, Sprite, Ticker } from "pixi.js";

export class Scene extends Container {
    private clampy: Sprite;
    constructor(screenWidth: number, screenHeight: number) {
        super();

        this.clampy = Sprite.from("clampy.png");

        this.clampy.anchor.set(0.5);
        this.clampy.x = screenWidth / 2;
        this.clampy.y = screenHeight / 2;
        this.addChild(this.clampy);

        Ticker.shared.add(this.update, this);

        // See how these chains all together
        new Tween(this.clampy.scale).to({ x: 0.5, y: 0.5 }, 1000).repeat(Infinity).yoyo(true).start();

        // This is the same code, but unchained
        // const tweeny = new Tween(this.clampy.scale);
        // tweeny.to({ x: 0.5, y: 0.5 }, 1000);
        // tweeny.repeat(Infinity);
        // tweeny.yoyo(true);
        // tweeny.start();
    }

    private update(): void {
        //You need to update a group for the tweens to do something!
        Group.shared.update()
    }
}

Note that you will still some ticker or loop to update the tweens

Ok, doing the math to move something with a given speed is fun and all, but what if I just want my element to do something in a certain amount of time and not bother to check if it already arrived, that it doesn't overshoot? To make it worse, what if I want some elastic moving movement?

Tweens to the rescue! For tweens, I'll be using Tweedle.js which is what I use every day in my job and it's my own fork of tween.js.
Just like with PixiJS API, I won't copypaste the full API, feel free to check the full Tweedle.js API Docs

Before we start, you need to remember You need to update tweens too!. This is usually achieved by updating the tween group they belong to. If you don't want to handle groups you can just use the shared static one that lives in Group.shared, just remember to update it.

Let's move through some of the basics:

Ok, but what other cool things can Tweedle do?

Getting interactive

In web HTML5 games we usually rely on 2 kinds of inputs: Some sort of pointing device (be it a mouse or a touchscreen) and the keyboard.

We will explore how to use both of them and then we will do a quick overview on how you can trigger some of your own custom events.

--

Pointer Events

mouse... touch.... both?

import { Container, InteractionEvent, Sprite } from "pixi.js";

export class Scene extends Container {
    private clampy: Sprite;
    constructor(screenWidth: number, screenHeight: number) {
        super();

        this.clampy = Sprite.from("clampy.png");

        this.clampy.anchor.set(0.5);
        this.clampy.x = screenWidth / 2;
        this.clampy.y = screenHeight / 2;
        this.addChild(this.clampy);

        // events that begin with "pointer" are touch + mouse
        this.clampy.on("pointertap", this.onClicky, this);

        // This only works with a mouse
        // this.clampy.on("click", this.onClicky, this);

        // This only work with touch
        // this.clampy.on("tap", this.onClicky, this);

        // Super important or the object will never receive mouse events!
        this.clampy.interactive = true;
    }

    private onClicky(e: InteractionEvent): void {
        console.log("You interacted with Clampy!")
        console.log("The data of your interaction is super interesting", e)

        // Global position of the interaction
        // e.data.global

        // Local (inside clampy) position of the interaction
        // e.data.getLocalPosition(this.clampy) 
        // Remember Clampy has the 0,0 in its center because we set the anchor to 0.5!
    }
}

PixiJS has a thing that whenever you click or move your mouse on the screen it checks what object were you on, no matter how deep in the display list that object is, and lets it know that a mouse happened.
(If curious, that thing is a renderer plugin called Interaction Manager)

The basic anatomy of adding an event listener to an imput is:
yourObject.on("stringOfWhatYouWantToKnow", functionToBeCalled, contextForTheFunction)

and the second super important thing is:
yourObject.interactive = true

Touch? Mouse? I want it all!

The web has moved forward since its first inception and now we have mouses and touchscreens!
Here is a small list of the most useful events with their mouse, touch, and catch-all variants.
The rule of thumb is that if it has pointer in the name, it will catch both mouse and touch.

Mouse only Touch only Mouse + Touch
click tap pointertap
mousedown touchstart pointerdown
mouseup touchend pointerup
mousemove touchmove pointermove

The event that fired

When your function gets called you will also receive a parameter, that is all the data that event produced. You can see the full shape of the object here.
I will list now some of the most common properties here now:

Magical stuff

You might come across by accident the fact that if you have a function that is called exactly the same as an event (for example a click function) and your object is interactive, that function gets called automagically.
That is because there is a line inside one of the Interaction Manager's methods that if it finds that a display object has a method with the same name as an event it calls it.

You can use it but I am not sure if this is legacy or why this exists.

--

Keyboard

import { Container, Sprite } from "pixi.js";

export class Scene extends Container {
    private clampy: Sprite;
    constructor(screenWidth: number, screenHeight: number) {
        super();

        // Clampy has nothing to do here, but I couldn't left it outside, poor thing
        this.clampy = Sprite.from("clampy.png");
        this.clampy.anchor.set(0.5);
        this.clampy.x = screenWidth / 2;
        this.clampy.y = screenHeight / 2;
        this.addChild(this.clampy);

        // No pixi here, All HTML DOM baby!
        document.addEventListener("keydown", this.onKeyDown.bind(this));
        document.addEventListener("keyup", this.onKeyUp.bind(this));
    }

    private onKeyDown(e: KeyboardEvent): void {
        console.log("KeyDown event fired!", e);

        // Most likely, you will switch on this:
        // e.code // if you care about the physical location of the key
        // e.key // if you care about the character that the key represents
    }

    private onKeyUp(e: KeyboardEvent): void {
        console.log("KeyUp event fired!", e);

        // Most likely, you will switch on this:
        // e.code // if you care about the physical location of the key
        // e.key // if you care about the character that the key represents
    }
}

And here is where PixiJS lets go of our hands and we have to grow up and use the DOM.

Luckily, the DOM was meant to use keyboards and we have two big events: keydown and keyup. The bad news is that this detects ALL keypresses, ALL the time.

To solve which key was pressed at any point in time, we have two string properties on the keyboard event: code and key.

key

key is the easiest one to explain because it is a string that shows what character should be printed into a textbox after the user presses a key. It follows the user keyboard layout and language so if you need the user to write some text, this is the ideal property.

code

code might be confusing at first, but let me tell you bluntly: Not everybody uses QWERTY keyboards. AZERTY and DVORAK keyboards are a thing (and we are not even getting into right to left keyboards) so if you bind your character jumping to the Z key you might find out later that not everybody has it in the same place in their keyboard distributions!
For problems like this, code was born. It represents a physical location in a keyboard so you can confidently ask for the Z and X keyboard keys and they will be one next to the other no matter what language it is using.

Consider this a Keyboard.ts recipe:

class Keyboard {
    public static readonly state: Map<string, boolean>;
    public static initialize() {
        // The `.bind(this)` here isn't necesary as these functions won't use `this`!
        document.addEventListener("keydown", Keyboard.keyDown);
        document.addEventListener("keyup", Keyboard.keyUp);
    }
    private static keyDown(e: KeyboardEvent): void {
        Keyboard.state.set(e.code, true)
    }
    private static keyUp(e: KeyboardEvent): void {
        Keyboard.state.set(e.code, false)
    }
}

But sometimes we need to know the state of a key: Is it pressed? To do this we need to keep track of this state of every key manually, luckily we can do it with a really simple static class.

We just need to once call the Keyboard.initialize() method to add the events and then we can ask Keyboard.state.get("ArrowRight") and if this says true then the key is down!

keyCode, which, keypress are deprecated!

--

Making custom events and the overall syntax

All display objects are event emitters so we can create a sprite to test:

const clampy: Sprite = Sprite.from("clampy.png");
clampy.on("clamp", onClampyClamp, this);

clampy.once("clamp", onClampyClampOnce, this);

// clampy.off("clamp", onClampyClamp); // This will remove the event!


// somewhere, when the time is right... Fire the clamp!
clampy.emit("clamp");


// If you come from c++ this will mess you up: Functions can be declared after you used them.
function onClampyClamp() {
    console.log("clampy did clamp!");
}

function onClampyClampOnce() {
  console.log("this will only be called once and then removed!");
}

using .off(...) can be tricky. If you used .bind(this) then it will probably not work. That is why there is an extra parameter so you can provide the this context to the on(...) function!

What do we do if we want to be notified when something happens? One way could be setting a boolean flag and looking at it at every update loop, waiting for it to change but this is clumsy and hard to read. Introducing Events!

An event is a way for an object to emit a scream into the air and for some other object to listen and react to this scream.
For this purpose, PixiJS uses EventEmitter3.
The API is made to mirror the one found on node.js
Let's see a quick overview of how EventEmitter3 works:

Collision detection

Stop touching meeeeeeee

We know how to put things on screen, how to make them move under our control but now we need to know when they happen to be one on top of the other.
To detect this we first need to understand a bit of math on how to know if two rectangles are overlapping, then we will see how to find global rectangles for our PixiJS objects.
PixiJS has the Rectangle class and the Bounds class and then PixiJS DisplayObjects have a getBounds() method but it returns a Rectangle and not a Bounds. Please don't think too much about it.

Pure rectangle math

Here you have a snippet to check if two rectangles intersect

// assume `a` and `b` are instances of Rectangle
const rightmostLeft = a.left < b.left ? b.left : a.left;
const leftmostRight = a.right > b.right ? b.right : a.right;

if (leftmostRight <= rightmostLeft)
{
    return false;
}

const bottommostTop = a.top < b.top ? b.top : a.top;
const topmostBottom = a.bottom > b.bottom ? b.bottom : a.bottom;

return topmostBottom > bottommostTop;

Ok, first I have to be honest with you, when I said Rectangles I exagerated. We will be working with Axis-aligned bounding boxes (AABB) and that means that the sides of our rectangles will always be parallel to one axis and perpendicular to the other. (In layman terms, no rotated rectangles here.)

With that out of the way let's see the naming, a rectangle has x, y, width, and height.
They also have: - top which is equal to y - bottom which is equal to y + height - left which is equal to x - right which is equal to x + width

In our algorithm we now calculate (please read this slowly and thoroughly): - rightmostLeft: Compare the left value of our two rectangles and pick the rightmost. - leftmostRight: Compare the right value of our two rectangles and pick the leftmost. - bottommostTop: Compare the top value of our two rectangles and pick the bottom-most. - topmostBottom: Compare the bottom value of our two rectangles and pick the topmost.

If you think about it, I now have a new set of left, right, top, and bottom values.
Here comes the magical part: if these values make sense, that is to say left is to the left of right AND top is above of bottom our initial rectangles are overlapping.
As soon as one of the pairs doesn't make sense, we know those rectangles can not be overlapping.

DisplayObjects into Rectangles

function checkCollision(objA: DisplayObject, objB: DisplayObject): boolean {
    const a = objA.getBounds();
    const b = objB.getBounds();

    const rightmostLeft = a.left < b.left ? b.left : a.left;
    const leftmostRight = a.right > b.right ? b.right : a.right;

    if (leftmostRight <= rightmostLeft) {
        return false;
    }

    const bottommostTop = a.top < b.top ? b.top : a.top;
    const topmostBottom = a.bottom > b.bottom ? b.bottom : a.bottom;

    return topmostBottom > bottommostTop;
}

Ok, now that we know how to check if two rectangles overlap we just need to make rectangles out of PixiJS DisplayObjects, introducing getBounds()
The bounds of a DisplayObject is an axis-aligned bounding box and in the case of containers, this box contains inside all its children.
The magic of this is that it works no matter how far apart are the DisplayObjects in the display tree because the method returns the global bounds of any object and that gives us a shared reference system for any pair of objects.

So, the logic is quite simple, we just need to getBounds() and then send them into the previous algorithm.

A note on colliding polygons.

For colliding angled rectangles, polygons, and more information about how they collide you can use the separating axis theorem (SAT) that says: Two convex objects do not overlap if there exists a line (called axis) onto which the two objects' projections do not overlap.

However, the algorithm is not as trivial as the AABB one, and getting DisplayObjects to become polygonal shapes adds another layer of complexity.

I do have an implementation for this but it's not elegant and I don't feel confident in giving it for the world to use. One day I might write a more robust implementation and make it free to the public.

Sounding Good

You and the marvelous world of sounds

To play sounds and music in our games we are going to use the PixiJS solution for sound.
Just like its rendering counterpart, PixiJS Sound hides away an implementation meant to work on every browser leveraging the WebAudio API giving us a simple-to-use interface with a lot of power under the hood.
If you want a more in-depth demo you can check the PixiJS Demo website.

Playing a sound

import { Container } from "pixi.js";
import { Sound } from "@pixi/sound";

export class Scene extends Container {
    constructor(screenWidth: number, screenHeight: number) {
        super();

        // Just like everything else, `from()` and then we are good to go
        const whistly = Sound.from("whistle.mp3");
        whistly.volume = 0.5;
        whistly.play();

    }
}

If you reached this point making Sprites you will see the Sound.from() syntax and feel at home.
You just create a sound object directly from a sound file URL and then you can tweak it and play it!
Some helpful things inside a sound are:

Recipes

This is where the basics end and while PixiJS still have plenty of classes and utilities I haven't explained yet I think so far I have explained everything you need to build your own game and research further.
That being said... there are some things I've picked up along the way making games: Introducing Recipes.
Recipes is going to be a way of providing you with the solutions to problems I have faced in my game-making career.

These Recipes will have a lot of code, so this column will probably the star of the show...

Recipe: Preloading assets

So far we have seen how to create images and sounds by downloading the asset behind them just as we need to show it to the user. While this is good enough if we want a quick way to make a proof of concept or prototype, it won't be good enough for a project release.
The elegant way of doing it is downloading all the assets you are going to need beforehand and storing them in some sort of cache. To this purpose, PixiJS includes Loader: An extensible class to allow the download and caching of any file you might need for your game.

In this recipe, we are going to create one of our Scene to load all the files we declare in a manifest object and then I will teach you how to recover the files from the Loader cache.

The file to download manifest.

Let's start with our manifest object. I will call this file assets.ts

export const assets = [
    { name: "Clampy the clamp", url: "./clampy.png" },
    { name: "another image", url: "./monster.png" },
    { name: "whistle", url: "./whistle.mp3" },
]

As javascript is unable to "scan" a directory and just load everything inside we need to add some sort of manifest object that lists all the files that we need to download. Here we can see we are how we can declare (and export for outside use) an array of objects that will be used by the Loader.
The name field is going to be our key to retrieve the downloaded object and the url field must point to where our asset is going to be located (by starting with ./ we mean "relative to our index.html").

How to use the Loader

This is just a snippet of how to use Loader. After this, we will see how to make a full loader Scene

// remember the assets manifest we created before? You need to import it here
Loader.shared.add(assets);

// this will start the load of the files
Loader.shared.load();

// In the future, when the download finishes you will find your entire asset like this
Loader.shared.resources["the name you gave your asset in the manifest"]; 
// You will probably want `.data` or `.texture` or `.sound` of this object 
// however for Pixi objects there is are better ways of creating them...

This is enough to start downloading assets... but what about progress? and how can we tell when we are done?

Like the Ticker class we saw before (and many other PixiJS classes), Loader has a shared instance we can access globally and we are going to use that to load our assets and keep them in cache so we can reference them without downloading them each time.
The add(...) method can take many shapes but we are feeding it our resource manifest we stored in assets.ts and then begin the download by calling the load() method.

Making it look pretty

This is our full code for LoaderScene.ts

import { Container, Graphics, Loader } from "pixi.js";
import { assets } from "../assets";

export class LoaderScene extends Container {

    // for making our loader graphics...
    private loaderBar: Container;
    private loaderBarBoder: Graphics;
    private loaderBarFill: Graphics;
    constructor(screenWidth: number, screenHeight: number) {
        super();

        // lets make a loader graphic:
        const loaderBarWidth = screenWidth * 0.8; // just an auxiliar variable
        // the fill of the bar.
        this.loaderBarFill = new Graphics();
        this.loaderBarFill.beginFill(0x008800, 1)
        this.loaderBarFill.drawRect(0, 0, loaderBarWidth, 50);
        this.loaderBarFill.endFill();
        this.loaderBarFill.scale.x = 0; // we draw the filled bar and with scale we set the %

        // The border of the bar.
        this.loaderBarBoder = new Graphics();
        this.loaderBarBoder.lineStyle(10, 0x0, 1);
        this.loaderBarBoder.drawRect(0, 0, loaderBarWidth, 50);

        // Now we keep the border and the fill in a container so we can move them together.
        this.loaderBar = new Container();
        this.loaderBar.addChild(this.loaderBarFill);
        this.loaderBar.addChild(this.loaderBarBoder);
        //Looks complex but this just centers the bar on screen.
        this.loaderBar.position.x = (screenWidth - this.loaderBar.width) / 2; 
        this.loaderBar.position.y = (screenHeight - this.loaderBar.height) / 2;
        this.addChild(this.loaderBar);

        // Now the actual asset loader:

        // we add the asset manifest
        Loader.shared.add(assets);

        // connect the events
        Loader.shared.onProgress.add(this.downloadProgress, this);
        Loader.shared.onComplete.once(this.gameLoaded, this);

        // Start loading!
        Loader.shared.load();
    }

    private downloadProgress(loader: Loader): void {
        // Progress goes from 0 to 100 but we are going to use 0 to 1 to set it to scale
        const progressRatio = loader.progress / 100;
        this.loaderBarFill.scale.x = progressRatio;
    }

    private gameLoaded(): void {
        // Our game finished loading!

        // Let's remove our loading bar
        this.removeChild(this.loaderBar);

        // all your assets are ready! I would probably change to another scene
        // ...but you could build your entire game here if you want
        // (pls don't)
    }
}

Keep in mind that if you have few assets (or a really fast internet connection), you might not see the progress bar at all!

I will not explain how the loading bar was made. If you need to refresh how to put stuff on screen check that chapter again.

Those events don't look exactly like the other events we saw in the Interaction section and that is because Loader uses their own kind of events called signals or minisignals however they work quite similarly to regular events, the big difference is that each signal is only good for one kind of event and that is why we don't have a string explaining what kind of event we need and instead we have two objects onProgress and onComplete.

How to easily use your loaded Sprites and Sounds?

Now that we downloaded and safely stored our assets in a cache... how do we use them?
The two basic components we have seen so far are Sprite and Sound and we will use them in different ways.

For Sprites, all our textures are stored in a TextureCache somewhere in the PixiJS universe but all we need to know is that we can access that cache by doing Sprite.from(...) but instead of giving an URL we just give the name we gave our asset in the manifest file. In my example above I could do Sprite.from("Clampy the clamp"). (If you ever need a Texture object, you can get it the same way with Texture.from(...) just remember that Sprites go on screen and Textures hide inside sprites.)

For Sounds, all our sounds are stored in what PixiJS Sound calls sound library. To access it we have to import it as import { sound } from "@pixi/sound";. That is sound with a lowercase s. From there you can play it by doing sound.play("name we gave in the manifest");.

Advanced loading

Ok, we have seen how to load our basic png and mp3 but what about other kinds of assets? What about spritesheets, fonts, levels, and more?
This section will show some of the most common asset types and how to load and use them.

But before we start, a bit of how things will work behind the curtains: Loader plugins.
The PixiJS Loader extends Resource Loader by Chad Engler and includes some plugins for downloading and parsing images, and spritesheets however for other types we will need custom plugins (e.g. for WebFonts) or the plugin will come bundled with a library (e.g. PixiSound includes a loader plugin)

Spritesheets

Add this to your assets.ts array...

{ name: "you wont use this name", url: "./yourSpritesheetUrl.json" }
// Don't add an entry for the .png file! Just make sure it exists next to your json file and it will work.

Then your textures from inside your spritesheet will exist in the cache! Ready to Sprite.from()

Sprite.from("Name from inside your spritesheet");
// or
Texture.from("Name from inside your spritesheet");

A spritesheet (also known as texture atlas) is a single image file with many assets inside next to a text file (in our case a json file) that explains how to slice that texture to extract all the assets. It is really good for performance and you should try to always use them.
To create a spritesheet you can use paid programs like TexturePacker or free ones like ShoeBox or FreeTexPacker.
If you have problems finding a PixiJS compatible format in the packer of your choice, it might also be called JSON Hash format

PixiJS includes a spritesheet parser so all you need to do is provide the url for the json file. You shouldn't add the URL for .png file but it must be next to the json file!

Fonts

Install PixiJS Webfont Loader by running npm i pixi-webfont-loader and then proceed to run this code before using your loader

// Add before using Loader!!!
Loader.registerPlugin(WebfontLoaderPlugin);
// Now you can start using loader like Loader.shared.add(assets);

Add your .css fonts to your assets.ts array

{ name: "you wont use this name", url: "./fonts/stylesheet.css" }

Your fonts will be registered into the system and you can just ask for the Font Name. You can check the font name by opening your .css with a text editor and checking the font-family inside. Also, remember you can make your text style with the PixiJS Textstyle editor.

const customStyleFont: TextStyle = new TextStyle({
    fontFamily: "Open Sans Condensed",
});
new Text('Ready to use!', customStyleFont); // Text supports unicode!

When I showed you text examples I used system fonts on purpose, but what if you want to use your custom font? or a google font?
In the web world, there are many font formats: ttf, svg, woff, woff2, and more but how do we make sure we have the right font format for each browser? Simple, WE HAVE THEM ALL!
An easy way is to get the font file you want to use and convert it using a service like Transfonter. That will give you a .css file and all the formats required.
Another way is to go to Google Fonts and download a Webfont from there.

To load our font css file we are going to need a Loader plugin: PixiJS Webfont Loader.
You can install the plugin by running npm i pixi-webfont-loader in a console in your project and then inside your code you must register the plugin.

Maps from level editors or custom txt, json, xml, etc.

Just add your custom file to your assets.ts

{ name: "my text", url: "./myTextFile.txt" },
{ name: "my json", url: "./myJsonFile.json" },
{ name: "my xml", url: "./myXMLFile.xml" },

A good way to see what you got is to console.log() the data

console.log(Loader.shared.resources["my text"].data); 
console.log(Loader.shared.resources["my json"].data); 
console.log(Loader.shared.resources["my xml"].data); 

The Loader class will recognize simple text formats like txt, json or xml and will give out a somewhat usable object inside the data object of the resource.

If you would like to parse a particular kind of file you will need to write your own Loader plugin. I might write a tutorial for that in the future but for now, you can read this one by Matt Karl.

Recipe: Scene Manager

In the Splitting Code chapter I explained how to create a Scene object to try to encapsulate different parts of our object and be able to swap them when needed but I didn't explain how to actually change from one scene to the next.
For this purpose, we are going to create a Manager static global class that wraps the PixiJS Application object and exposes a simple way to change from one scene to the next.

The Manager Class

Ok, let's write our Manager.ts file. This file will store the static class Manager and an Interface for our Scenes

import { Application } from "@pixi/app";
import { DisplayObject } from "@pixi/display";

export class Manager {
    private constructor() { /*this class is purely static. No constructor to see here*/ }

    // Safely store variables for our game
    private static app: Application;
    private static currentScene: IScene;

    // Width and Height are read-only after creation (for now)
    private static _width: number;
    private static _height: number;


    // With getters but not setters, these variables become read-only
    public static get width(): number {
        return Manager._width;
    }
    public static get height(): number {
        return Manager._height;
    }

    // Use this function ONCE to start the entire machinery
    public static initialize(width: number, height: number, background: number): void {

        // store our width and height
        Manager._width = width;
        Manager._height = height;

        // Create our pixi app
        Manager.app = new Application({
            view: document.getElementById("pixi-canvas") as HTMLCanvasElement,
            resolution: window.devicePixelRatio || 1,
            backgroundColor: background,
            width: width,
            height: height
        });

        // Add the ticker
        Manager.app.ticker.add(Manager.update)
    }

    // Call this function when you want to go to a new scene
    public static changeScene(newScene: IScene): void {
        // Remove and destroy old scene... if we had one..
        if (Manager.currentScene) {
            Manager.app.stage.removeChild(Manager.currentScene);
            Manager.currentScene.destroy();
        }

        // Add the new one
        Manager.currentScene = newScene;
        Manager.app.stage.addChild(Manager.currentScene);
    }

    // This update will be called by a pixi ticker and tell the scene that a tick happened
    private static update(framesPassed): void {
        // Let the current scene know that we updated it...
        // Just for funzies, sanity check that it exists first.
        if (Manager.currentScene) {
            Manager.currentScene.update(framesPassed);
        }

        // as I said before, I HATE the "frame passed" approach. I would rather use `Manager.app.ticker.deltaMS`
    }
}

// This could have a lot more generic functions that you force all your scenes to have. Update is just an example.
// Also, this could be in its own file...
export interface IScene extends DisplayObject {
    update(framesPassed: number): void;
}

Lots to unpack here but let's take it easy.
First of all, see that since I made this a fully static class I never use this object but instead I refer to everything by using the Manager. notation. This prevents the need to keep track of the context in this class.

Next, we have a private constructor; that means nobody will be able to do new Manager(). That is important because our class is fully static and it's meant to be initialized by calling Manager.initialize(...) and we feed parameters about the screen size and the background color for our game.
The Application instance and the width and height are safely stored and hidden in private static variables but these last two have getters so we can ask globally what is the size of our game.

The initialize function has one last trick: it adds links from your entire Manager to a ticker so that our Manager.update() method gets called. That update will then call the current scene update method. This will allow you to have an update method in your scenes that appears to be called magically.

Then we have the Manager.changeScene(...) method, this is exactly why we started all of this, an easy way to tell our game that the current scene is no longer useful and that we want to change it for a new one.
It's quite simple to see that Manager removes the old one from the screen, destroys it (actually, destroy() is a PixiJS method!), and then proceeds to put your new scene directly on screen.

But...
What is a Scene? A miserable little pile of pixels!
Well, it has to be a DisplayObject of some sort because we need to put it on the screen (remember that Containers are DisplayObjects) but we also want it to have our custom update(...) method and to enforce that we create an interface IScene (I like to begin my interfaces with I). This makes sure that if an object wants to be a Scene then it must follow the two rules: be some sort of DisplayObject and have an update(...) method.

Now, some reruns of your favorite classes!

Look at this buffed up LoaderScene.ts that now implements IScene and uses Manager to know its size!

import { Container, Graphics, Loader } from "pixi.js";
import { assets } from "../assets";
import { IScene, Manager } from "../Manager";
import { GameScene } from "./GameScene";

export class LoaderScene extends Container implements IScene {

    // for making our loader graphics...
    private loaderBar: Container;
    private loaderBarBoder: Graphics;
    private loaderBarFill: Graphics;
    constructor() {
        super();

        const loaderBarWidth = Manager.width * 0.8;

        this.loaderBarFill = new Graphics();
        this.loaderBarFill.beginFill(0x008800, 1)
        this.loaderBarFill.drawRect(0, 0, loaderBarWidth, 50);
        this.loaderBarFill.endFill();
        this.loaderBarFill.scale.x = 0;

        this.loaderBarBoder = new Graphics();
        this.loaderBarBoder.lineStyle(10, 0x0, 1);
        this.loaderBarBoder.drawRect(0, 0, loaderBarWidth, 50);

        this.loaderBar = new Container();
        this.loaderBar.addChild(this.loaderBarFill);
        this.loaderBar.addChild(this.loaderBarBoder);
        this.loaderBar.position.x = (Manager.width - this.loaderBar.width) / 2;
        this.loaderBar.position.y = (Manager.height - this.loaderBar.height) / 2;
        this.addChild(this.loaderBar);

        Loader.shared.add(assets);

        Loader.shared.onProgress.add(this.downloadProgress, this);
        Loader.shared.onComplete.once(this.gameLoaded, this);

        Loader.shared.load();
    }

    private downloadProgress(loader: Loader): void {
        const progressRatio = loader.progress / 100;
        this.loaderBarFill.scale.x = progressRatio;
    }

    private gameLoaded(): void {
        // Change scene to the game scene!
        Manager.changeScene(new GameScene());
    }

    public update(framesPassed: number): void {
        // To be a scene we must have the update method even if we don't use it.
    }
}

In this new LoaderScene that extends from IScene there are really only two changes:
* It now extends from IScene so it must have an update(...) method even if we don't need it. * When the loader finishes, it changes the scene to another one! We can finally see Manager.changeScene() in action!

And this is what the simpler GameScene.ts looks like.

import { Container, Sprite } from "pixi.js";
import { IScene, Manager } from "../Manager";

export class GameScene extends Container implements IScene {
    private clampy: Sprite;
    private clampyVelocity: number;
    constructor() {
        super();

        // Inside assets.ts we have a line that says `{ name: "Clampy from assets.ts!", url: "./clampy.png" }`
        this.clampy = Sprite.from("Clampy from assets.ts!");

        this.clampy.anchor.set(0.5);
        this.clampy.x = Manager.width / 2;
        this.clampy.y = Manager.height / 2;
        this.addChild(this.clampy);

        this.clampyVelocity = 5;
    }
    public update(framesPassed: number): void {
        // Lets move clampy!
        this.clampy.x += this.clampyVelocity * framesPassed;

        if (this.clampy.x > Manager.width) {
            this.clampy.x = Manager.width;
            this.clampyVelocity = -this.clampyVelocity;
        }

        if (this.clampy.x < 0) {
            this.clampy.x = 0;
            this.clampyVelocity = -this.clampyVelocity;
        }
    }
}

Turning on the entire machine

index.ts has always been the entry point of our game. Let's use it to start our Manager and open our first ever Scene

import { Manager } from './Manager';
import { LoaderScene } from './scenes/LoaderScene';

Manager.initialize(640, 480, 0x6495ed);

// We no longer need to tell the scene the size because we can ask Manager!
const loady: LoaderScene = new LoaderScene();
Manager.changeScene(loady);

So far we have a Manager that can change from one Scene to the next but... how do we start it and go to the first scene ever?
That is what the Manager.initialize(...) was for and we will call it directly from index.ts and right after that, we ask it to go to the first scene of our game: the LoaderScene.

That is all there is to it. You initialize your manager and are ready to rumble from scene to scene!

Recipe: Resize your game.

By now you probably have realized that all the examples so far have the game in the top-left corner of the screen, this is because we never bothered to move it or change its size: Let's make it fill the screen.
There are two ways to fill the screen that I like to call letterbox and responsive.
In letterbox you scale your entire game and add black bars to the side when the aspect ratio doesn't match, this is the easier approach and is good enough for small games that will be embedded in an iframe of another website. By using this method the resize is done entirely by the browser with some css trickery so you don't have to add any logic inside your game. In responsive you make your stage grow to fill the entire screen and you have to resize and rearrange your game elements to make your game take advantage of all the screen space available, this is almost is mandatory if you are trying to make a game that looks and feel native on every device (mostly smartphones). However, this approach is harder to implement as you have to always be mindful of the scale of your objects, sizes, and positions of the elements of your game.

To ask the browser what is the current screen size we will use a small catch-all piece of code. This is to make sure we get the correct measurement no matter what web browser the user has.

const screenWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const screenHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);

In any case, we will know the initial screen size (and if the screen changed the size) by using what the browser provides us.

Letterbox scale

Here you have a modified Manager.ts class that listens for a resize event and uses css to fix the size of the game.

export class Manager {
    private constructor() { }

    private static app: Application;
    private static currentScene: IScene;

    private static _width: number;
    private static _height: number;


    public static get width(): number {
        return Manager._width;
    }
    public static get height(): number {
        return Manager._height;
    }


    public static initialize(width: number, height: number, background: number): void {

        Manager._width = width;
        Manager._height = height;

        Manager.app = new Application({
            view: document.getElementById("pixi-canvas") as HTMLCanvasElement,
            resolution: window.devicePixelRatio || 1,
            backgroundColor: background,
            width: width,
            height: height
        });

        Manager.app.ticker.add(Manager.update)

        // listen for the browser telling us that the screen size changed
        window.addEventListener("resize", Manager.resize);

        // call it manually once so we are sure we are the correct size after starting
        Manager.resize();
    }

    public static resize(): void {
        // current screen size
        const screenWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
        const screenHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);

        // uniform scale for our game
        const scale = Math.min(screenWidth / Manager.width, screenHeight / Manager.height);

        // the "uniformly englarged" size for our game
        const enlargedWidth = Math.floor(scale * Manager.width);
        const enlargedHeight = Math.floor(scale * Manager.height);

        // margins for centering our game
        const horizontalMargin = (screenWidth - enlargedWidth) / 2;
        const verticalMargin = (screenHeight - enlargedHeight) / 2;

        // now we use css trickery to set the sizes and margins
        Manager.app.view.style.width = `${enlargedWidth}px`;
        Manager.app.view.style.height = `${enlargedHeight}px`;
        Manager.app.view.style.marginLeft = Manager.app.view.style.marginRight = `${horizontalMargin}px`;
        Manager.app.view.style.marginTop = Manager.app.view.style.marginBottom = `${verticalMargin}px`;
    }

    /* More code of your Manager.ts like `changeScene` and `update`*/
}

If you are not using a Manager you need to focus on the window.addEventListener(...) and the resize() method.

Letterbox scale implies two things: Making our game as big as possible (while still fitting on screen) and then centering it on the screen (letting black bars appear on the edges where the ratio doesn't match).
It is very important that when we make it as big as possible we don't stretch it and make it look funky, to make sure of this we find the enlargement factor and increase our width and height by multiplying this factor.
To get a factor you just divide the size of the screen by the size of your game but since you have width and height you end up with two factors. To make sure our game doesn't bleed out of the screen and fits inside we pick the smaller of the two factors we found and we multiply our width and height by it.

After we have our englarged size by multiplying by a factor, we calculate the difference between that size and the size of the screen and split it evenly in the margins so our game ends up centered.

Responsive Scale

Here you have a modified Manager.ts class that listens for a resize event and notifies the current scene of the new size.


export class Manager {
    private constructor() { }
    private static app: Application;
    private static currentScene: IScene;

    // We no longer need to store width and height since now it is literally the size of the screen.
    // We just modify our getters
    public static get width(): number {
        return Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
    }
    public static get height(): number {
        return Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
    }

    public static initialize(background: number): void {

        Manager.app = new Application({
            view: document.getElementById("pixi-canvas") as HTMLCanvasElement,
            resizeTo: window, // This line here handles the actual resize!
            resolution: window.devicePixelRatio || 1,
            backgroundColor: background,
        });

        Manager.app.ticker.add(Manager.update)

        // listen for the browser telling us that the screen size changed
        window.addEventListener("resize", Manager.resize);
    }

    public static resize(): void {
        // if we have a scene, we let it know that a resize happened!
        if (Manager.currentScene) {
            Manager.currentScene.resize(Manager.width, Manager.height);
        }
    }

    /* More code of your Manager.ts like `changeScene` and `update`*/
}

export interface IScene extends DisplayObject {
    update(framesPassed: number): void;

    // we added the resize method to the interface
    resize(screenWidth: number, screenHeight: number): void;
}

If you are not using a Manager you need to focus on the resizeTo: window, line in the constructor of Application.

To turn on Responsive resize is actually just one line of code: you add resizeTo: window, to your PixiJS Application object and it will work however your game won't realize that now has to move and fill the new space (or fit inside a smaller space).
To make this task a bit less hard, we need to know when the game changed size and what is that new size.
To do this, we will listen for the resize event, update our Manager.width and Manager.height variables, and let the current scene know that a change in size occurred. To let the scene know we are going to add a function to our IScene interface so all scenes must now have the resize(...) method.

After that... you are on your own: You need to write your own code inside each scene resize(...) method to make sure every single object in your game reacts accordingly to your new screen size. Good luck.

Resolution (Device Pixel Ratio)

new Application({
    view: document.getElementById("pixi-canvas") as HTMLCanvasElement,
    resolution: window.devicePixelRatio || 1, // This bad boy right here...
    backgroundColor: background,
    width: width,
    height: height
});

The keen-eyed of you might have noticed that when we create our Application object we have a line that says resolution: window.devicePixelRatio || 1 but what does that mean?
The standard for screens was set to 96 dpi (dots per inch) for a long time and we were happy until the Apple nation attacked with their Retina Display that had twice as many dots per inch, thus if devicePixelRatio was equal to 1 it meant 96 dpi and if it was equal to 2 it was Retina display.
And then the world kept moving forward, adding more and more dots per inch, in seemingly random amounts so devicePixelRatio started reporting decimal numbers left and right: everything as a factor of that original 96 dpi.

By letting feeding the Apllication constructor the devicePixelRatio we render our game in a native resolution for the displays dpi, resulting in a sharper image on devices that have more than 96 dpi but at the cost of some performance since we are effectively supersampling every pixel.

If a particular device (most likely an iOS device) isn't strong enough to render all the pixels of the native resoluton you can always force the resolution to 96 dpi with resolution: 1 and get an image that might be blurry but you get a non-despicable performance boost.