Level Up Coding

Coding tutorials and news. The developer homepage gitconnected.com && skilled.dev && levelup.dev

Follow publication

Building a Game With TypeScript. Input system 2/3

Greg Solo
Level Up Coding
Published in
10 min readDec 29, 2020

--

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

Cloud vector created by vectorjuice — www.freepik.com

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:

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

  1. Introduction
  2. Input Game Component
  3. Quick Housekeeping
  4. Onclick Component
  5. Onclick, The Abstract Component
  6. 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.

Technology vector created by macrovector — www.freepik.com

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.

Business vector created by jemastock — www.freepik.com

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).

Marketing vector created by stories — www.freepik.com

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

Pattern photo created by wirestock — www.freepik.com

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 the constructor function. When we run a function with the new 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 reserved prototype property of an object holds a reference to a parent constructor. When we say Function & { prototype: T } we mean that this “thing” must be a function AND should have a specific T type of parent constructor. In other words, it has to extend that T.

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:

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Written by Greg Solo

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

Responses (1)

Write a response