Game Object

In the previous tutorial we created a demo application that showed an arrangement of playing cards that automatically rotated and showed a different card on every rotation. In that example, to better organize our code, we defined a Card object.

type Card struct {
    FrontSprite, BackSprite sprite.Sprite
    Rotating                bool
}

Card stores the front and back sprites for that card and some state (Rotating) that tracks whether the card is spinning or not. The card also had it’s own constructor function,NewCard, and an Update function that is responsible for spinning the card and changing it’s face when the card completed a full rotation.

This type of object comes up very often in games and most game engines have support for it. Game objects are used to represent various things found in a game: players, enemies, weapons, obstacles and so on. In this tutorial we will define a our own game object type which we will call… GameObject.

Attributes

A game object should hold data that are common to most objects in the game. Lets take the example of a hypothetical, simple, hack-and-slash game called “Knight vs Trolls”. In this game, our playable character, a knight, wields a sword and uses it to slay foul trolls. When the trolls die, they drop gold coins that our hero must collect.

The knight, trolls and coins are game objects. The player’s sword can also be a game object if we want our player to be able to change weapons. Each game object is positioned somewhere in the scene and has a size and rotation (in this example sprites face left or right but to be general we will allow all rotations). Also, each game object has one or more sprites. The player and trolls have a few sprites that create a walk animation and the sword and coin have a single sprite. From the above we can devise a GameObject type for our game with the following attributes.

type GameObject struct {
    Translation math.Vector3[float32]
    Rotation    math.Vector3[float32]
    Scale       math.Vector2[float32]
    Sprites     []sprite.Sprite
}

All game objects in our example must be rendered so it makes sense to add a render method to GameObject.

