Building a game with TypeScript. Input system 1/3
Chapter V in the series of tutorials on how to build a game from scratch with TypeScript and native browser APIs

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:
- Introduction
- Chapter I. Entities and Components
- 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
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
- “Active” Node
- Listening to Events
- Node, the Occupant
- Point of click
- Global and Local
- 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.

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.

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 canvas
is:

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:
- Introduction
- Chapter I. Entities and Components
- 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