Building a Game With TypeScript. Colors and Layers

Greg Solo
Level Up Coding
Published in
8 min readOct 6, 2020

--

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

Design vector created by freepik

Great to see you back, reader! In this series of tutorials, we learn how to build a simple turn-based game from scratch using TypeScript, native browser’s APIs, SOLID architecture, incremental approach and unit testing.

This is the first installment of Chapter IV. This chapter will introduce us to the new element of the game: Ships. These are elements players can move around the board and attack other player’s Ships. The ultimate goal of the game is to destroy all enemy ships. If you are looking for other chapters of this series, you can find them here:

Feel free to switch to the drawing-grid-5 branch of the repository. It contains the working result of the previous posts and is a great starting point for this one.

We are not going to utilize high-fidelity graphics at this point. Our goal is to understand the principles and build systems, not to ship AAA games. So, our ships will look rather modest:

Yep, those circles are, in fact, ships

But let this not discourage you. The fact we have simple shapes to represent moving elements does not mean our system will be less powerful. We focus on programming aspects of the game and build a prototype of a sort. That doesn’t mean we can’t put high-quality graphics on top of the code we are building later on.

In the last chapters, we build quite a few utils to help us deal with many aspects of the game: Vector2D, Entities and Components, Lifecycle Events. We start to deal with drawings and colors quite extensively, so the time has come to introduce one more utility: Color helper.

Table of Contents

  1. Color Utility
  2. Testing Color Utility
  3. Drawing Circle
  4. Canvas Foreground Layer
  5. Conclusion

Color Utility

Background vector created by macro vector — www.freepik.com

Previously, we dealt with colors the way native canvas does it: as a string. But string is quite an unsafe structure and not very flexible. For example, take a look at how we set up Node color:

This string contains 4 different pieces of information: color setup for 4 color channels (Red, Green, Blue, Alpha). If we would like to change any channel, we have to do some weird string manipulation. Not to mention, we have zero validation or type checking and could accidentally set something rgva(245, 245, 245, 1) or even rgba(-245, 245, 2450, 10). Overall, it would be much better to have a dedicated data type for our colorful needs:

I hope you got used to updating barrel files every time we create a new thing:

We should be able to instantiate the Color class with explicit color channels, store and provide public read access:

However, we should verify the arguments first, and proceed with the instantiation only if they satisfy our constraints. To do so, we have to skip the handy TypeScript shortcut in this case and setup fields manually:

The validation itself is simple. We should verify that each number lays within reasonable range (0–255 for color channels, 0–1 for an alpha channel):

Also, the color channel should be an integer:

And now we can run the validation within the constructor:

Why is IsValidChannel static? Because provided validation rules have nothing to do with the instance of the Color, aka particular color. These rules are universal for the entire type.

Since Canvas API consumes strings, new Color helper should provide a way to convert its values to the string:

Let’s now update the Settings to contain Color instead of string:

Canvascan now work with Color rather than string too:

Which will require updating the canvas.spec.ts:

Nice! At this point, our code should successfully compile with npm start and all test should pass with npm t:

We use the handy little converter method AsString. Let’s take a quick detour and also add the opposite conversion functionality: FromString This static method will be able to convert string to an instance of the Color:

The method parses the provided string and validates that every component is indeed a number and then instantiates the new Color.

Testing Color Utility

Pattern photo created by freepik — www.freepik.com

Awesome! All is left for us is to cover our new helper with unit tests:

First, we can assert Color can be properly instantiated:

We simply verify that each channel of newly created Color has a proper value. Next, we should check if the validation works properly:

We feed the Color with an invalid option for every channel and insure every time it reports with a respective error.

Great! We covered the constructor and now we should move our focus on other methods:

AsString converter should take an instance of the Color and return a properly formatted string:

To verify Color.FromStringwe can provide a string and check each channel has a proper value:

Finally, we can check different invalid strings and expect errors to be thrown:

At this point, our code should successfully compile with npm start and all test should pass with npm t:

Awesome! Now, when we have Colorhelper set in place, we can finally get started with drawing ships. Or should I say, circles?

Drawing Circles

As I mentioned before, ships in our game are represented simply as circles. If you recall we have a wonderful tiny rendering engine, which we set up in previous chapters: Canvas. But we have no tool to draw anything aside from rectangles. Time to fix this inconvenience!

Background vector created by rawpixel.com

We need a new method that can draw a circle for us:

We painted rectangles using native canvas rect and fill it in. Then, we repeat this approach for a circle but thanks to the arc method:

And, of course, we should cover this new functionality with tests:

The same way we tested FillRect, we will spy on the native API and expect it to be executed after calling canvas.FillCircle:

Nice! At this point, our code should successfully compile with npm start and all test should pass with npm t:

Foreground Canvas Layer

Background vector created by Sketchepedia

The shape is not the only thing that differs ships from the grid. They always have to be drawn on top of the grid. In other words, they should be drawn at the top-level “layer”. But we have only one, “background” layer:

Not a problem! Adding a new layer is a trivial task:

The process is identical to the one we used for background: we instantiate the new Canvas of the proper size, awake it, and link it to the private field. You can imagine, that almost all of this code is repetitive. Let’s define a separate method to avoid repeating ourselves:

And now use this new method both in Background and Foregroundgetters:

Nice! But we have a slight problem here: these two canvases are identical. The only difference is their names: background vs foreground. Nothing actually makes foreground to lay on top.

Each canvas is, in fact, a DOM element. To put them on different “levels” we can use good ‘ol CSS: element with higher z-index gets closer to the viewer (assuming they belong to the same parent, which is exactly our case).

There are a plethora of ways how we can achieve that. One of them is to let Canvas handle the styling, while CanvasLayer only utilizes a specific rule. This way they will maintain their dedicates responsibilities: Canvas manages canvas DOM element, CanvasLayer creates layers of canvases.

To implement this behavior, we should provide a new API within the Canvas:

This method takes possible CSS options and applies them to the canvas element:

Note, that I use Partial to let consumers provide only a few possible options.

We can now update unit tests to verify this behavior:

And hook it up to CanvasLayer:

Awesome! Let’s add a cherry on top of this cake by testing new Foreground layer:

Testing Foreground methodology is absolutely the same as for Background: no matter how many times we request the layer, Canvas instantiation should happen only once.

At this point, our 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 ships-1 branch of the repository.

Conclusion

Cool! We made all the necessary preparations to set up a stage for drawing Ships. We created a convenient tool that helps us work with colors. Also, we updated our rendering engine to support a new primitive shape: circle. And of course, we introduced a new canvas layer: Foreground, a special place for our future ships to ensure they always stay on top of the image.

Next time we will talk about teams and opposition. We also are going to create a dedicated entity for the Fleet of Ships. That’s right: we will have our own fleet!

I would really love to hear your thoughts! 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 liked this article, 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 IV in the series of tutorials “Building a game with TypeScript”. Other Chapters are available here:

--

--

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