Building a Game With TypeScript. Drawing Grid 4/5

Greg Solo
The Startup
Published in
7 min readSep 1, 2020

--

Chapter III in the series of tutorials on how to build a game from scratch with TypeScript and native browser APIs

Watercolor vector created by brgfx

Welcome to part 4 of the Chapter III “Drawing Grid”! In previous parts of the chapter, we learned that drawing itself is simply a matter of calling proper canvas APIs of the browser. But the underlying logic of the Grid has to be a bit more sophisticated.

We divided this logic onto separate elements: Grid entity, Node entity and NodeDrawComponent. Each of them has its responsibility. We also presented the very basic Vector2D that helps us operate 2d structure.

In this article, we are looking to make a few more tweaks.

In Chapter III “Drawing Grid”, we are implementing a fundamental piece of our turn-based game: we are drawing the grid of nodes. Other Chapters are available here:

Feel free to switch to the drawing-grid-3 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

  1. Introduction
  2. Canvas Rendering Engine
  3. Testing Canvas
  4. Conclusion

Introduction

Last time we successfully rendered the grid on the screen:

However, if you check the dev tools of the browser, you can see that something fishy is going on here:

Apparently, we now render a bunch of canvases. Generally, it’s better practice to keep as little canvases as possible, and having dedicated canvas for every Node is definitely something we want to avoid. So, how should we go about it?

If you recall, we explicitly create a canvas DOM element on every Awake of NodeDrawComponent:

We had to do this because we require access to canvas context to draw anything:

Awake happens once for every Node we create. Thus, the number of canvases now is equal to the number of nodes.

We could create a canvas DOM element somewhere else to fix this situation. For example, we could do that in the Grid before the loop and then pass the reference of the context to the Node:

This would require Node to change its constructor and expect the fourth parameter, Ctx :

And then NodeDrawComponent could access the Ctx from the Node entity. And they would live happily ever after…

I have a problem with this approach, however. In this scenario, Node becomes tightly coupled with the notion of canvas and drawing. Indeed, Ctx becomes required parameter of the Node.

But as we determined earlier, being able to be drawn is only one of its numerous components, not necessarily a core feature. We discussed that Node could potentially even be invisible, lacking the Draw component whatsoever. Also, I would rather keep code that deals with the browser’s API (like document.createElement or ctx.beginPath) independent of the game code.

Ideally, we want some layer of abstraction that can handle communication with the underlying rendering platform. And make only NodeDrawComponent interact with it. For our purposes, I will call this layer a rendering engine.

Canvas Rendering Engine

Infographic vector created by macrovector official

I start by defining a dedicated utility class that holds the responsibility of dealing with Canvas API:

Browser requires width and height to create canvas DOM element, hence they should be required by our engine too. We can reuse Vector2d to represent this data as a “tuple” of a sort:

Traditionally, we should set up barrel files:

Our rendering engine has to create a canvas element to draw on it. It would be convenient to preserve a reference to this element, as well as the rendering context of it. Canvas is the only one who can change them, but the outside world should have read-only access. To implement this, I define private fields with public getters:

We could create a canvas in the constructor. But, as we discussed a few articles ago, it’s better to keep a constructor lean. And manipulating the DOM can be quite expensive, so we better do it someplace else.

If you recall, we have an excellent mechanism to deal with all initialization logic. Please welcome, our good old friend, Awake method:

Note, we double-check that context actually exists. Otherwise, we report an error

Nothing stops us now from defining a method that draws a rectangle:

Everything should look familiar. We basically copied code from NodeDrawComponent. Coordinates and size, however, are arguments of the function now. Nice and clean API!

Finally, let’s create a method that allows the cleanup of a rectangle. We will need it to make sure a particular area has no stale drawings:

Awesome! Yet, it is a rather humble rendering engine at this point. Of course, it may have much, much more functionality. But following the incremental approach, we implement features when we need them, keeping the options open for further extension.

Testing Canvas

Background vector created by freepik

We are in a good position to test this little rendering engine. I create a spec file and make a basic setup:

We have quite a few things to verify. First, we should check Canvas creates and attaches to the DOM a new element when it awakes:

I start by spying on native DOM API: createElement and appendChild:

And then check both spies get called after Canvas awakes:

Awesome! Your code should compile with npm start and tests should pass with npm t:

But that’s just half of the story. We have two more methods to check: FillRect and ClearRect.

We can cover them under one umbrella, API, since both methods are primary API of this class:

In both cases, Canvas must awake first, so I use beforeEach here to make that happen.

I start with ClearRect because it’s slightly easier to test. The approach should sound familiar: first, I spy on something, then trigger something and then expect something to happen.

In this case, I spy on the native clearRect function. Then execute our ClearRect method and expect a spy to be called:

Testing FillRect follows the same approach. The difference is only in the number of native functions we expect to be triggered:

Cool! At this point, your code should compile again 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-4 branch of the repository.

Conclusion

Nice! In this post, we created our own little rendering system, the abstraction layer on top of the browser’s canvas API. But how can we wire it up with the NodeDrawComponent? And how can we make sure we won’t couple drawing logic with the Node entity?

We will look into that in our final post of the Chapter III “Drawing Grid”. We also will talk about canvas z positioning and how we can make sure one independent image is drawn on top of another. Last but not least, we will make sureNode drawings are always stay fresh and up-to-date.

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! Thank you for reading, and I’ll see you next time!

This is Chapter III in the series of tutorials “Building a game with TypeScript”. Other Chapters are available here:

--

--

Greg Solo
The Startup

Software Engineer. Immigrant. Entrepreneur. I have been telling stories through software for 15 years in the hope to craft a better future