Tutorial 7
HUD Text
In this tutorial we’ll cover adding text to our HUD and how to properly update
that text as things happen in our game. To do this, we’ll create an HUDTextSystem
that will make use of engo.Mailbox
to get changes from other systems. We’ll
also go over putting text on the screen, how to use text files, and how to change
the displayed text as the game changes.
Recap
Remember what we did in the last tutorial?
We used a sprite sheet to add city sprites to our game, and then we made our CityBuildingSystem automatically generate cities as time went on.
Final Code
The final code for tutorial 7 is available here at GitHub.
Adding Text to the Game
First, we’ll need to add text to our game. We’re going to use golang’s own font,
as it is free to use and doesn’t require a download. We can get it with
go get golang.org/x/image/font/gofont/gosmallcaps
. We’ll then import it on our
traffic.go file. We can then use it by adding it to our scene’s Preload. However,
we have to add it differently than we do with assets we have on file, since it’s
already binary data. Fortunately, engo has a way of dealing with this via
engo.Files.LoadReaderData(url string, f io.Reader)
. We’ll just need to do a
little work to get our font as an io.Reader, then we can load it. The url passed
to this function can be any string, it’s just a name to give it so we can access
it later, and actually has nothing to do with the location of any file. It does,
however, have to have the proper extension, or engo won’t know what kind of data
it is.
We’ll do this by adding this to our scene’s Preload:
Now that we have the text file loaded, we’re going to create a new scene that
handles all the text on our HUD. First, we’ll create a new file called hudText.go
in our systems folder. Once we have that, in our HUDTextSystem’s New, we’ll add
our text to the screen.
To add text to the screen, we first need a font. A font is the combination of a
font resource file, as well as the size and color of the font. If you want to use
a different size or color from the same resource, you’ll have to create another
font. After you’ve made a font, you’ll have to use &common.font.CreatePreloaded()
in
order to generate the textures and atlas needed to actually use it.
Now that we’ve created the font, we can use it to create a RenderComponent that
can be used to render our text to the screen. This is common.Text
, and it
can be used as our RenderComponent’s Drawable. Once this is done, our entire hudText.go
file will look like
Don’t forget to add the system to the world. In our scene’s Setup make sure you
add world.AddSystem(&systems.HUDTextSystem{})
. When that’s done, if you run
your game, you should see “Hello, world!” in our HUD!
Communicating Between Systems
When a new city is added and we mouse over it, we’ll want our HUD text to tell
us some information about the city, such as the revenue or how satisfied the
people there are with our traffic management. To do this, we’ll need to collect
information on the city from other systems. Our CityBuildingSystem, for example,
needs to tell our HUDTextSystem which tiles cities are on. In engo, we can do this
by utilizing the engo.Mailbox
. The Mailbox lets you listen for messages in one
system and send the messages out from another! This allows one system to update
itself based on messages sent from other systems.
Implementing the Message Interface
To be able to use the Mailbox, we’ll have to create a struct that implements the
engo.Message
interface. This only requires Type() string
, so we can do this
fairly easily. Our HUD text will change based on the game tile our mouse is
hovering over, so we’ll need to know what tile that is, as well as what to change
our message to while over that tile. To do this, we’ll create a struct HUDTextMessage
that contains a common.SpaceComponent
which tells us where the tile is, as
well as the lines printed to the HUD. We’re also going to add entities to our
system that keeps track of which tiles contain what message. We’ll keep track
of the ecs.BasicEntity
in case a tile ever needs to be removed, and the
common.MouseComponent
will tell us when a tile is clicked.
We’ve gone from just one line of text to 4 lines of text as well as a line that shows players how much money the have available. We’ll need to update our New() for this. Rather than the one text field, replace it with the additional ones
Listening for Messages
Now that we have our message, we can subscribe to it in our system’s New(). To
subscribe to a message, we use
engo.Mailbox.Listen(messageType string, handler engo.MessageHandler)
.
messageType is just the same string returned by your message’s Type(), in this
case we made a constant HUDTextMessageType that we’ll use. handler is the callback
function that is called whenever a message is sent. It’s a function that has the
signature func(engo.Message)
.
The first thing our Listen function does is assert from engo.Message
, which
is a generic interface that only has access to the Type() string
function. We
want to get all the fields on our struct, so we have to assert it to the right
type. We use ok to check if the underlying type is correct. If it isn’t, we don’t
have anything to do with this message. After that, we add our entity to the
common.MouseSystem
so it’ll register clicks as well as our HUDTextSystem
.
Dispatching Messages
Now that we have our message listener, we’re going to dispatch messages to it.
To do this, we’ll use engo.Mailbox.Dispatch(message engo.Message)
. In our
CityBuildingSystem, let’s add this to the generateCity()
function.
Now, whenever a city is built, we update our HUDTextSystem that a city was just built on that tile.
Updating the HUDTextSystem
Now that we have the city locations, we can update the text for our HUD based on
where the player clicks. If a tile is clicked, we want to change the text lines
to whatever the entity there has. One key thing to note here is that the
RenderComponent.Drawable
isn’t a pointer; when we get it we’re just getting a
copy. To set it properly, we have to set it at the end. Hence all the
RenderComponent.Drawable = txt
even though we do txt = RenderComponent.Drawable
.
Keeping Track of Money
Now, since we’re doing messages, let’s add another system to our game that is heavily message based. We’re going to add a system for keeping track of our money. Doing things in multiple systems as opposed to having one system do everything makes our code easier to read, bugs easier to fix, and helps us update things easier down the road.
Money in this game is going to be used to build new roads and hire police to keep our roads safe. We’ll earn money every so often from each city based on its size and collect fines due to traffic violations. To do this, we’ll need a system that keeps track of how many cities there are, how many police we have, and (when those systems are available) when we build roads and collect fines. We will do all of this using messages.
For now, let’s create a file money.go
in our systems folder. We’re going to use
the system to keep track of different types of cities, how many there are, the
amount of money we have, and the time since the last “day”.
Now we’ll need to add a couple messages to this, so it can update them as the
cities change and officers are added. We’ll start with the cities. We’ll also
use go’s version of an enum, iota
to create a type to distinguish between
the different types of cities. Our CityUpdateMessage will include an Old and New
so we can grow or shrink the city dependent on the game conditions down the road.
We’re also going to use a message to update the number of officers the player has. That’s just going to be a basic message with no properties in the struct. The number of officers will increase when the player adds them, so all we need to do is listen for this message and increase the number of officers by one whenever it is called.
Now that we have the messages we need, we’ll put listeners in our MoneySystem
’s
New(). The CityUpdateMessage
listener will add one to the message’s New property
and subtract one from the message’s Old, so when a city goes from one state to
another our system reflects that. The AddOfficerMessage
just increases the number
of officers by one.
Now for the rest of the system, we want to update our amount of money based on the number of cities and officers. We’re going to do this every ten seconds. To do this, we’ll keep track of the elapsed by adding dt from our Update to it each frame.
In our update, we’re updating the amount of money displayed every 10 seconds. To
do this, we’re going to have to change our hudText system to listen for the
HUDMoneyMessage
so that it can update the text to reflect these changes. To do
this, we’ll create the HUDMoneyMessage
struct in hudText.go
Then in our HUDTextSystem
we’ll have to add a couple properties:
updateMoney bool
will let us know if the amount has been updated, so we only
update the text when the amount has changed, and amount int
to keep track of
how much money to display. Once those are added, we want to put a listener in
New()
to adjust these whenever the message is received.
All that’s left for our HUDTextSystem
is to handle the changes in our Update()
.
At the end of the Update()
, after looping through all the entities we’ll put:
Finally, at the end of generateCity()
in our citybuilding.go
file, we want to
dispatch a message to our MoneySystem
so it can update the number of cities
when one is generated.
Now we have working HUD Text! It changes based on where you click the mouse, and updates to let the player know how much money they have access to. We learned how to communicate between systems so we can modularize our code and make simple systems that only do one thing. This is great for debugging and adding features as we make our game more complex. Next time, we’re going to add a system that allows players to build roads between their cities, set speed limits, and hire officers to keep those roads safe.