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(*sprite.Renderer)
Render
// transform getters/setters
() math.Vector3[float32]
GetTranslation() math.Vector3[float32]
GetRotation() math.Vector2[float32]
GetScale(math.Vector3[float32])
SetTranslation(math.Vector3[float32])
SetRotation(math.Vector2[float32])
SetScale
// hierarchy
() GameObject
GetParent(GameObject)
SetParent() []GameObject
GetChildren(g GameObject)
AddChild}
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 {
() math.Vector3[float32]
GetTranslation() math.Vector3[float32]
GetRotation() math.Vector2[float32]
GetScale(math.Vector3[float32])
SetTranslation(math.Vector3[float32])
SetRotation(math.Vector2[float32])
SetScale}
type GameObject interface {
// game loop methods
()
Update(*sprite.Renderer)
Render
// transform getters/setters
Transformer
// hierarchy
() GameObject
GetParent(GameObject)
SetParent() []GameObject
GetChildren(g GameObject)
AddChild}
Finally, the hierarchy methods allow us to create object/child
relationships between our game objects. We provide a default
implementation for these in GameObjectCommon
.
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(*sprite.Renderer)
Render
// transform getters/setters
Transformer
// hierarchy
() GameObject
GetParent(GameObject)
SetParent() []GameObject
GetChildren(g GameObject)
AddChild
() int
Id}
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 {
+= 1
commonIdSource return commonIdSource
}
type GameObjectCommon struct {
int
id .Vector3[float32]
translation math.Vector3[float32]
rotation math.Vector2[float32]
scale math// ...
}
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 .animation = game.NewAnimation()
knight//...
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) {
:= func(g GameObject) {
fn if !g.Initialized() {
.Init()
g}
.Update(dt)
g}
.ObjectsUpdated = depthFirst(&s.root, fn)
s}
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.
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 {
//...
() []int
GetTags(int)
AddTag}
func (g *GameObjectCommon) AddTag(tag int) {
.tags = append(g.tags, tag)
g}
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.
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() {
.bbox.Destroy()
c.GameObjectCommon.Destroy()
c//... 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 {
//..
() GameObject
GetParent(GameObject)
SetParent() []GameObject
GetChildren(g GameObject)
AddChild(id int) // NEW
RemoveChild() // NEW
Destroy//...
}
func (g *GameObjectCommon) RemoveChild(id int) {
for i := range g.children {
if id == g.children[i].Id() {
.children[i] = g.children[len(g.children)-1]
g.children = g.children[:len(g.children)-1]
gbreak
}
}
}
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() {
(g, func(a GameObject) {
depthFirst.parent.RemoveChild(g.id)
g})
}
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() {
.collisionSystem.Delete(b)
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() {
.bbox.Destroy()
c.GameObjectCommon.Destroy()
c}
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 {
(func())
RunOnDestroy//...
}
func (g *GameObjectCommon) RunOnDestroy(fn func()) {
.onDestroyFuncs = append(g.onDestroyFuncs, fn)
g}
func (g *GameObjectCommon) Destroy() {
(g, func(a GameObject) {
depthFirst.parent.RemoveChild(g.id)
gfor i := range g.onDestroyFuncs {
.onDestroyFuncs[i]()
g}
})
}
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 {
//..
.bbox = Game.Collisions.NewBoundingBox(true, c)
c.RunOnDestroy(c.bbox.Destroy())
c}
The RunOnDestroy
pattern could also be used for
gameplay, like implementing on-death effects. For our coin we
could to this:
.RunOnDestroy(c.bbox.Destroy())
c.RunOnDestroy(func(){
c:= rand.Int() % 5
newCoins for i := 0; i < newCoins; i++ {
:= math.Vector2[float32]{
pos : rand.Float32()*400 + 50,
X: rand.Float32()*400 + 50,
Y}
.Level.AddGameObject(NewCoin(pos))
Game}
})
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
animation game*game.BoundingBox
bbox .GameObjectCommon // makes coin a GameObject by implementing all of GameObject's methods
game}
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) {
.SetParent(g)
gg.children = append(g.children, gg)
g}
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) {
.embededIn = gg
g}
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) {
.SetParent(g.GameObject())
gg.children = append(g.children, gg)
g}
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}
}
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
animation game*game.BoundingBox
bbox .GameObjectCommon
game}
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() {
:= rand.Int() % 3
newSculls for i := 0; i < newSculls; i++ {
:= math.Vector2[float32]{
pos : rand.Float32()*400 + 50,
X: rand.Float32()*400 + 50,
Y}
.Level.AddGameObject(NewScull(pos))
Game}
.Println("Spawning ", newSculls, " sculls")
fmt}
We also modify Coin so that it sometimes spawns sculls as well.
func (c *Coin) SpawnCoins() {
:= rand.Int() % 5
newCoins for i := 0; i < newCoins; i++ {
:= math.Vector2[float32]{
pos : rand.Float32()*400 + 50,
X: rand.Float32()*400 + 50,
Y}
.Level.AddGameObject(NewCoin(pos))
Game}
:= rand.Int() % 2
newSculls for i := 0; i < newSculls; i++ {
:= math.Vector2[float32]{
pos : rand.Float32()*400 + 50,
X: rand.Float32()*400 + 50,
Y}
.Level.AddGameObject(NewScull(pos))
Game}
.Println("Spawned ", newCoins, " coins and ", newSculls, " sculls")
fmt}
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 {
.Println("coin")
fmt} else {
.Println("scull")
fmt}
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 (
= iota
TagPowerup
TagDebuff)
Then in the Coin and Scull constructors we assign a tag to each.
func NewScull(position math.Vector2[float32]) *Scull {
:= &Scull{}
s //...
.AddTag(TagDebuff)
sreturn s
}
Then in our collision check we can perform actions based on the type.
func (k *Knight) Update(dt time.Duration) {
//...
:= k.bbox.CheckForCollisions()
collisions for i := range collisions {
if game.HasTag(collisions[i], TagDebuff) {
.coins--
k} else {
.coins++
k}
.Println("Knight's coins: ", k.coins)
fmt[i].Destroy()
collisions}
if k.coins < 0 {
.Println("Game Over")
fmt.Exit(0)
os}
}
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.