Jonathan Ho
After investing a significant amount of my life over the past year using and learning PixiJS, I took the opportunity afforded to me during my last co-op job (or internship), not to focus on what PixiJS puts on the screen, but how it gets there. While working on a project that was pushing the conventional limits of PixiJS, I realized that there are some features commonly found in typical game engines like Unity that would be incredibly useful. PixiJS does not have built-in features for scene management, settings management, broad animations tracking, nor a lighting effects tool. To be fair PixiJS IS NOT a game engine nor does it claim to be, but we were using it as one, so enter my work to add that functionality.
With some help from the other developers on the team, we began by determining what features needed to added. Through the exercise of modeling the game structure and virtual whiteboard (thank you COVID) and asking ourselves “how we would build it in Unity”, we arrived at the design requirements of our new pseudo-engine. We needed a way to effectively manage the different scenes, a way to manage the game state, a way to create animations and a way to add dynamic lighting effects at run-time. The culmination of all this planning was the solution.
At the top of any PixiJS application is the stage and, it is a container. A big part of pixiJS is containerization, everything is a container - graphics objects, sprites - everything goes into everything. Every scene has many parts and many layers but more on those later. The best thing about containerization is that when moving a parent container all its children come with it; if the parent becomes invisible then so, too, do the children. We can use a container as the base of a scene. I chose to make the SceneManager a singleton for a few reasons. Firstly, exporting an instance of the object instead of the class itself is useful since the only instance can be accessed from anywhere in the product. The scene manager controls what scene is visible as well as any extra layers that need to visible, like a heads-up display. With each scene being its own separate entity, we can instruct the scene manager to track it and simply call the goTo(SceneName) function. This centralized system helped the development team easily keep track of what scene was on the screen at any given moment.
Another challenge we faced was developing a system that could elegantly track and apply difficulty. Players could change difficulty in the Settings menu however every scene does not interface with the menu, this was controlled by the scene manager. The challenge was to develop this feature without creating a code mess. Enter the GameManager. The GameManager serves as the brain of our engine. Using the singleton pattern again, we can give everything access to the brain simply by importing the only instance. This is useful because, at any time, the current scene can ask for important information like: ‘What language should I be in?’ or ‘How hard should I make this puzzle?’- and all of this information is one function call away. Centralizing the state of the game is a key learning from my experience with Redux, where one source of truth is the best source of truth.
Those of us experienced with PixiJS know that every application has a ticker and to make something move we just a stepping function to the ticker and Voila we can move sprites across the screen. What if the game you make doesn't have anything ticking, like an online escape room? This is where the handy animation controller comes in. All the information needed to create an effect over time is passed to the animation controller. By simply providing a time duration and a stepping function, we can move anything on the screen - all of this without creating long and complicated tick functions that are required to pause our scenes. This works by keeping a list of active animations and on every ticker tick, it calls each animation’s step function. After the animation is compete the controller can also call the animations completion callback allowing for easy animation clean up.
Most game engines have the ability to change lighting. However, PixiJS does not. For the project I was working on, we needed a way to have ultraviolet, magnifying and flashlight effects. We can do this by adding more layers and creating masking objects. To create the magnifying glass we didn't need another layer. We can use the PixiJS renderer’s function, generateTexture(), to generate a texture centered at the mouse and then simply add that texture to a sprite that follows the mouse, quick and easy. To get the flashlight and ultraviolet light on screen and to add a revealing, we add layers hidden behind empty masks. When the scene wants to hide content behind a UV light effect, we add it to its UV Layer. Then to reveal the content, the user draws pure white on the invisible masking canvas while the colored translucent sprite, following the mouse gives the effect of uncovering hidden clues. We draw a white layer because it gets treated as one when the GPU multiplies the masking layer with what its masking, resulting in the UV or darkness hidden visual asset to appear as if it was 'coming out of nowhere' when it was actually there the whole time.
I learned alot from making this psuedo-engine and it is something I would like to continue to develop in the future. I think my next feature will be adding the ability rotate the application about the y-axis, giving users of this project the ability to create the illusion of 3d space through a buillboarding effect. My end goal is to make this available on the npm registry.
WOW! Thank-you for making it this far. If you read it or just scrolled to the bottom I still appreciate it. If you liked what you read, had questions, didn't like it, or just want to connect reach out to me in any of the ways below. See you in my inbox :)