More on Game Objects

In the game object tutorial we provided the base interface for the game objects in our games. We then extended it in the Hierarchy tutorial to enable parent-child relationships in our games and saw how this enables common game behavior like the player holding something. In this tutorial we will add the final extensions needed to complete the game object interface.

Currently our GameObject looks like this:

type GameObject interface {
    // game loop methods
    Update()
    Render(*sprite.Renderer)
    
    // transform getters/setters
    GetTranslation() math.Vector3[float32]
    GetRotation() math.Vector3[float32]
    GetScale() math.Vector2[float32]
    SetTranslation(math.Vector3[float32])
    SetRotation(math.Vector3[float32])
    SetScale(math.Vector2[float32])
    
    // hierarchy
    GetParent() GameObject
    SetParent(GameObject)
    GetChildren() []GameObject
    AddChild(g GameObject)
}

The game loop methods are called on every loop iteration to update the game logic and render the game object. Update holds the game logic and it is where our users will code most of their game. Render is much simpler and it typically just calls the Animation.Render function to draw a sprite.

The transform setters and getters do what they say on the tin. To tidy up a bit, we can add these in their own interface and then embed it into game object. The following is identical to game object above.

type Transformer interface {
    GetTranslation() math.Vector3[float32]
    GetRotation() math.Vector3[float32]
    GetScale() math.Vector2[float32]
    SetTranslation(math.Vector3[float32])
    SetRotation(math.Vector3[float32])
    SetScale(math.Vector2[float32])
}

type GameObject interface {
    // game loop methods
    Update()
    Render(*sprite.Renderer)
    
    // transform getters/setters
    Transformer
    
    // hierarchy
    GetParent() GameObject
    SetParent(GameObject)
    GetChildren() []GameObject
    AddChild(g GameObject)
}

Finally, the hierarchy methods allow us to create object/child relationships between our game objects. We provide a default implementation for these in GameObjectCommon.

Game Object Ids

Missing from our game object is a way to identify game objects. For example, in our collision tutorial we had our player check for collisions and upon finding a collision they would ‘pick up’ the coins on the ground. That worked because there where only coins in the game. If there where also enemies in the game we would need a way to distinguish between the two. To this end, we will add two ways to identify game objects.

The first is the game object’s id. This is an integer that uniquely identifies a game object. On the game object interface we add the Id() method that returns it.

type GameObject interface {
    // game loop methods
    Update()
    Render(*sprite.Renderer)
    
    // transform getters/setters
    Transformer
    
    // hierarchy
    GetParent() GameObject
    SetParent(GameObject)
    GetChildren() []GameObject
    AddChild(g GameObject)
    
    Id() int
}

Setting an id is just a matter of giving each game object a unique number. The number itself doesn’t matter. In GameObjectCommon we create these ids by using a counter (another solution could be to use large random numbers like UUIDs).

var commonIdSource int = 0 //0 is uninitialized ids
func nextId() int {
    commonIdSource += 1
    return commonIdSource
}

type GameObjectCommon struct {
    id int
    translation math.Vector3[float32]
    rotation    math.Vector3[float32]
    scale       math.Vector2[float32]
    // ...
}

func (g *GameObjectCommon) Id() int 
{ 
    return g.id 
}

We then have the issue of how to assign the id to the game object. So far, we have used constructor methods to create game objects. For example, our knight is created with:

func NewKnight(position math.Vector2[float32]) *Knight {
    knight := &Knight{}
    knight.animation = game.NewAnimation()
    //...
    return &knight
}

However, this is a user function and we don’t want to transfer the responsibility of assigning ids to our user. Instead, we will introduce an Init function to our game object and give it a default implementation in GameObjectCommon.

