Michał Malik
Sep 21 · 5 min read

In this multipart tutorial, I would like to walk the reader through the process of developing a simple HTML5 platform game. The game is written with TypeScript and uses PixiJS v5 as rendering engine.

This course is split into following parts:

For convenience each part corresponds to a branch in github repository: https://github.com/MMMalik/platform-game-tutorial

The final result can be accessed under the following url: https://mmmalik.github.io/platform-game-tutorial/

I expect the reader to be familiar with TypeScript and have some basic experience with PixiJS.

As a disclaimer, I would like to note that game development is not my main area of expertise. I am merely having fun developing games and learning about game development. If you find some of the ideas I share here as incomplete or simply wrong, I will be glad to know.

In this part, I would like to setup state management, where State will be just one more game component. In the previous part, we have defined a game component as a function which returns an instance of a Pixi sprite. Since State has to be updated on every frame, and it has no associated sprite instances, definition of game component has to be a little more flexible.

First of all, a game component must return a render function which will be invoked on each animation frame. To further unify interface of a component function, let’s have it return an instance of Pixi DisplayObject (e.g. Sprite or Container). To better handle this definition, let’s also create a helper initializeComponents function.

Let’s take a look:

Generic type will help us establish connection between a game component and shape of our particular game’s state. Before we start implementing State component itself, we need to define the shape of the actual GameState. For now, let’s focus on Character component and its state:

State of the character extends WorldObject. As such, it must contain object’s position: x, y, and its velocity (both horizontal and vertical): vX, vY. Please note that the position in the world of an object corresponds to a set of different coordinates than those of a Pixi’s sprite. This distinction is important in cases where we might have a camera set to follow our character in a way that character is always placed in the middle of the screen. Although the character stays in the middle of the scene, it does not mean it has not moved in the context of world coordinates.

Camera following main character

Therefore, we maintain position of the character on our own, independently of Pixi’s sprite coordinates. Although this might not be needed yet (the camera is still static), it will help us later on.

In addition to the generic properties of a WorldObject, a character will have: mode(jumping, running, idle, etc.), direction(which way it is facing — left or right), jump(current jump height).

Now, let’s create State game component.

This component is relatively simple. Inside render, it reads current state of the keyboard and then all the heavy lifting is done in calculateCharacterState. Let’s take a look at the latter function then:

The logic of calculating each piece of state is divided into smaller helper functions:

  • getCharacterMoveDirection — if an arrow is pressed, let’s set character’s direction to the movement direction. Otherwise, remain faced in the previous direction.
  • isCharacterMovingX — checks if an arrow right or left is pressed.
  • isCharacterJumping — checks if jump has a value greater than zero.
  • getCharacterMode — the preference for setting a character’s mode is as follows: If character is jumping, set mode to Jumping. Else if it’s not on the ground, set mode to Falling. Else if it’s moving horizontally, then set mode to Running. Set mode to Idle as a fallback.
  • getCharacterJump — if space is pressed and character is on the ground, then start jumping. Else if character is already jumping and it is below the jump threshold, then increase jump height. Reset jump otherwise.
  • getCharacterVy — if character is jumping, then set vY to jump speed (negative sign!). Return 0 if character is on the ground. Otherwise apply gravity. We will improve this logic once collision detection is established.
  • getCharacterVx — if character is moving horizontally, apply character’s speed multiplied by direction (either 1 or -1). Otherwise, set vX to 0. We will improve this logic once collision detection is established.
  • isOnTheGround — if y value is greater than half of the scene height, assume character is on the ground. We will improve this logic once collision detection is established.

Invoking those functions helps us establish new character state. This state can be used now to update the visual part of the game (managed by Pixi). Let’s take a look at improved Character component:

Inside the render function, we read the state and update the following:

  • sprite’s x scale to change direction the character is facing.
  • sprite’s x and y coordinates based on current character’s vX and vY, respectively.
  • sprite’s texture based on current character’s mode. First, we need to check if the texture has changed, so that we can play the animation associated with that mode right from the start.

Regarding character’s animation, src/assets/adventurer.json has to be updated:

New character animations

On a more general note, we have two kind of components now:

  • Stateful — represented by State component. In the render function, new state will be calculated on each frame based on user input (keyboard), possible collisions, etc. The render function of this component has to be invoked as the first one among all components, as it will set the basis for rendering subsequent components.
  • Stateless (presentational) — represented by all other components (Character, Platform, Background). Within these components, game state is read-only and is used to present the current game state. In other words, it is used to update properties of Pixi sprites. For instance, we use it to set correct character animation (idle, running, jumping, etc.), update sprite’s scale and position.

Now, let’s take a look at the current state of the game:

Part 2 — Game has state management and it listens to user input

In the next part, we will introduce collision detection. Stay tuned!

JavaScript in Plain English

Learn the web's most important programming language.

Michał Malik

Written by

Web developer, former organic synthetic chemist, craft beer enthusiast.

JavaScript in Plain English

Learn the web's most important programming language.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade