engo


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:

engo.Files.LoadReaderData("go.ttf", bytes.NewReader(gosmallcaps.TTF))

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.

fnt := &common.Font{
  URL:  "go.ttf",
  FG:   color.Black,
  Size: 24,
}
fnt.CreatePreloaded()

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

package systems

import (
	"image/color"

	"engo.io/ecs"
	"engo.io/engo"
	"engo.io/engo/common"
)

// Text is an entity containing text printed to the screen
type Text struct {
	ecs.BasicEntity
	common.SpaceComponent
	common.RenderComponent
}

// HUDTextSystem prints the text to our HUD based on the current state of the game
type HUDTextSystem struct {
	text Text
}

// New is called when the system is added to the world.
// Adds text to our HUD that will update based on the state of the game.
func (h *HUDTextSystem) New(w *ecs.World) {
	fnt := &common.Font{
		URL:  "go.ttf",
		FG:   color.Black,
		Size: 24,
	}
	fnt.CreatePreloaded()

	h.text = Text{BasicEntity: ecs.NewBasic()}
	h.text.RenderComponent.Drawable = common.Text{
		Font: fnt,
		Text: "Hello, world!",
	}
	h.text.SetShader(common.TextHUDShader)
	h.text.RenderComponent.SetZIndex(1001)
	h.text.SpaceComponent = common.SpaceComponent{
		Position: engo.Point{X: 0, Y: engo.WindowHeight() - 200},
		Width:    200,
		Height:   200,
	}
	for _, system := range w.Systems() {
		switch sys := system.(type) {
		case *common.RenderSystem:
			sys.Add(&h.text.BasicEntity, &h.text.RenderComponent, &h.text.SpaceComponent)
		}
	}
}

// Update is called each frame to update the system.
func (h *HUDTextSystem) Update(dt float32) {}

// Remove takes an enitty out of the system.
// It does nothing as HUDTextSystem has no entities.
func (h *HUDTextSystem) Remove(entity ecs.BasicEntity) {}

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.

// HUDTextMessage updates the HUD text based on messages sent from other systems
type HUDTextMessage struct {
	ecs.BasicEntity
	common.SpaceComponent
	common.MouseComponent
	Line1, Line2, Line3, Line4 string
}

const HUDTextMessageType string = "HUDTextMessage"

// Type implements the engo.Message Interface
func (HUDTextMessage) Type() string {
  return HUDTextMessageType
}

// HUDTextSystem prints the text to our HUD based on the current state of the game
type HUDTextSystem struct {
  text1, text2, text3, text4, money Text

  entities []HUDTextEntity
}

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

h.text1 = Text{BasicEntity: ecs.NewBasic()}
h.text1.RenderComponent.Drawable = common.Text{
  Font: fnt,
  Text: "Nothing Selected!",
}
h.text1.SetShader(common.TextHUDShader)
h.text1.RenderComponent.SetZIndex(1001)
h.text1.SpaceComponent = common.SpaceComponent{
  Position: engo.Point{X: 0, Y: engo.WindowHeight() - 200},
}
for _, system := range w.Systems() {
  switch sys := system.(type) {
  case *common.RenderSystem:
    sys.Add(&h.text1.BasicEntity, &h.text1.RenderComponent, &h.text1.SpaceComponent)
  }
}

h.text2 = Text{BasicEntity: ecs.NewBasic()}
h.text2.RenderComponent.Drawable = common.Text{
  Font: fnt,
  Text: "click on an element",
}
h.text2.SetShader(common.TextHUDShader)
h.text2.RenderComponent.SetZIndex(1001)
h.text2.SpaceComponent = common.SpaceComponent{
  Position: engo.Point{X: 0, Y: engo.WindowHeight() - 180},
}
for _, system := range w.Systems() {
  switch sys := system.(type) {
  case *common.RenderSystem:
    sys.Add(&h.text2.BasicEntity, &h.text2.RenderComponent, &h.text2.SpaceComponent)
  }
}

h.text3 = Text{BasicEntity: ecs.NewBasic()}
h.text3.RenderComponent.Drawable = common.Text{
  Font: fnt,
  Text: "to get info",
}
h.text3.SetShader(common.TextHUDShader)
h.text3.RenderComponent.SetZIndex(1001)
h.text3.SpaceComponent = common.SpaceComponent{
  Position: engo.Point{X: 0, Y: engo.WindowHeight() - 160},
}
for _, system := range w.Systems() {
  switch sys := system.(type) {
  case *common.RenderSystem:
    sys.Add(&h.text3.BasicEntity, &h.text3.RenderComponent, &h.text3.SpaceComponent)
  }
}

h.text4 = Text{BasicEntity: ecs.NewBasic()}
h.text4.RenderComponent.Drawable = common.Text{
  Font: fnt,
  Text: "about it.",
}
h.text4.SetShader(common.TextHUDShader)
h.text4.RenderComponent.SetZIndex(1001)
h.text4.SpaceComponent = common.SpaceComponent{
  Position: engo.Point{X: 0, Y: engo.WindowHeight() - 140},
}
for _, system := range w.Systems() {
  switch sys := system.(type) {
  case *common.RenderSystem:
    sys.Add(&h.text4.BasicEntity, &h.text4.RenderComponent, &h.text4.SpaceComponent)
  }
}

h.money = Text{BasicEntity: ecs.NewBasic()}
h.money.RenderComponent.Drawable = common.Text{
  Font: fnt,
  Text: "$0",
}
h.money.SetShader(common.TextHUDShader)
h.money.RenderComponent.SetZIndex(1001)
h.money.SpaceComponent = common.SpaceComponent{
  Position: engo.Point{X: 0, Y: engo.WindowHeight() - 40},
}
for _, system := range w.Systems() {
  switch sys := system.(type) {
  case *common.RenderSystem:
    sys.Add(&h.money.BasicEntity, &h.money.RenderComponent, &h.money.SpaceComponent)
  }
}

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

engo.Mailbox.Listen(HUDTextMessageType, func(m engo.Message) {
  msg, ok := m.(HUDTextMessage)
  if !ok {
    return
  }
  for _, system := range w.Systems() {
    switch sys := system.(type) {
    case *common.MouseSystem:
      sys.Add(&msg.BasicEntity, &msg.MouseComponent, &msg.SpaceComponent, nil)
    case *HUDTextSystem:
      sys.Add(&msg.BasicEntity, &msg.SpaceComponent, &msg.MouseComponent, msg.Line1, msg.Line2, msg.Line3, msg.Line4)
    }
  }
})

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.

engo.Mailbox.Dispatch(HUDTextMessage{
  BasicEntity: ecs.NewBasic(),
  SpaceComponent: common.SpaceComponent{
    Position: engo.Point{X: float32((x + 1) * 64), Y: float32((y + 1) * 64)},
    Width:    64,
    Height:   64,
  },
  MouseComponent: common.MouseComponent{},
  Line1:          "Town",
  Line2:          "Just built!",
  Line3:          "A town generates",
  Line4:          "$100 per day.",
})

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.

for _, e := range h.entities {
  if e.MouseComponent.Clicked {
    txt := h.text1.RenderComponent.Drawable.(common.Text)
    txt.Text = e.Line1
    h.text1.RenderComponent.Drawable = txt
    txt = h.text2.RenderComponent.Drawable.(common.Text)
    txt.Text = e.Line2
    h.text2.RenderComponent.Drawable = txt
    txt = h.text3.RenderComponent.Drawable.(common.Text)
    txt.Text = e.Line3
    h.text3.RenderComponent.Drawable = txt
    txt = h.text4.RenderComponent.Drawable.(common.Text)
    txt.Text = e.Line4
    h.text4.RenderComponent.Drawable = txt
  }
}

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

// MoneySystem keeps track of money available to the player
type MoneySystem struct {
	amount                int
	towns, cities, metros int
	officers              int
	elapsed               float32
}

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.

