DEV Community

Chris Simmons
Chris Simmons

Posted on

How I built my new portfolio using Pixi.js

My New Portfolio

View the site here:

I recently launched a new version of Endigo Design, my personal portfolio. This is a process I undergo every few years. I devote a few days building a simple site that covers the essentials of who I am and what I do. This time though, I decided I wanted to go big. To flex my combined knowledge and skill between web and game development and create a unique showcase of my work. In this post I’ll review the site and discuss some of the technology and techniques powering it behind the scenes.

Exploring the Site

To begin, let’s take a look at the homepage.

The homepage includes selection between game and traditional modes.

Here you’re provided two options. The first, to view an interactive game-like experience. This operates similar to a virtual art gallery. Whereas you browse a virtual 2D world and inspect each of my projects, represented as monuments or tall black pillars. You’re also provided a means to fallback to a traditional site — which adheres to standard UI and UX patterns for navigating between each project and the About page. This is the safety net and fallback for users that are either in a hurry, or don’t have a device or network connection to property fun the game experience.

For the game experience you’re immediately presented with several unique characters to interact with, as well as a large monument — which triggers a modal embedded version of my biography page. When tapping each character, it trigger a traditional RPG-styled dialog box to appear. Each character provides small hints at how to interact with the world. A throw back to old school 2D Pokemon games.

The initial view of the game experience. Showcasing a stone monument and NPC characters.

Talking with my likeness via a small dialog.

At this point you’re set loose to navigate the rest of the world. Using your mouse or finger, you can tap and drag to move the camera. There are several uniquely styled biomes provide, which provided a means to group each set of projects. Along the way you’ll find pillars that represent each project I’ve worked on or contributed to. Tapping a pillar provides a description and other media within popup modal.

Discovering one of the project pillars.

Tapping a pillar reveals the details, including a description and media.

Each pillar you locate and interact with will immediately begin to animate. This helps visually identify which you have and have not yet visited. Overall it provides a really fun and unique experience, allowing you to take in the sights and sounds of this little world, while browsing my full catalog of work.

Tooling and Technology

When I set out to design this site, I knew right right away I wanted to create a presentation that looked and felt like a traditional 2D top-down RPG game. It would also need to live within the confines of available browser APIs and modern web-based technology. Given this, the use of the canvas element and WebGL were a given. When paired, these allow you to render graphics within either a 2D or 3D context. For 2D this means drawing primitive shapes like rectangles and circles, images (textures), all with direct interaction and input from end users. When combined, these provide the basic building blocks of a game.

Game Rendering

My early prototypes made use of pure HTML and Javascript paired with a canvas element. This would have involved building everything from the ground up. However, I quickly realized this would not be a scale well for a presentation of this size. I was lacking mainstays of game development, such as animated spritesheets and found interaction to be quite tricky. Instead I opted to use Pixi.js.

If you’re not familiar with Pixi, it provides several quality of life improvements and features over your standard canvas integration. This includes, but not limited to:

  • Containers: which allow you to create a group of elements, similar to Game Objects in engines like Unity.

  • Textures: preloaded image assets that may be used within your scene.

  • Graphics: dynamically rendered primitives such as rectangles, arcs, etc.

  • Sprites/Spritesheets: typically made up of one or more graphics or textures. Pixi even provides animated spritesheets using row and column based references from a single image for character animations.

  • Interaction: the Pixi API provides a very means to listen to and act on events for any scene object.
    Render Loop: a standardized loop meant to handle animation and monitor elapsed time within your scene.

  • Text: direct support for text, fonts, and even rasterized bitmap for high performance.

Web Application Framework

Next I knew I’d want to utilize UI components and a fully featured web application framework to build the foundation of the site. For this I turned to Svelte and Sveltekit. When combined these fulfill a similar role to tools such as Angular, Next.js for React, and Nuxt for Vue.

I used the follow features extensively via Svelte and Sveltekit:

  • Components: the reusable blocks of code/templates/styles that create a set of building blocks for the site. Things like the reusable footers, page templates, etc.

  • Routing: Sveltekit provides a great file-based routing system for helping control navigation of my site and pages.

  • State Management: which is handled via Svelte writables. The data stores use a pub/sub like model for data flow.

  • Build & Bundling: Sveltekit was developed on-top of Vite, a build tool created by Evan You (of Vue fame). It provides a very fast and responsive set of tooling for creating local servers, handle hot module refresh, Typescript compilation, building, bundling, and more.

Style and Design

Finally, I needed means to style and control the layout and design of each page and component. For this I turned to Tailwind CSS. Tailwind provides a full suite of utility CSS classes, while still allowing you to “draw outside the lines” with arbitrary overrides. It’s an acquired taste for sure, but a tool that can dramatically increase productivity in the right hands. It pairs really well with component libraries, such as Svelte, React, or Vue.

Pulling it All Together

While each tool is great in their own right, one of the key struggles of building web applications (at any scale) is finding the proper way to make everything work in unison. I’d like to take some time to explain how I paired everything together to build the most critical portions of the experience, with a focus on core game features.

The Pixi Component

The first challenge I faced was determining where to house the canvas element and begin rendering to the screen with Pixi. For this I opted for a simple standalone component. This allowed me to create a basic game manager, and included all logic, layout, and styles all within a single file. This made it quick and easy to jump between shared references and iterate the code.

