Making of: 3D Chess in React

January 6, 2021

Recently I made a tiny demo where I implemented a subset of the chess game, made it in 3D and implemented using React exclusively for the job. It will be a quick summary of what happened and how it happened.

Screenshot of the final effect.

Screenshot of the final effect.

Models

I some have experience with Blender so I quickly put together a set of geometric (Bauhaus style) pieces.

Blender models of pieces.

Blender models of pieces.

Three.js in React

In order to display them I had to go for WebGL as usual with 3D rendering on the web. Normally I like to do it all myself, but this time I decided that maybe instead of spending a week on proving a point that I can do it myself, I might rather finish the project.

So here we go, Three.js to make handling 3D easier than usual and react-three-fiber because it exists and I really want to check it out.

The first thing that is mind-blowing to me is that I can define Three.js in JSX. How? Custom renderers. There is a great talk from React Conf 2019 that explains how it works.

What's even greater, you can mix JSX targetting this renderer with regular React for React DOM too, so Three.js elements can be nested in regular React. Although, sometimes it might be tricky to remember all caveats that apply there. For example, rendering context is not passed to the inner renderer, so if you have a state library with a state provider outside in the React context, you will have to pass it to the inside one.

This is what it looks like in practice:

<Canvas
  concurrent
  pixelRatio={[1, 2]}
  camera={{ position: [4, 10, 0] }}
>
  <ambientLight intensity={0.3} />
  <Suspense fallback={null}>
    {/* ... */}

Chessboard

Not everything has to be modelled and imported. For example, chessboard is easy to achieve using simple shapes (simplified Square.tsx):

<group position={[x, 0, y]}>
  <mesh>
    <boxBufferGeometry attach="geometry" args={[size, 0, size]} />
    <meshStandardMaterial attach="material" color={color} />
  </mesh>
</group>

It can be probably even simpler than this, since – if I remember correctly – there is Plane as a primitive type but I haven't looked into that too much.

Implementing chess

Basically, all game logic happens in one place here – when showing available moves. It is probably not the most readable or concise implementation you will see but was straightforward to define and implement.

For a given coordinate, I check:

  • If there is a piece there.
  • What is its type.

I have a helper that checks a given direction (specified as { x: -1 | 0 | 1, y: -1 | 0 | 1 }, which expresses any diagonal, horizontal or vertical movement in both directions) and goes on until:

  • It goes out of the chessboard.
  • It exceeds range (usually no limit, implemented as 8, or 1 field).
  • It finds another piece (if the same color then stops there, if the enemy then recognizes it as a possible attack move).

For pawns, I use an assumption (but I don't have to worry about the rules of chess changing or getting more complex any time soon, right?) that a white pawn on y === 6 or a black one on y === 1 is entitled to make the initial, 2-field-long move.

I haven't implemented castling, promotion or en passant as I was able to get away with not having them for the demo video (oops).

State updates

The game can be in one of the following states:

  • Nothing is happening.
  • User is hovering their piece.
  • User has picked up a piece, and, as we all know after watching Queen's Gambit, they are obliged to make a move with it now (it's not a bug, it's a feature!).

As a lazy React developer, I put all the actual logic wherever the final press event is handled. It happened to be in HoverableSquare, as it is displayed over the fields that the user can move to.

What happens then?

I check if there is some piece on the target field (then it's an attack) or not (a move). I update my field -> Piece | null mapping array (most likely buggy, but works reasonably well for the first few moves).

Then the tricky part happens: hovered element becomes null (re-render alert for <AvailableMoves /> and <Figures />; basically pink color disappears from the screen). The game state is updated (the changes I mentioned in the previous paragraph come to life and the current turn is passed to the other player).

Getting fancy

Presentation plays important role in any visual-heavy project. It's most likely the deciding factor for why you are actually reading this note.

I have (limited) experience with 3D modelling and wanted to finish it quickly so I went for simple tricks: directional lighting to make everything visible, shadows for realism's sake and additional point lights for fancy glares.

Comparison before and after effects.

Comparison before and after effects.

Playable example

Left to do

This demo lacks at least several things:

  • Being a complete chess game (there are missing moves, there is no way to win or lose the game, nothing).
  • Animations (react-spring should be perfect for the job).
  • More UI to show what's going on.

Thanks for reading!

Hope this was informative and met your expectations. If any part was unclear or you have any more questions feel free to mention/DM me on Twitter.

I think this is also a good place to thank some people without whom I wouldn't be writing this now:

  • @wojtus_7, my friend who started playing with chess-oriented demo recently and inspired me to give it a try.
  • @0xca0a, who is the person behind react-three-fiber.
  • @aivandroid, who asked me to create a blog/video of the development process.

Have a great day!

<-
Homepage

Stay up to date with a newsletter

Sometimes I write blogposts. It doesn’t happen very often or in regular intervals, so subscribing to my newsletter might come in handy if you enjoy what I am writing about.

Never any spam, unsubscribe at any time.