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:
- Part 1: Setup repository and create game assets
- Part 2: Setup state management
- Part 3: Setup collision detection
- Part 4: Create a bigger level and setup a camera
- Part 5: Add score and enemies
- Part 6: Manage multiple levels
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
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:
y, and its velocity (both horizontal and vertical):
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.
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
directionto the movement direction. Otherwise, remain faced in the previous direction.
isCharacterMovingX— checks if an arrow right or left is pressed.
isCharacterJumping— checks if
jumphas 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
vYto 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
vXto 0. We will improve this logic once collision detection is established.
yvalue 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:
xscale to change direction the character is facing.
ycoordinates based on current character’s
- 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:
On a more general note, we have two kind of components now:
- Stateful — represented by State component. In the
renderfunction, new state will be calculated on each frame based on user input (keyboard), possible collisions, etc. The
renderfunction 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:
In the next part, we will introduce collision detection. Stay tuned!