Building a game with TypeScript. Input system 1/3

Greg Solo
ITNEXT
Published in
9 min readDec 17, 2020

--

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

Design vector created by freepik — www.freepik.com

Hello there! Welcome to Chapter V of a series of tutorials “Building a game with TypeScript”! Here we learn how, using native browser APIs, plain TypeScript, Test Driven Development, and SOLID design patterns, to build a simple turn-based game.

We spent the last chapter talking about Ships: we learned how to draw them utilizing our little Render system, talked about conflicts and teams, introduced a few helpers like Color and Fleet.

But so far, the game was rather… dead. Sure, we rendered quite a few elements but our Players have no way to interact with the game. The time has come to fix this unfortunate overlook! In Chapter V “Input system”, we are going to build a simple system that will give the Player the opportunity to communicate with the game. You can find other Chapters of this series here:

Feel free to switch to the ship-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

  1. “Active” Node
  2. Listening to Events
  3. Node, the Occupant
  4. Point of click
  5. Global and Local
  6. Conclusion

“Active” Node

Of course Player may have a whole spectrum of ways to interact with the game. But primarily, the Player should be able to move the currently active Ship to a specific Node by clicking on this node. That is the core gameplay of our game:

Now, plenty of logic behind this feature is way beyond the scope of this chapter, like which Ship is considered active, which Node is available to click according to the range of the Ship movement, progressive animated movement of theShips and so on. We will cover these topics in future installments, for now, our focus is the actual fact of interaction. In other words, we are going to build a system that can notify different parts of our game that the Player has clicked on something. We are not concerned about keystrokes or gestures or pretty much any other type of input since the core gameplay here is about clicking things.

To confirm that our Input system works, let’s use an oversimplified partial version of the gameplay. Let’s just pretend Node can be activated by clicking on it. Depending on the state, Node will be colored differently:

Again for this chapter, we are ignoring many things: can Player actually click the Node, how exactly should Node react on it, etc. We simply highlight Node as soon as it gets clicked.

To accomplish this, we can start by introducing the IsActive property of the Node entity. This is a temporary solution, and we will get rid of it in future chapters:

Next, lets set up dedicated colors for active and inactive states of the Node:

Finally, let’s teach NodeDrawComponent to respect this state:

And of course, we should update tests to reflect these changes:

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

Now, the game has to have a way to identify which particular Node has been clicked. What can be easier? Let’s just add a good old event listener to the Node and be done with it!

Listening to Events

Well, unfortunately, it’s not that easy. addEventListener works with DOM nodes. But, if you recall, there are no DOM elements within canvas. It loses track of the drawn elements immediately after it draws them. This is great because it allows the browser not to keep information about all these circles and rectangles in the memory. But unfortunately for us, it means we have to find a way to track events manually.

The music vector created by rawpixel.com — www.freepik.com

Hopefully, we don’t have to recreate the entire event system. As I mentioned, gameplay focuses only on the “click” event, and we can disregard every other input. Yet still, how can we listen to clicks without DOM?

Let’s separate this problem into two parts. First, we have to track clicks on a specific element of the Game: Ship or Node for instance. Second, we need a way to notify these elements that click happens. We don’t care how exactly the element is going to react to the event, that’s the responsibility of the element itself.

To track the fact that a Player clicks a specific element, we need to know:

a) the point of click: “position” of the mouse at the moment of click

b) position of any element.

If the point of click is “within” the area occupied by some element, it is safe to assume that the click was made on this element. There is also a complication of z-space (some elements can be behind the other, meaning it would be impossible for users to click on them), but for the purpose of the game, we can disregard this nuance.

Node, the Occupant

Our Node entity has Start and End properties that store information about the position of the Node. If the point of click happens within the rectangle between Start and End, that means Player clicks on this Node. We can define a helper method to ease things a bit:

Method Occupies simply checks if the provided point is indeed within the area of this particular Node. Let’s cover it with tests quickly:

Remember we defined this test Node to start at point (1, 2) and end at (5, 6)? So, naturally, points (6, 2) and (3, 7) should be outside the Node area while (3, 2) should be inside.

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

Point of Click

But how can we identify the actual point of click? Well, even though canvas has no notion of DOM nodes we still can take advantage of a beautiful DOM event system. Instead of waiting for an event on every single element of the game, we can listen for an event on the top DOM element: body.

The arrow vector created by macrovector — www.freepik.com

This will effectively give us a mechanism to be notified when any click happens. We then can locate where exactly Player clicks by comparing the location of the mouse at the moment of the click with the area occupied by Nodes, Ships, or any other element. And luckily for us, the browser nicely provides us access to this point within MouseEvent which is a parameter of addEventListener callback for mouse events.

For example, we can start by adding an event listener within the Node entity:

As soon as Node awakes it starts listening to all clicks on the page. But it cares only about those of them that happen within the area occupied by Node:

Here we get a point of click from MouseEvent and check if it’s indeed within Node boundaries. If so, we then make this Node active.

Nicely done! However, when you start the game in your browser and try to click any Node you may notice that something is off:

For some reason, the game highlights arbitraryNode instead of the one we click. The point of click is the one to blame for this wonky behavior.

If you recall, we set up the position of the elements, Nodes and Ships, to be relative to the canvas. In other words, it is a local coordinate, while MouseEvent gives us a global coordinate. Global coordinate system starts at the top-left corner of the browser. Our local coordinate system starts where canvas starts. The more offset there is betweencanvas and the edge of the browser, the more irrelevant e.clientX and e.clientY become.

Global and Local

What we need is a way to transform a global point of click to align it with our local coordinate system:

Calculations are trivial: we simply consider the offset of this canvas from the top-left corner of the browser, accounting for scrolling. If the provided global point is away from canvas we return null.

Note, since we have multiple canvas layers, these calculations are valid for a specific canvas. But because all our layers start at the same point, we are safe.

Of course, we should cover this functionality with tests. We can mock getBoundingClientRect to fake the position of the canvas, effectively pretending that it starts at the (20,20) and ends at the (500,500) in the global coordinate system:

We have two cases to test. First, we must ensure that our method returns null if the provided point is not even within the canvas boundaries:

Secondly, we can check that global point is successfully converted to local system:

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

Finally, we can apply CalcLocalPointFrom to transform point of click:

And now Nodes should be highlighted properly, no matter how big the offset of the canvasis:

Conclusion

Awesome job! We did the very first, “dirty” round of code for our little “input system”. A plethora of questions remains unanswered. We are listening to the event on every single Node. Which means, we react to the same event 36 times (that’s the number of Nodes we have now)? Is there a better way?

Also, what if we have something else that is clickable, not just Node? Do we have to repeat the same code we just wrote within Awake for every element we want to click? We will start answering all these questions next time. See you then!

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 enjoy 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 V “Input system” 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