Making a 2D RPG game with react-three-fiber
Tags: react, gamedev, r3f, threejs
Published: 2020-10-22 06:52:28

In this article we're going to take a closer look at an [open source](https://github.com/coldi/r3f-game-demo) demo published by [@coldi](https://twitter.com/coldi). Coldi made a game, called [Colmen's Quest](https://coldigames.itch.io/colmens-quest) (that you should definitely check out), using react and react-three-fiber. He was kind enough to share the core engine that he made for his game to the community.

![Colmen's Quest](https://dev-to-uploads.s3.amazonaws.com/i/rkr8oueuc7cb4ewfovin.png)

It might sound odd to use a 3D library like ThreeJS to make a 2D game but it is actually not that uncommon at all. For example Unity, the popular 3D game engine, is also used a lot for 2D games like [Hollow Knight](https://unity.com/fr/madewith/hollow-knight).

Speaking about Unity, the game architecture that Coldi used is also inspired by Unity and resolve around the concept of [GameObject](https://blog.eyas.sh/2020/10/unity-for-engineers-pt1-basic-concepts/) components that we will talk about just after.
Adding react-three-fiber to the stack provides a terrific dev experience to make a webgl game with React.

This project is a really valuable learning material. By exploring it in this article we will learn a lot about game dev techniques, react-three-fiber and also React knowledge in general. We will also try to apply our newly acquired knowledge by tweaking the demo a bit. Let's dive in !

## The game demo

[![R3F 2D game demo](https://dev-to-uploads.s3.amazonaws.com/i/pum0sxin0cvtteiqqmv0.png)](https://compassionate-pare-ece3e5.netlify.app/)

[Demo link](https://compassionate-pare-ece3e5.netlify.app/)

Let's start by analyzing the elements and features that we have in this demo.
We have:
- 🗺 A map
    - defined with [tilesets](https://en.wikipedia.org/wiki/Tile-based_video_game)
- 🚶‍♂️ A character that can be moved with either a mouse or a keyboard
    - the mouse movement is trickier as it needs to compute the path ahead
- 🧱 A collision system
    - which prevents to walk into walls or objects
- 👉 An interaction system
    - pizza can be picked up and it is possible to interact with computers and coffee machines
- 📽 A scene system
    - to move from one room to another

We can start by cloning the demo here:

{% github https://github.com/coldi/r3f-game-demo %}

## Folders architecture

![Folder structure](https://dev-to-uploads.s3.amazonaws.com/i/yz43rkbd3k20gfuqebxy.png)

- **@core**: everything that is reusable and not specific to the current demo
- **components**:  components that hold logics more specific to the current demo.
- **entities**: describe elements in the game world (Pizza, Plant, Player...). All these elements are `GameObject`. We're going to explain more of this concept just below.
- **scenes**: represents the different rooms in the game. Scenes are an aggregation of `GameObject`. In the demo there are two scenes (Office and Other).

## Game architecture

![game architecture](https://dev-to-uploads.s3.amazonaws.com/i/zejl72unn2wfwyf09d0u.png)

The component architecture looks like this:

```tsx
    <Game>
        <AssetLoader urls={urls} placeholder="Loading assets ...">
            <SceneManager defaultScene="office">
                <Scene id="office">
                    <OfficeScene />
                </Scene>
                <Scene id="other">
                    <OtherScene />
                </Scene>
            </SceneManager>
        </AssetLoader>
    </Game>
```

We're going to explain each of them.

### Architecture - top part

#### Game

This component has 4 main features:

- register all `GameObject` inside the game
- a global state
- render the `Canvas` component from `react-three-fiber`
- pass a context to all its children with the global state and methods to find/register `GameObject`

#### AssetLoader

This component will load all image and audio assets of the game with the `Image` and `Audio` web object. It also displays an html overlay on top of the canvas while the assets are loading.

#### SceneManager

This component holds the state regarding the `Scene` currently being displayed. It also exposes a method `setScene` through a `Context` in order to update the current scene.

#### Scene

This component, besides displaying its children `GameObject`, will dispatch the events `scene-init` and `scene-ready` whenever the current scene changes.

There is also a level system present in the file that is not being used by the demo.


### Architecture - Bottom part

Now we are going to look a little deeper, inside the code of the `OfficeScene`.

```tsx
    <>
        <GameObject name="map">
            <ambientLight />
            <TileMap data={mapData} resolver={resolveMapTile} definesMapSize />
        </GameObject>
        <GameObject x={16} y={5}>
            <Collider />
            <Interactable />
            <ScenePortal name="exit" enterDirection={[-1, 0]} target="other/start" />
        </GameObject>
        <Player x={6} y={3} />
    </>
```

The `GameObject` component we saw earlier is the most important piece of the architecture. It represents almost every element in the game world. For example for the `OfficeScene` just above we have 3 `GameObject`:

- A Map
- A Scene changer
- The Player

`GameObject` holds state information like `position`, `enabled/disabled` or its `layer` in the game (ie: ground, obstacle, item, character ...). They can contain other `GameObject` as well.
`GameObject` can also contain other components that Coldi called `Scripts`. These scripts can hold the logic for interaction, collision or movement for example. Basically game objects are a composition of these reusable `Scripts` and other `GameObject`. This is a really powerful API because you can describe a game object behaviour component by just dropping components in it.

## Game Objects 

We're going to explore more the 3 `GameObject` we saw earlier:

### The map

This component will create the map of the Scene based on an **entities mapping string**. For example the Office mapping string looks like this:

```
# # # # # # # # # # # # # # # # #
# · W T # T · · W T · W · · · T #
# · · · · · · · · · · · · · · o ·
# o · · # · · · # # # # · · # # #
# # # # # · · · # W o W · · T W #
# C C C # · · · T · · · · · · · #
# o · · · · · · · · · · · · · o #
# # # # # # # # # # # # # # # # #
```

Inside the `OfficeScene` there is a function called `resolveMapTile` which will map each character to a game entity. Entities are `GameObject` that match a real element in the game world.
In this case we have the following **entities mapping:**

![entities](https://dev-to-uploads.s3.amazonaws.com/i/7ruti4fetdm7v63esq0w.png)

- # : wall
- . : floor
- W : workstation
- C : coffee machine
- T : plant

The child component `TileMap` will then be responsible to return the map base on the **entities mapping string** and the `resolveMapTile` functions.

The final map is a 2D grid, with each cell holding one or several `GameObject` components.

#### Entities - workstation

![workstation](https://dev-to-uploads.s3.amazonaws.com/i/ko8mwzuugd95738wkbn2.png)

Let's take a closer look at what an entity looks like. We're going to look at the `Workstation` one.

```tsx
export default function Workstation(props: GameObjectProps) {
    return (
        <GameObject {...props}>
            <Sprite {...spriteData.objects} state="workstation-1" />
            <Collider />
            <Interactable />
            <WorkstationScript />
        </GameObject>
    );
}
```

We can see the `GameObject` component we were talking about and some child components(`Sprite`, `Collider`, `Interactable` and `WorkstationScript`) that define its behaviour.

##### Sprite

The Sprite component is responsible for displaying all graphical elements in the game.
We didn't talk much about `react-three-fiber` until now, but it is in this component that most of visual rendering happens.

In ThreeJS elements are rendered through `mesh` objects. A mesh is a combination of a geometry and material.

In our case for the geometry we're using a simple Plane of 1x1 dimension:
```tsx
THREE.PlaneBufferGeometry(1, 1)
```

And for the material we're just applying the Threejs basic material:

```tsx
<meshBasicMaterial attach="material" {...materialProps}>
    <texture ref={textureRef} attach="map" {...textureProps} />
</meshBasicMaterial>
```

With a plain basic material however we would be just seeing a simple square. Our sprites are actually displayed by giving the `<texture>` object, which will apply sprites to the  `<meshBasicMaterial>`.

To sum up, the visual render of this demo is mostly 2D plane with texture applied to them and a camera looking at all of them from the top.

##### The collider

This component is responsible for handling collisions. It has two jobs:

- store the walkable state (if it is possible to step on it or not) of the `GameObject` using it. By default the `Collider` is initialized as non walkable.
- listen and trigger events to do some logic whenever there is a collision.

The component also uses the hook `useComponentRegistry` to register itself to its `GameObject`. This allows other elements in the game (like the player) to know that this game object is an obstacle.

For now we just have added an obstacle on the map, let's continue with the next component.

##### Interactable

This component is responsible for handling logic when the player interacts with other elements in the game. An interaction occurs when the player has a collision with another `GameObject` (this is why the `Collider` from earlier was needed).

`Interactable` has severals methods:
- interact: executed by the `GameObject` that **initiates** an interaction
- onInteract: executed by the `GameObject` that **receives** an interaction
- canInteract: is it possible to interact with it

The `Interactable` component, as the `Collider`, registers itself to its `GameObject`.

##### The WorkstationScript

```tsx
function WorkstationScript() {
    const { getComponent } = useGameObject();
    const workState = useRef(false);

    useGameObjectEvent<InteractionEvent>('interaction', () => {
        workState.current = !workState.current;

        if (workState.current) {
            getComponent<SpriteRef>('Sprite').setState('workstation-2');
        } else {
            getComponent<SpriteRef>('Sprite').setState('workstation-1');
        }

        return waitForMs(400);
    });

    return null;
}
```

At last we have a script, specific to this entity, to handle some logic.
We can see here that this script is listening to the `interaction` event from earlier. Whenever this happens it just swaps the sprite of the computer.

![Workstation interaction](https://dev-to-uploads.s3.amazonaws.com/i/fv5pnpdqgitjtgod0jq9.gif)

#### Exercice 

We're going to add a monster entity, disguised as a plant. Inside the object sprite sheet asset, we can see that there are two plants that are not used in the demo.
The goal will be to use them to create a new entity called ZombiePlant and place it inside the other Scene.

When interacting with the entity, the plant should swap from one sprite to the other one.

We will also have to change both the **entities mapping string** and the `resolveMapTile` function inside the `OtherScene`.

![zombie plant](https://dev-to-uploads.s3.amazonaws.com/i/jxmbcczapvupqrbqm2vi.png)

[Solution](https://github.com/Flow11/r3f-game-demo/commit/227c68d573179d0ae089e6718e0bf8f6a729c7a5)

### The scene changer

```tsx
        <GameObject x={16} y={5}>
            <Collider />
            <Interactable />
            <ScenePortal name="exit" enterDirection={[-1, 0]} target="other/start" />
        </GameObject>
```

Now let's look at the components that handle the scene change.
This component will be triggered when the player steps on it.

To create this effect, the scene changer has 3 child components:

- Collider
- Interactable
- ScenePortal

We are already familiar with some elements like `Interactable` and `Collider`. This shows us how reusable `GameObject` can be with this architecture. Let's look at the ScenePortal.

#### Scene Portal

![Scene portal](https://dev-to-uploads.s3.amazonaws.com/i/b0rnzifmrymmaroe8cf2.png)

This component is responsible for doing the scene change when the player interacts with it.
It has the following props:

- **name**: name of the portal
- **target**: destination where the player should be teleported (scene + portal). This parameter is a string with the following template: `sceneName/portalName`
- **enterDirection**: direction that the player should face when entering the new scene;

The component listens to the `interaction` event through the hook `useInteraction`. When he receives an interaction, it will check if it comes from the player. In that case the `port` function is called. It will change the current scene in the global game state. After that the component will wait for the `SceneInitEvent` and `SceneReadyEvent` to move the player in the right position and direction.

#### Workflow example

Let's try to visualize the whole workflow of the ScenePortal:

![portal workflow](https://dev-to-uploads.s3.amazonaws.com/i/lhjvvziaiqdpst3oib0k.png)


### The Player

![player](https://dev-to-uploads.s3.amazonaws.com/i/jgusychwds2xtn4l6hwd.gif)

We're now going to explore the biggest `GameObject` of the game, the `Player` one.
The player `GameObject` looks like this:

```tsx
    <GameObject name="player" displayName="Player" layer="character" {...props}>
        <Moveable />
        <Interactable />
        <Collider />
        <CharacterScript>
            <Sprite {...spriteData.player} />
        </CharacterScript>
        <CameraFollowScript />
        <PlayerScript />
    </GameObject>
```

We are still familiar with `Interactable` and `Collider`. 
Let's see what the new components are doing.

#### Moveable

This component just exposes an API, it does not listen to any events. It means that there will be another `GameObject` that will call the Movable's API to move the `GameObject` using it (in our case the Player).

The most important method is the `move` one. It takes a targetPosition as parameter, checks if this position is a collision and if not move the `GameObject` to it.

It also triggers a lot of events that can be used elsewhere. The events sequence look like that:

![move schema](https://dev-to-uploads.s3.amazonaws.com/i/50dda949oqeayytzfhrs.png)

Also the method `move` uses the [animejs](https://animejs.com/) library to animate the player sprite from one position to another.


#### CharacterScript

```tsx
    useGameLoop(time => {
        // apply wobbling animation
        wobble();

        // apply breathe animation
        if (!movementActive.current) {
            // breathe animation while standing still
            const breathIntensity = 20;
            scaleRef.current.scale.setY(1 + Math.sin(time / 240) / breathIntensity);
        } else {
            // no breathe animation while moving
            scaleRef.current.scale.setY(1);
        }
    });
```

This component is responsible for doing some animation to the Player Sprite. The script handle:
- flipping the sprite in the current moving direction (use the `attempt-move` event we saw earlier)
- apply a `wobble` effect while moving
    - this effect is applied inside the `useGameLoop` hook. Under the hood this hook uses the `useFrame` hook from react-three-fiber. This hook is really useful as it allows us to perform update on each frame
- add a footstep sprite and sound while moving
- make the animation bounce while moving (use the `moving` event we saw earlier)

To sum up this component perform sprite animation by listening to movement events from the `Moveable` component.

#### PlayerScript

Final piece of the `Player` entity, the `PlayerScript`.
This component handles the logic that the player can do. It will deal with both cursor and keyboard inputs.

##### Keyboard controls

There are 4 hooks `useKeyPress` that add the listener to the key given in parameter. These hooks return a boolean whenever the listed keys are pressed. These booleans are then checked inside a `useGameLoop`, that we saw previously, and compute the next position consequently. The new position is set in the local state of `PlayerScript`.

##### Cursor controls

![astar](https://dev-to-uploads.s3.amazonaws.com/i/go8guv2ou4vtovg6p1jo.gif)

This part is a bit more tricky. While the keyboard controls could move the player one tile by one tile, the cursor can move it to several tiles. It means that the whole path to the selected position must be computed before moving.

In order to do that the method use a popular path finding algorithm named **A star (or A*)**. This algorithm computes the shortest path between two points in a grid by taking collision into consideration.

As for the keyboard events the new position is updated into the local `PlayerScript` state. In addition the path is also displayed visually in this case. In the render method there is `PlayerPathOverlay` component which is responsible for doing just that.

##### Moving to the new position

In both cases we saw that the new position is updated in the local state of the component.
There is a useEffect that listens to that change and that will try to move the `GameObject`. Remember the `Moveable` component from before ? Here we get it and call its `move` method on him. If the move is not possible, the method returns `false`. In that case we will try to interact with the `GameObject` that is in the position that the player couldn't go to.

#### Exercice

This was a big piece but now we should understand how game objects work together, let's try to make a new thing now.

Remember our `ZombiePlant` entity? We're going to add some new features to it:
- When the player interacts with it: should bounce back from the player (like if the player was attacking it)
- Whenever the interaction occurs: should play a sound effect (we can reuse the eating for example)
- On the third interaction the zombie plant should disappear

![zombie plant 2](https://dev-to-uploads.s3.amazonaws.com/i/1182gd78xyvjaov7l2x2.gif)

[Solution](https://github.com/Flow11/r3f-game-demo/commit/e05d870c2d533bace1be2a1ec8600e88cd76e01e)

### Conclusion

This is it, we've gone through most of the demo !
I hope you learned a lot of things in this demo walkthrough (I did). Thanks again to [@coldi](https://twitter.com/coldi) for sharing this demo with the community.
Also as he said a lot of things could have been implemented differently. For example the collision system could have been done with a physic engine like `react-use-cannon`.
This is still a terrific example of how to make games with `react-three-fiber`.

Hopefully this gives you some ideas to make a game of your own !

If you're interested in front-end, react-three-fiber or gamedev, I will publish more content about these topics [here](https://twitter.com/flagrede).

Thanks for reading, happy coding.