The rough order of operations goes something like this:

  1. The canvas element is bound using Svelte’s bind:this={elemRef} syntax.

  2. The Pixi application is initialized, which takes in the element reference and sets global settings, such as display size.

  3. A Pixi loader ingests a set of static resources, which allows you to ensure all required elements are preloaded ahead of time. This can be tapped to present a loading progress bar on screen.

  4. When the game app is fully loaded a number of steps occur…

  5. The over world map is generated via a preloaded texture, then injected into the “level” container. This container houses the map and all other entities such as NPCs.

  6. Next, the camera class is initialized, which primarily controls the x/y position of the level container within the 2D world space. I explain this in greater detail in the section below.

  7. I initialize a grid class, which can optionally be enabled to visually display the individual “tile” segmentation and coordinates on-screen for debugging purposes. Tiles have a base of 16x16 pixels here.

  8. A slew of game objects, such as the pillars, npcs, and creatures are initialized and added as children to the level container.

  9. The level container is added to the Pixi stage (the scene). Which finally makes it visible on screen.

  10. On load, I adjust the camera focus to center on my personal character.

  11. Finally the game render loop it started, which enables all animation and movement to function. The camera, container level, and entities have their respective render methods updated per frame.

Camera Controls

Cameras present an interest challenge when rendering in a 2D virtual space. If you have created a game using an engine such as Unity or Godot you may take these for granted, as most provide these turn key. Pixi and canvas require you to solve this yourself. One method for accomplishing this requires a bit of an illusion for the end users. I’ve provided an illustration below to visualize this.

Since the canvas (red) is fixed within the visible page canvas, it cannot actually move. Instead, effect of camera movement is created by moving the the contents rendered within the canvas area itself. This typically involves creating a container that houses your game world map (green). This container may also contain children, such npcs, creatures, and buildings (blue).

Instead of a small guy carrying a camera (ala Mario 64) following you around, it’s the map and it’s children that actually move! It’s a really neat trick that works very well in execution.

To actually handle this in the code, the Camera class simply controls how far a user has dragged from Point A to Point B and sets an offset value represented a x and y. These offset values are then read during the animation loop to update the position of the level container.

Game Objects

Game Objects are not inherent to Pixi. Instead they are a concept borrowed from Unity. These objects may present anything that exists within you game. This is typically handled by creating a containing element (like a group) that includes visual elements (such as an animated sprite), and pairs pairs this with the code containing the logic and rules for how the object should interact with the world.

An NPC (non-playable character) is a good example of one of these. In my game I have multiple NPC entities, each containing their animated sprites for visual idle and walking animations. But also includes a set of logic for handling what happens when you mouse over and click the entity.

A common convention for implementing this via code is using Object-Oriented Programming(OOP for short). OOP involves creating a structured Class that describes the properties of an object, as well as containing the logic for how it should function. One or more of these classes can be instantiated to created standalone copies of that entity.

Returning to that NPC example — my GameObject class describes each NPC’s properties such as name, which texture image to display, and other descriptive details. While also including its own logic, such as an onclick handler that triggers a dialog to appear.

The core GameObject class, for which all entities use or extend.

This GameObject class contains all the core and shared properties and functional logic available to all entities in my game. I use this class to instantiate everything from NPCs, to crabs, to even the project pillars. Each instance has its own name, animated sprite reference, coordinate positions, and dimensions. They also share common functionality, via the class methods, that allow them to move on pre-defined paths and handle input events. Note that I do not use every feature on every entity though. Crabs don’t “talk” in my game…but they certainly could!

You might be asking yourself, though, what happens when you need properties or features outside the scope of the base GameObject class? This is where another key feature of OOP comes into play — class inheritance. To do this, I create a separate class that extends the GameObject. This means it inherits all properties and methods of the base class, while still allowing you to overwrite or extend the features specific for this unique type of entity.

I use this to great effect with Layla the Cat, the NPC-like entity that appears near the start of the game. Here I create a Cat class that extends the base GameObject class — which means she can still have her own name, animated sprite, and still trigger features such as dialogs. While still allowing me to overwrite and extend her base functionality. In this case, entities of the Cat type can flip the sprite on the X axis based on the mouse position. This makes her appear to follow the position of the mouse cursor on screen. This is a feature that’s unique to cats, since we know cats love to chase mice!

Wrapping Up

While there’s much more I’d love to cover, it may be keen to save those for later articles! However, if you’re curious to learn more about how the site was built, I invite you to explore the source code, which is available on Github:
GitHub - endigo9740/endigo-design: The portfolio of Chris Simmons

I offer a number of ways to contact me if you would like to follow up and ask me any questions directly as well.

For now thanks for reading and please enjoy the site!

Top comments (8)

oscarteg profile image
Oscar te Giffel

Looking pretty good. You should mention that you are really inspired by Hyper Light Drifter.

kimdontdoit profile image

Really cool! Instinctively wanted to use arrow keys to move Chris (or the map) Hope your portfolio gets the recognition it deserves 👏👏

It also really makes you want to experiment with Pixi.js some more, thanks!

miketalbot profile image
Mike Talbot ⭐

Nice write up and a very interesting portfolio. Shows how old I am that I tried to navigate with the cursor keys lol.

endigo9740 profile image
Chris Simmons

Ha, I'm right there with you. That actually sounds like a good quality of life improvement, plus keyboard a11y is always nice too! I'll keep that in mind of the future!

andrewbaisden profile image
Andrew Baisden

Impressive and that pixel game is pretty dope.

shriji profile image

Awesome work!

posandu profile image

Interesting portfolio!

maurerkrisztian profile image
Krisztián Maurer

Wow, Impressive work!