Vinter, programming
Back

In my free time I decided to build a roguelike in React. Why? Because I thought it might be fun and I wanted to have a small game present on my website. I probably could've used WebGL or WebAssembly but where's the fun in that? Let's use the wrong tools to build something cool!

Preface: The intent of this series of posts is to document my thoughts, not to build a production-ready application. I have absolutely no clue about what I'm doing so the code will reflect this

Implementing an Entity Component System

I don't have any precise idea of what I want to do so let's start with the how. One of the latest patterns used in game development (especially for roguelikes) is the Entity Component System pattern.

ECS

Let's break down what's going on. We have three main actors:

The main idea is to have Systems running in a loop targeting Entities that have specific Components. In our example above we can see that there's a RendererSystem that acts on every Entity that has a RenderComponent, a MoveSystem that acts on every Entity that has a MoveComponent and finally a FollowComponent that works with Entities with a FollowComponent. If you look closely you can see that some Components hold data (for example the Render one has a position), while others do not.

A basic pseudocode implementation for the RenderSystem above could look like this:

define RenderSystem:
    entities = globalState.getAllEntitiesWithComponent(RenderComponent)
    for each entity in entities:
        render(entity.render.symbol).atCoordinates(entity.render.position.x, entity.render.position.y)

The biggest advantage of this method is that we're using composition over inheritance making our components as loosely coupled as possible. We can have independent Components and add functionality to Entities as needed, even at runtime.

ECS in React

Instead of trying to reinvent the wheel I tried searching for a React ECS library and found [this one]. Its documentation is kinda sparse but after playing a bit with it I managed to get it working.

First of all you have an ECS.Provider which holds all the data pertaining to the application. Inside you can define Entities and Systems. An Entity is then a collection of Components (which are called Facets to avoid confusion, so from now on I will be calling them like this as well)

Let's try and render something on our screen. The architecture is the one we had above, so we need to define a PlayerComponent and a RenderSystem. Instead of a RenderFacet I will create a PositionFacet since everything that has a position will need to be rendered anyways.

import { Facet } from '@react-ecs/core'

export default class Position extends Facet<Position> {
  location? = { x: 0, y: 0 }
}

The code is simple. It's just a class that extends Facet and has a location attribute. Now to the PlayerEntity

export default function Player() {
  return (
    <>
      <Position />
      <DOMView>
        <img
          style={{
            position: 'absolute',
            left: 0,
            top: 0,
            width: 100,
            height: 100
          }}
          src="https://i.imgur.com/kFjaH5l.png"
        />
      </DOMView>
    </>
  )
}

This one is sligly more complex and there a couple of things to explain. First of all the DOMView component which is a helper wrapper provided by the library that allows its children to be viewed on the DOM. Then we have the React fragment (<>). The library requires each Entity to be wrapped with an <Entity> tag but by moving it to a separate file I started having hook issues. I still didn't manage to find a solution so for now this separation will have to suffice.

Finally we have the ViewSystem

export default function ViewSystem({}) {
  const query = useQuery((e) => e.hasAll(DOMView, Position)) // [1]

  return useSystem((dt) => { // [2]
    query.loop([DOMView, Position], (e, [view, pos]) => { // [3]
      view.element.style.left = `${pos.location.x * 100}px`
      view.element.style.top = `${pos.location.y * 100}px`
    })
  })
}

What's going on here?

Finally we need to put this everything together. I created an ecs.tsx file that will hold our game logic.

export default function Canvas() {
  const ECS = useECS()
  useAnimationFrame(ECS.update)

  return (
    <div>
      <ECS.Provider>
        <ViewSystem />
        <Entity>
          <Player />
        </Entity>
      </ECS.Provider>
    </div>
  )
}

So far it's a pretty simple function. We instantiate an ECS hook by using useECS() and then we tell him to update the systems on each frame. In the return we have the ECS.Provider and inside we can find our Systems and Entities. (Note how we need to put the <Entity> tag around our Player because of what was said before)

By running this we can see that the ECS is indeed working and rendering a simple Smiley face on the screen. Rejoice!

The face

In our next installment we will look at how to read for player input and make our face move. Stay tuned!

© thevinter.RSS