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

Welcome back, reader! Last time we configured the Node
entity to listen to click events on the body
DOM element. While this gave us what we wanted: a way to be notified when the event occurs, it was far from being efficient. Indeed, if we run our code and add debugger
or console.log
we can notice that we react to the event multiple times:
We capture a click 36 times (the number of Nodes
within the game right now):

This is not particularly terrible since we still can narrow it down to a specific Node
by checking if the point of click is within the area this node Occupies
. But even without this check 36 event listeners is hardly a performance bottleneck for any modern browser. However, the problem can intensify if we have other parts of the game that should listen to the clicks as well. And the issue is not really about performance, at least until we have thousands of such elements (which, to be honest, is quite feasible in case we build a more complex game). The problem is more that, with this design, we have to duplicate the code from Awake
for every single such element. We can do better, and in this installment, we are going to discuss how can we refactor our code with more sound architecture.
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
input-1
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
- Input Game Component
- Quick Housekeeping
- Onclick Component
- Onclick, The Abstract Component
- Conclusion
Input Game Component
The reason we have so many event handlers is that we attach them for every Node
. This is even more bizarre since from a DOM perspective we actually listen to the entire body
.

We have only one body
so we can move the listener up in the hierarchy of entities: all the way up to the Game
. It is hardly a core characteristic of the Game
itself, rather a component we can turn on or off:
IComponent
interface requires us to implement Awake
and Update
, as well as defining Entity
. Also, we should not forget about barrel files:
Great! We can now start listening to the click events as soon as the component awakens:
Nice! Since we have only one Game
entity we can be sure that we don’t add any redundant listeners.
HandleClick
has some important duties. First, it must insure click happens within the game itself:
This should look familiar. We send the point of click to the canvas
to analyze if this point even belongs to it. If the response is negative, we ignore this event.
Note, that we use the
Background
layer. As we discussed last time, it does not really matter which layer we check because they are effectively occupying the same area. In more complex scenarios, when the layer differs in location and/or we care about the Z coordinate, the check would be more sophisticated. Fortunately, we can always extend this component in time of need.
To check if this works, let’s add console.log
right after the point
check:
But if we run the game with npm start
and open a browser, we will see no logs aside from the old ones coming from Node
:

That is because we forgot to attach our new GameInputComponent
to the Game
:
And now you should see the ‘game component click’ log message for every click within the canvas along with the “old” 36 messages. Note, the game ignores events outside the canvas boundaries:

Perfect, just as we wanted. We have one centralized listener for all clicks, which filters out all useless events. But we have no mechanism to notify actual responders about the event. After all, how would Node
know that click happens?
Quick Housekeeping
Before we jump into solving this case, lets cleanup a bit. First, we can simplify game tests
radically. If you recall, we added a bunch of fake components when tested Game
entity a few episodes ago.

Now, when we have a real Input Component
attached, we can get rid of the fakes:
I simply removed all code concerned with fake components C1
, C2
, C3
and instead added a test to verify Game
has a real GameInputComponent
attached and running.
Also, let’s define a dedicated mock for the Game
itself, we will need it soon:
Traditionally, update the barrel:
And use the mock within the test:
At this point, our code should successfully compile with npm start
and all tests should pass with npm t
:

Finally, allow me to get rid of the “dirty” code we have added last time. We already implemented most of it anyway:
Alright, we are done with the cleanup. So, how can Nodes
, Ships
, and others get notifications about fired events? There are many ways how we can achieve this.
We could go with one of my favorite patterns: Observer
. To implement this pattern we would have to store the list of all items that ought to be notified by GameInputComponent
. This is an incredibly powerful approach, however, we have to update this list every time we want new items to start listening to the event. Or, on contrary, make items stop listening. This functionality may require these items to be aware of GameInputComponent
.
I would rather avoid this kind of coupling and instead suggest we once again make use of our Entities and Components. We can define a new component, let’s say the Onclick
component, and simply notify all Game
children that have this component about the event:
So, any child of the Game
can “subscribe” to these notifications by simply attaching OnclickComponent
. Of course, TypeScript complaints about non-existing OnclickComponent
. And it has all the right to do so. We are yet to define the component itself, but here is a catch.
Onclick Component
Previously, all our components belonged to some Entity
. GameInputComponent
clearly belongs to Game
entity. NodeDrawComponent
cannot fathom its existence without Node
.
OnclickComponent
is a different beast. It does not care about a specific entity at all. Its only purpose is to promise the “entity, who attaches me, is interested in the click event and must react on it”. Simply put, it guarantees the existence of the public method ClickOn(point: Vector2D)
.