```go
type GameObject interface {
    Init()             //NEW
    Initialized() bool //NEW
    
    // game loop methods
    Update()
    Render(*sprite.Renderer)
    
    // transform getters/setters
    Transformer
    
    // hierarchy
    GetParent() GameObject
    SetParent(GameObject)
    GetChildren() []GameObject
    AddChild(g GameObject)
    
    Id() int
}

func (g *GameObjectCommon) Init() {
    g.id = nextId()
}

We also modify our scene Update to initialize any uninitialized objects. This also allows for objects to be dynamically spawned into the game without having the user worry about calling Init.

// Update all gameobjects
func (s *Scene) Update(dt time.Duration) {
    fn := func(g GameObject) {
        if !g.Initialized() {
            g.Init()
        }
        g.Update(dt)
    }
    s.ObjectsUpdated = depthFirst(&s.root, fn)
}

The user if free to overwrite the implementation of Init in their own code in which case they become responsible for setting up the id value.

Tags

Very often, we want the ability to categorize our game objects. For example we could have ‘player’, ‘enemy’ and ‘obstacle’ categories. These allow us to define behavior for whole groups of objects. In our knight vs trolls game we want the player to be able to damage enemies with their sword but not obstacles. We achieve this using tags. Tags are integer enumerations that we keep in an array on the game object.

type GameObject interface {
    //... 
    GetTags() []int
    AddTag(int)
}

func (g *GameObjectCommon) AddTag(tag int) {
    g.tags = append(g.tags, tag)
}

func (g *GameObjectCommon) GetTags() []int {
    return g.tags
}
    

This is a simple but powerful feature and we will see an example of it in use later in this tutorial.

Destroying Game Objects

In the collisions tutorial we used a Destroy method to make our coins disappear and have others spawn when that happens.

func (c *Coin) Destroy() {
    c.bbox.Destroy()
    c.GameObjectCommon.Destroy()
    //... spawn new coins
}

Let’s define what Destroy is. When we destroy a game object we want it gone from the game. In languages where memory is directly managed (like C), we would delete the game object from memory. In Go it’s sufficient to remove it from our object hierarchy. To do that, we introduce the RemoveChild method. It’s default implementation is to go over the children array and remove the first child with a matching id (assumes ids are unique).

type GameObject interface {
    //..
    GetParent() GameObject
    SetParent(GameObject)
    GetChildren() []GameObject
    AddChild(g GameObject)
    RemoveChild(id int) // NEW
    Destroy()           // NEW
    //...   
}

func (g *GameObjectCommon) RemoveChild(id int) {
    for i := range g.children {
        if id == g.children[i].Id() {
            g.children[i] = g.children[len(g.children)-1]
            g.children = g.children[:len(g.children)-1]
            break
        }
    }
}

The Destroy method simply unlinks the object from its parent. Go’s garbage collection takes care of the actual memory de-allocation. Our object might have children and those must be destroyed too so we set our destroy to run recursively.

func (g *GameObjectCommon) Destroy() {
    depthFirst(g, func(a GameObject) {
        g.parent.RemoveChild(g.id)
    })
}       

One possible issue is when a game object contains other objects that must be manually de-allocated. Normally this should never happen, and if it does it’s a good sign that that we designed something poorly. Is some cases though it can happen and we already saw an example of this with our collision objects. Our BoundingBox is created in a global manager object called CollisionManagerAABB. When a bounding box is no longer needed it must be destroyed by calling it’s destroy method which removes it from the manager:

func (b *BoundingBox) Destroy() {
    b.collisionSystem.Delete(b)
}

This was a design decision aimed at performance. We could have omitted the manager part, and had collisions scan the whole object hierarchy. This would have been a cleaner solution and we wouldn’t need a destroy method, but it would be slower.

With the current implementation, if a game object has a bounding box, it must overwrite the Destroy method of GameObjectCommon and have it call bounding box’s destroy as we did with the coin.

func (c *Coin) Destroy() {
    c.bbox.Destroy()
    c.GameObjectCommon.Destroy()
}

To avoid this, we introduce the RunOnDestroy method. It accepts a function that is run automatically when an object is destroyed. User’s can call RunOnDestroy multiple times to pass different functions that get run when the object is destroyed.

type GameObject interface {
    RunOnDestroy(func())
    //...
}

func (g *GameObjectCommon) RunOnDestroy(fn func()) {
    g.onDestroyFuncs = append(g.onDestroyFuncs, fn)
}

func (g *GameObjectCommon) Destroy() {
    depthFirst(g, func(a GameObject) {
        g.parent.RemoveChild(g.id)
        for i := range g.onDestroyFuncs {
            g.onDestroyFuncs[i]()
        }
    })
}

With this pattern, we don’t need to overwrite the default destroy method. Instead we pass any de-allocation or cleanup functions when we create our game object. For example, our coin would be rewritten like this:

func NewCoin(position math.Vector2[float32]) *Coin {
    //.. 
    c.bbox = Game.Collisions.NewBoundingBox(true, c)
    c.RunOnDestroy(c.bbox.Destroy())
}

The RunOnDestroy pattern could also be used for gameplay, like implementing on-death effects. For our coin we could to this:

c.RunOnDestroy(c.bbox.Destroy())
c.RunOnDestroy(func(){
    newCoins := rand.Int() % 5
    for i := 0; i < newCoins; i++ {
        pos := math.Vector2[float32]{
            X: rand.Float32()*400 + 50,
            Y: rand.Float32()*400 + 50,
        }
        Game.Level.AddGameObject(NewCoin(pos))
    }
})

Embedding

Our GameObject relies on GameObjectCommon to provide the implementation for common functions. To use GameObjectCommon we simply embed it in our game object. For example, to turn our Coin object into a GameObject we use:

type Coin struct {
    animation game.Animation
    bbox      *game.BoundingBox
    game.GameObjectCommon // makes coin a GameObject by implementing all of GameObject's methods
}

This introduces a subtle issue. Consider what happens when we add a child object to our coin. We do that using the AddChild method which we get from GameObjectCommon.

func (g *GameObjectCommon) AddChild(gg GameObject) {
    gg.SetParent(g)
    g.children = append(g.children, gg)
}

The issue here is that GameObjectCommon adds itself as a parent to the child object and not the coin. This is a problem if the child needs to call a function that its parent has overwritten, such as the coin’s custom Destroy that we saw earlier. If the child does GetParent().Destroy() this would call the GameObjectCommon.Destroy() method and not the one defined in Coin.

We solve this issue with the SetEmbed method. SetEmbed stores a reference to the object that GameObjectCommon is embedded in and that object is then used in place of GameObjectCommon when GameObjectCommon needs to return a ref of itself.

func (g *GameObjectCommon) SetEmbed(gg GameObject) {
    g.embededIn = gg
}

To get the embedded game object we use the GameObject method. It returns the embedded object if one was set or GameObjectCommon if it wasn’t set.

func (g *GameObjectCommon) GameObject() GameObject {
    if g.embededIn != nil {
        return g.embededIn
    }
    return g
}

Then our AddChild method uses GameObject which will correctly add the embedded object as the parent:

func (g *GameObjectCommon) AddChild(gg GameObject) {
    gg.SetParent(g.GameObject())
    g.children = append(g.children, gg)
}

An alternative to using SetEmbed is the GameObjectCommon constructor which sets the embed when you call it.

func NewGameObjectCommon(embededIn GameObject) GameObjectCommon {
    return GameObjectCommon{embededIn: embededIn}
}

Knights vs Trolls

This time our addition to Knight vs Trolls will be small. When our knight pick’s up a coin the coin will have a chance of spawning coins and/or sculls. Picking up sculls removes our coins and if we touch a scull without having any coins we die. Let’s first create the scull object.

type Scull struct {
    animation game.Animation
    bbox      *game.BoundingBox
    game.GameObjectCommon
}

Scull’s constructor is pretty much identical to coin so we omit it here. The only big difference is that sculls only spawn sculls when the are picked up. We set the following to run on a scull’s death using RunOnDestroy.

func (s *Scull) SpawnSculls() {
    newSculls := rand.Int() % 3
    for i := 0; i < newSculls; i++ {
        pos := math.Vector2[float32]{
            X: rand.Float32()*400 + 50,
            Y: rand.Float32()*400 + 50,
        }
        Game.Level.AddGameObject(NewScull(pos))
    }
    fmt.Println("Spawning ", newSculls, " sculls")
}

We also modify Coin so that it sometimes spawns sculls as well.

func (c *Coin) SpawnCoins() {
    newCoins := rand.Int() % 5
    for i := 0; i < newCoins; i++ {
        pos := math.Vector2[float32]{
            X: rand.Float32()*400 + 50,
            Y: rand.Float32()*400 + 50,
        }
        Game.Level.AddGameObject(NewCoin(pos))
    }

    newSculls := rand.Int() % 2
    for i := 0; i < newSculls; i++ {
        pos := math.Vector2[float32]{
            X: rand.Float32()*400 + 50,
            Y: rand.Float32()*400 + 50,
        }
        Game.Level.AddGameObject(NewScull(pos))
    }

    fmt.Println("Spawned ", newCoins, " coins  and ", newSculls, " sculls")
}

We now need a way for our knight to distinguish between picking a scull or a coin. We can do this in two ways. One is to take our collision object and use type assertion to check if its a *Coin.

if _, ok := collisions[i].(*Coin); ok {
    fmt.Println("coin")
} else {
    fmt.Println("scull")
}

The other option, and the one we will use, is to assign tags to coins and sculls. Tags are integers which we can enumerate:

const (
    TagPowerup = iota
    TagDebuff
)

Then in the Coin and Scull constructors we assign a tag to each.

func NewScull(position math.Vector2[float32]) *Scull {
    s := &Scull{}
    //...
    s.AddTag(TagDebuff)
    return s
}

Then in our collision check we can perform actions based on the type.

func (k *Knight) Update(dt time.Duration) {
    //...
    
    collisions := k.bbox.CheckForCollisions()
    for i := range collisions {
        if game.HasTag(collisions[i], TagDebuff) {
            k.coins--
        } else {
            k.coins++
        }
        fmt.Println("Knight's coins: ", k.coins)
        collisions[i].Destroy()
    }

    if k.coins < 0 {
        fmt.Println("Game Over")
        os.Exit(0)
    }
}

Tags are a very simple and very useful mechanic. One other potential use is to optimize collision detection. Instead of placing all of our bounding boxes in one list we could split them up into bins based on their tag. Then, we can make collision checks that check only against specific types of objects.

The code for this version of Knights vs Trolls can be found here.