// CityType keeps track of the type of city
type CityType int

const (
	// CityTypeNew is a brand new city
	CityTypeNew = iota
	// CityTypeTown is a town, the lowest level
	CityTypeTown
	// CityTypeCity is a city, the moderate city type
	CityTypeCity
	// CityTypeMetro is a metro area, the largest city type
	CityTypeMetro
)

// CityUpdateMessage updates the city types when sent from Old to New
type CityUpdateMessage struct {
	Old, New CityType
}

// CityUpdateMessageType is the type of the CityUpdateMessage
const CityUpdateMessageType string = "CityUpdateMessage"

// Type implements the engo.Message interface
func (CityUpdateMessage) Type() string {
	return CityUpdateMessageType
}

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.

// AddOfficerMessage tells the system to add an officer
type AddOfficerMessage struct{}

// AddOfficerMessageType is the type of an AddOfficerMessage
const AddOfficerMessageType string = "AddOfficerMessage"

// Type implements the engo.Message interface
func (AddOfficerMessage) Type() string {
	return AddOfficerMessageType
}

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.

// New listens to messages to update the number of cities and police in the game.
func (m *MoneySystem) New(w *ecs.World) {
	engo.Mailbox.Listen(CityUpdateMessageType, func(msg engo.Message) {
		upd, ok := msg.(CityUpdateMessage)
		if !ok {
			return
		}
		switch upd.New {
		case CityTypeNew:
			m.towns++
		case CityTypeTown:
			m.towns++
			if upd.Old == CityTypeTown {
				m.towns--
			} else if upd.Old == CityTypeCity {
				m.cities--
			} else if upd.Old == CityTypeMetro {
				m.metros--
			}
		case CityTypeCity:
			m.cities++
			if upd.Old == CityTypeTown {
				m.towns--
			} else if upd.Old == CityTypeCity {
				m.cities--
			} else if upd.Old == CityTypeMetro {
				m.metros--
			}
		case CityTypeMetro:
			m.metros++
			if upd.Old == CityTypeTown {
				m.towns--
			} else if upd.Old == CityTypeCity {
				m.cities--
			} else if upd.Old == CityTypeMetro {
				m.metros--
			}
		}
	})

	engo.Mailbox.Listen(AddOfficerMessageType, func(engo.Message) {
		m.officers++
	})
}

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.

// Update keeps track of how much time has passed since the last addtion of money.
// When enough time passes, it adds money based on the number and type of cities
// and subtracts money based on the size of the police force employed.
func (m *MoneySystem) Update(dt float32) {
	m.elapsed += dt
	if m.elapsed > 10 {
		m.amount += m.towns*100 + m.cities*500 + m.metros*1000
		m.amount -= m.officers * 20
		engo.Mailbox.Dispatch(HUDMoneyMessage{
			Amount: m.amount,
		})
		m.elapsed = 0
	}
}

// Remove doesn't do anything since the system has no entities.
func (m *MoneySystem) Remove(b ecs.BasicEntity) {}

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

// HUDMoneyMessage updates the HUD text when changes are made to the amount of
// money available to the player
type HUDMoneyMessage struct {
	Amount int
}

// HUDMoneyMessageType is the type for an HUDMoneyMessage
const HUDMoneyMessageType string = "HUDMoneyMessage"

// Type implements the engo.Message interface
func (HUDMoneyMessage) Type() string {
	return HUDMoneyMessageType
}

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.

engo.Mailbox.Listen(HUDMoneyMessageType, func(m engo.Message) {
  msg, ok := m.(HUDMoneyMessage)
  if !ok {
    return
  }
  h.amount = msg.Amount
  h.updateMoney = true
})

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:

if h.updateMoney {
  txt := h.money.RenderComponent.Drawable.(common.Text)
  txt.Text = fmt.Sprintf("$%v", h.amount)
  h.money.RenderComponent.Drawable = txt
}

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.

engo.Mailbox.Dispatch(CityUpdateMessage{
  New: CityTypeNew,
})

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.