func (g *GameObject) Render(renderer *sprite.Renderer) {
    activeSprite = 0
    renderer.QueueRender(g.Sprites[activeSprite)
}

In the finished code we must figure out a way to switch activeSprite so we can have animation but we will leave that for later.

GameObject Interface

In “Knight vs Trolls” our knight is controlled using the keyboard and the trolls are controlled by a very simple AI function. This control functionality is exclusive to each object. The trolls don’t need keyboard controls and the knight doesn’t need AI. This means that this functionality should not be part of GameObject and must be added to the respective specialized game objects of the knight and troll.

In object-oriented languages this can be achieved with inheritance. In Java, for example, we can define a Knight class that inherits GameObject and includes a keyboard control function. A Troll class would also inherit GameObject and specialize by providing an AI function. In Go, we achieve the same result using interfaces. An interface is a collection of functions that types must implement. Lets see an example.

type GameObject interface {
    Update()
    Render(*sprite.Renderer)
}

In the above, we specify that all game objects must have an Update() function and a Render(*sprite.Renderer) function. We can create types that satisfy the interface by creating methods for the type that match these signatures. For example, our knight type could be:

type Knight struct {
    Translation math.Vector3[float32]
    Rotation    math.Vector3[float32]
    Scale       math.Vector2[float32]
    Sprites     []sprite.Sprite
    activeSprite int
}

To make Knight a GameOblect we must implement the Update and Render functions.

func (k *Knight) Update() {
    keys := GetKeyboard()
    if keys["right"] {
        k.Translation = k.Translation.Add(1,0,0)
    }
    //...
    activeSprite = (activeSprite+1) % len(k.Sprites)
}

func (k *Knight) Render(*sprite.Renderer) {
    renderer.QueueRender(k.Sprites[activeSprite)
}

Knight now implements GameObject, no special implements directive is required. Troll is defined similarly:

type Troll struct {
    Translation math.Vector3[float32]
    Rotation    math.Vector3[float32]
    Scale       math.Vector2[float32]
    Sprites     []sprite.Sprite
    AI          AISystem
    activeSprite int
}

Troll also has an Update and Render so it satisfies GameObject but its Update implementation is different:

func (t *Troll) Update() {
    moveVector := t.AI.GetMovement()
    t.Translation.Add(moveVector)
    activeSprite = (activeSprite+1) % len(k.Sprites)
}

func (t *Troll) Render(*sprite.Renderer) {
    renderer.QueueRender(t.Sprites[activeSprite)
}

Because both Knight and Troll are Gameobjects we can store them in the same collection, in this case an array:


gameScene := []GameObject{}
gameScene = append(gameScene, Knight{})
gameScene = append(gameScene, Troll{})
gameScene = append(gameScene, Troll{})
gameScene = append(gameScene, Troll{})

// Game loop
for {
    for gameObject := range scene {
        gameObject.Update()
        gameObject.Render(renderer)
    } 
}

Interfaces allow us to create collections of similar objects and process them together. In fancy computer science terms this is called polymorphism. In the above code, the knight and the trolls are stored in the same gameScene array. In the game loop, we iterate over the knight and troll game objects and call their respective Update and Render functions. This mechanism will serve as the basis of our scene processing code later.

Common Attributes

In our example, the knight and trolls have transform attributes Translation, Rotation and Scale. Most game objects in a game will have these attributes so we can define them in their own type to make it easier to include in game objects.

type GameObjectCommon struct {
    Translation math.Vector3[float32]
    Rotation    math.Vector3[float32]
    Scale       math.Vector2[float32]
}

In Go, we can compose structs by adding an unnamed field with the struct type into another struct. This is called struct embedding.

type Troll struct {
    GameObjectCommon //embedded type
    Sprites     []sprite.Sprite
    AI          AISystem{}
    activeSprite int
}

The fields of the GameObjectCommon struct are now present in Troll. This creates the same Troll type that we had before. Embedding a type is functionally equivalent to just adding a field of that type:

type TrollNoEmbed struct {
    CommontStuff GameObjectCommon
    Sprites      []sprite.Sprite
    AI           AISystem{}
    activeSprite int
}

But with embedding we can access the fields of GameObjectCommon directly which is more convenient.

emb := Troll{}
emb.Translation = math.Vector3[float32]{1,1,1}

noEmb := TrollNoEmbed{}
noEmb.CommontStuff.Translation = math.Vector3[float32]{1,1,1}

Embedding significantly reduces code repetition as the number of game object fields increases. We don’t have a way to force game objects to include GameObjectCommon but we can make GameObject require the existence of these attributes by adding getter and setter methods for them.

type GameObject interface {
    Update()
    Render(*sprite.Renderer)
    
    GetTranslation() math.Vector3[float32]
    GetRotation() math.Vector3[float32]
    GetScale() math.Vector2[float32]
    
    SetTranslation(math.Vector3[float32])
    SetRotation(math.Vector3[float32])
    SetScale(math.Vector2[float32])
}

This creates a problem. Every time we implement a new game object we must also implement these attribute methods. This would be very cumbersome. One way to solve this is to automatically create code templates for new game objects using code generation. Go has built-in support for code generation using the go generate command1.

Another approach, and the one we will be using here, is to add the implementation of these methods into GameObjectCommon.

func (g *GameObjectCommon) GetTranslation() math.Vector3[float32] { return g.Translation }
func (g *GameObjectCommon) GetRotation() math.Vector3[float32]    { return g.Rotation }
func (g *GameObjectCommon) GetScale() math.Vector2[float32]       { return g.Scale }

func (g *GameObjectCommon) SetTranslation(t math.Vector3[float32]) { g.Translation = t }
func (g *GameObjectCommon) SetRotation(r math.Vector3[float32])    { g.Rotation = r }
func (g *GameObjectCommon) SetScale(s math.Vector2[float32])       { g.Scale = s }

With this, objects that embed GameObjectCommon automatically implement the attribute getter/setter methods. The following is valid, for example:

type Troll struct {
    GameObjectCommon
}

troll := Troll{}
troll.GetScale()

We can take this a step further and have GameObjectCommon implement all methods of GameObject.

func (g *GameObjectCommon) Render(r *sprite.Renderer) {}
func (g *GameObjectCommon) Update() {fmt.Println("Not Implemented")}

This way, any object that embeds GameObjectCommon is automatically a GameObject. Objects can overwrite the implementation of any of the GameObjectCommon methods by providing their own. For example, the following would print “Trolling”.

type Troll struct {
    GameObjectCommon
}

func (t *Troll) Update() {
    fmt.Println("Trolling")
}

Troll{}.Update()

The GameObjectCommon method is still accessible if we need it, it’s just not the default. The following would print “Not Implemented”.

Troll{}.GameObjectCommon.Update()

Remarks

The GameObject interface will be our main tool for building and organize our games. In the following tutorials we will expand GameObject to include common functionality found in games. A few tutorials down the line we will be able to implement the “Knight vs Trolls” for real.