Building a Game With TypeScript. Drawing Grid 5/5
Chapter III in the series of tutorials on how to build a game from scratch with TypeScript and native browser APIs
Welcome to the final part of Chapter III, “Drawing Grid”! The previous post was about implementing and testing a humble yet powerful rendering system. In this final article of the chapter, we will tie up all loose ends.
Having the rendering system is great, but how can NodeDrawComponent
access it? Do we have to pass the instance of Canvas
to the Node
? If so, then we have the same problem as before: we couple Node
with a drawing context. The only difference being that we would have to pass a reference to Canvas
rather than native context, which doesn’t minimize the problem. Something is off.
In Chapter III “Drawing Grid”, we are implementing a fundamental piece of the turn-based game: we are drawing the grid of nodes. Other Chapters are available here:
- Introduction
- Chapter I. Entity Component System
- Chapter II. Game loop (Part 1, Part 2)
- Chapter III. Drawing Grid (Part 1, Part 2, Part 3, Part 4, Part 5)
- Chapter IV. Ships (Part 1, Part 2, Part 3, Part 4)
- Chapter V. Input system (Part 1, Part 2, Part 3)
- Chapter VI. Pathfinding and Movement (Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, Part 7)
- Chapter VII. State Machina
- Chapter VIII. Attack System: Health and Damage
- Chapter IX. Winning and Losing the Game
- Chapter X. Enemy AI
Feel free to switch to the
drawing-grid-4
branch of the repository. It contains the working result of the previous posts and is a great starting point for this one.
Table of Contents
- Introduction
- Canvas Layers
- Testing CanvasLayer
- Drawing with Layers
- Re-Drawing
- Conclusion
Canvas Layers
There is also another question we have to answer. We agreed to draw all nodes on the same canvas. But what about other elements, for example, ships? Take a look at this gif:
This is the game we are trying to implement. These circles are “Ships”, and we will talk about them in the next chapter. But for now, we should keep in mind that these “Ships” have to be on top of the Grid. Always.
To address this, we can introduce a notion of layers” of the canvases, similar to the layer system you may have seen in image editors like Gimp. Having layers, we can put one canvas on top of another. For example, we can place Grid
along with any decorations on one, the “bottom” (“background”) layer. And then put Ships on another, “top” (“foreground”) layer. This approach can guarantee Ships are always drawn on top of the Grid.
At this moment, native Canvas API has no easy way to manage “layers”. There is
globalCompositeOperation
, of course, but as the name suggests, it’s a global setting. Working with it has a plethora of limitations. To give ourselves a room for maneuver, we will define our layering system.
I start by defining the CanvasLayer
class. It will manage access to every layer we decide to add to the game:
Let’s not forget about the barrel file:
This class will ensure the game always has only one canvas of a specific type. Though neither Canvas
nor CanvasLayer
are singletons, static CanvasLayer
ensures Canvas
is instantiated only ones:
First, I defined a static field for background Canvas
and a standard public getter with one caveat. This getter first checks if the field is empty. If so, it means background canvas has not yet been instantiated, and getter has to construct it first.
We are going to use CanvasLayer
in static context only, so I make constructor
private to prevent anyone from accidental instantiation.
Testing CanvasLayer
Before we move any further, let’s take a second to cover CanvasLayer
with tests.
We should verify that Background
is always the same, no matter how many times we access it:
I start by importing and mocking the Canvas
. I also make sure it was never called before:
Then I request the Background
layer a couple of times and check how many times Canvas
is instantiated:
Great! At this moment your code should successfully compile with npm start,
and all test should pass with npm t
:
Drawing with Layers
Now, finally, we can clean up NodeDrawComponent
and utilize our awesome layer system:
Your code should compile with npm start
and render:
Moreover, if you check the dev tools, you should now see only one canvas:
Excellent, exactly what we were after! Yet, there is one more thing left to cover. At this point, we draw a node only once: when NodeDrawComponent
awakens. This works and is super efficient but, unfortunately, not very flexible.
It is easy to fall into the trap and assume that the grid is somewhat static and should be drawn only once per lifetime. After all, it’s just a background of the game, right? Well, yes and no.
It sure is a background in terms of positioning on the screen. But that doesn’t mean it’s static. We have to interact with this layer: click on the node should be signal to a player’s ship to move to this node:
This is the reason we made all this journey to create Grid
and Node
entities rather than simply draw them on canvas. But if you take a closer look at the gif above, you may notice something else interesting. Color of Node
should be able to change to help players understand where they can click.
How can we achieve this behavior? The way browser canvas works, after the shape has been drawn, you lose all the control of it. There is no reference to color property or something. To change the color of a rectangle, NodeDrawComponent
has to redraw: clean up and draw again but with a new color.
Re-Drawing
There are a few ways we can do this. One of them is utilizing the Update
method. If you recall, each Component
has Update
, a method which is called by the Entity
this Component
belongs to.
NodeDrawComponent
is no exception and has its own Update
method, which we kept empty for a while:
Instead of drawing once NodeDrawComponent
awakes, we will draw on every Update
. This way, we can be sure that if something changes, Node
will have the most relevant representation.
To do so, we can move the Draw
method call from Awake
to Update
:
Of course, we should clean up the respective area first. Fortunately, we already prepared a proper API in our little rendering engine, the ClearRect
method:
First, we set up a private Clear
method. Its responsibility is to clean up anything that has been drawn on a specific area of the canvas, precisely the spot where we used to draw a rectangle. Then, on every frame, we clean up and draw again.
“We draw again every frame??? Is that even performant?!”, you may ask. We could indeed implement some smart system that checks if there even was any change. And, if so, only then trigger the redraw. This would be more performant, of course.
But, as you probably have noticed, I keep using an incremental approach in this tutorial. We start simple, adding more complexity as we go. The current solution is performant enough for us: the number of nodes is limited, and the drawing of each node is quite cheap. Our architecture allows us to revisit and improve if we’ll face performance bottlenecks in the future.
And again, your code should successfully compile with npm start
and all test should pass with npm t
:
Before we say goodbye to this chapter, let’s add a cherry on top of this beautiful cake. Let’s cover NodeDrawComponent
with some tests!
Testing NodeDrawComponent
Initial setup should look familiar:
However, we will have plenty of other Node
components. And for every single one of them, we would have to make the same setup. Let’s save us some time and effort and create a mock factory for the Node
, which we can reuse over and over again.
Mock factory is simply a function that builds a Node
for us with the specified params:
To make it slightly more convenient, we can set up a default arguments:
Don’t forget to update the barrel file:
Now, back in the NodeDrawComponent
test I will use the mock factory instead of calling Node’s constructor directly:
Nice! All is left is to check if NodeDrawComponent
executes proper methods of CanvasLayer
. For example, as soon as NodeDrawComponent
awakes, it should clean up the canvas:
Similarly, NodeDrawComponent
should cleanup and redraw on every update:
Yet again, your code should successfully compile with npm start
and all test should pass with npm t
:
You can find the complete source code of this post in the
drawing-grid-5
branch of the repository.
Conclusion
Awesome! This concludes Chapter III, “Drawing the Grid”. We accomplished a lot in this final part of the Chapter! We discussed the notion of layers
of canvases in our game and set up a provider of the Background
layer. We used it within NodeDrawComponent,
which now continuously redraws Node
every frame, ready to react on any change.
This chapter was dedicated to drawing the Grid
: the fundamental piece of our game. Let’s stop for a moment and appreciate the path we have passed.
We started our first unsure steps by drawing the grid directly and “dirty” with the browser’s canvas API. We then established a structural hierarchy of the game by defining Grid
and Node
entities. We found it reasonable to make drawing logic a specific component of the Node
: NodeDrawComponent
. We make it possible to easily pass tuple data, like coordinates and sizes, thanks to the Vector2d
structure. Finally, we created a small rendering engine and layer system. It sure has been a long journey, but I hope you enjoined it!
Next time we start a new chapter. We will take a look at the core game mechanics and introduce even more entities and components. I cannot wait to see you there!
If you have any comments, suggestions, questions, or any other feedback, don’t hesitate to send me a private message or leave a comment below! If you like this series, please share it with others. It really helps me keep working on it. Thank you for reading, and I’ll see you next time!
This is Chapter III in the series of tutorials “Gamedev Patterns and Algorithms in Action with TypeScript”. Other Chapters are available here:
- Introduction
- Chapter I. Entity Component System
- Chapter II. Game loop (Part 1, Part 2)
- Chapter III. Drawing Grid (Part 1, Part 2, Part 3, Part 4, Part 5)
- Chapter IV. Ships (Part 1, Part 2, Part 3, Part 4)
- Chapter V. Input system (Part 1, Part 2, Part 3)
- Chapter VI. Pathfinding and Movement (Part 1, Part 2, Part 3, Part 4, Part 5, Part 6, Part 7)
- Chapter VII. State Machina
- Chapter VIII. Attack System: Health and Damage
- Chapter IX. Winning and Losing the Game
- Chapter X. Enemy AI