Naturally, it is an interface
but slightly stricter than IComponent
. It requires implementation of ClickOn
method:
Since it does not belong to any entity we can place it under utils
. Let’s remind ourselves about barrel files:
But what exactly the purpose of this interface? The thing is, we don’t know what entities are going to utilize click functionality. And most importantly, the GameInputComponent
does not know it either. And should not.
Imagine we want Node
to be notified of the clicks. We can create a specific NodeOnclickComponent
that implements IOnClickComponent
interface. Similarly, we can define ShipOnclickComponent
if we want Ship
to be notified. But GameInputComponent
will only be aware of IOnClickComponent
and call anyone who implements this interface. This effectively decouples GameInputComponent
from any logic regarding react to the event. Even better: GameInputComponent
does not have to even keep track of “subscribers”
Alas, it doesn’t work. TypeScript keeps throwing a pesky error at us: 'IOnClickComponent' only refers to a type, but is being used as a value here
.
Indeed, the interface is only a type while both HasComponent
and GetComponent
require an actual thing. If you recall, our Entity Component System is set up to work in runtime, which gives us the flexibility to add and remove components while the game runs. And in the runtime, there are no interfaces left, so our ECS has virtually nothing to work with.
Well, how can we solve this puzzle? ECS needs some “real” thing like function or class or object. We can convert our IOnClickComponent
into a class:
We can remove redundant onclick.h
file and update the barrel:
But you may notice we have to implement Awake
and Update
per the IComponent
requirement. This is annoying and, more importantly, incorrect from an architectural perspective. OnclickComponent
has nothing to do with Awake
or Update
. Moreover, as we discussed earlier, we expected future components (like ShipOnclickComponent
, NodeOnclickComponent
etc) to implement IOnClickComponent
. ClickOn
is supposed to be overwritten too.
If that was not enough, the entire class OnclickComponent
by itself exists only to be extended. We don’t expect to have an instance of this class. Fortunately, there is a special tool in the OOP world for this job: abstract class.
Onclick, The Abstract Component

Indeed, we can simply mark this class and all methods we expect to be overwritten as “abstract” and delegate implementation to the future “specific” classes:
Great! Now we can update GameInputComponent
to work with this abstract class instead of interface:
Aaaaand… it does not work either. We got rid of one TS error only to get another: Argument of type 'typeof OnclickComponent' is not assignable to parameter of type 'constr<OnclickComponent>’
. What does it even mean?!
Well, don’t be alarmed. TypeScript only tries to help us here, by forcing us to make reasonable, thoughtful choices. In this case, it signals that OnclickComponent
well, not a “usual” class. You see, when we designed our Entities Component System we never thought the component could be an abstract class. Take a look at this code:
Type constr<T>
clearly sets an expectation for the component to be regular (read: “instantiable”) class. Keyword new
here is exactly for this reason. But abstract
class, by definition, cannot be instantiated.
So, what we can do, is to extend our options a bit and allow Entity
to expect a component in the form of abstract class:
It does not mean we can attach an abstract class directly to the Entity
but we can check, lookup, and even remove a component that extends some abstract class. Exactly what we need right now!
How does this
AbstractComponent<T>
type works? To understand, we have to recall a few things about classes in JavaScript. First,class
is essentially a syntax sugar around theconstructor
function. When we run a function with thenew
keyword, we run it in a “context” of constructing an object. Second, JavaScript uses a prototyped inheritance, which is a long and interesting topic on its own. To keep things simple, let me just say that a special reservedprototype
property of an object holds a reference to a parent constructor. When we sayFunction & { prototype: T }
we mean that this “thing” must be a function AND should have a specificT
type of parent constructor. In other words, it has to extend thatT
.
As soon as we update Entity
error disappears, and we should be able to compile and run all tests again:

You can find the complete source code of this post in the
input-2
branch of the repository.
Conclusion
We did a tremendous job converting out “dirty” code from the previous installment into a more robust system. We started by moving addEventListener
to the top of the “food chain”: all the way to the Game
entity, introducing GameInputComponent
. Also, we quickly updated the way we test the Game
entity by getting rid of redundant fake components and checking real-life ones in action.
We then spent a great deal of time talking about OnclickComponent
. This component is responsible for making sure those who are interested in the click event will get their message. This discussion led us through different approaches: interface, regular class, and finally abstract class.
All this is great. Yet, we still need to define an actual component that shall extend OnclickComponent
and actually react to the event. We also neglected the tests of GameInputComponent
. We will take care of all this in the last installment of Chapter V.
I would 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 N in the series of tutorials “Building a game 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