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 {
, BackSprite sprite.Sprite
FrontSpritebool
Rotating }
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.
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 {
.Vector3[float32]
Translation math.Vector3[float32]
Rotation math.Vector2[float32]
Scale math[]sprite.Sprite
Sprites }
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) {
= 0
activeSprite .QueueRender(g.Sprites[activeSprite)
renderer}
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.
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(*sprite.Renderer)
Render}
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 {
.Vector3[float32]
Translation math.Vector3[float32]
Rotation math.Vector2[float32]
Scale math[]sprite.Sprite
Sprites int
activeSprite }
To make Knight
a GameOblect
we must
implement the Update
and Render
functions.
func (k *Knight) Update() {
:= GetKeyboard()
keys if keys["right"] {
.Translation = k.Translation.Add(1,0,0)
k}
//...
= (activeSprite+1) % len(k.Sprites)
activeSprite }
func (k *Knight) Render(*sprite.Renderer) {
.QueueRender(k.Sprites[activeSprite)
renderer}
Knight
now implements GameObject
, no
special implements
directive is required. Troll is
defined similarly:
type Troll struct {
.Vector3[float32]
Translation math.Vector3[float32]
Rotation math.Vector2[float32]
Scale math[]sprite.Sprite
Sprites
AI AISystemint
activeSprite }
Troll also has an Update
and Render
so it satisfies GameObject
but its
Update
implementation is different:
func (t *Troll) Update() {
:= t.AI.GetMovement()
moveVector .Translation.Add(moveVector)
t= (activeSprite+1) % len(k.Sprites)
activeSprite }
func (t *Troll) Render(*sprite.Renderer) {
.QueueRender(t.Sprites[activeSprite)
renderer}
Because both Knight
and Troll
are
Gameobject
s we can store them in the same collection,
in this case an array:
:= []GameObject{}
gameScene = append(gameScene, Knight{})
gameScene = append(gameScene, Troll{})
gameScene = append(gameScene, Troll{})
gameScene = append(gameScene, Troll{})
gameScene
// Game loop
for {
for gameObject := range scene {
.Update()
gameObject.Render(renderer)
gameObject}
}
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.
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 {
.Vector3[float32]
Translation math.Vector3[float32]
Rotation math.Vector2[float32]
Scale math}
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 {
//embedded type
GameObjectCommon []sprite.Sprite
Sprites {}
AI AISystemint
activeSprite }
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[]sprite.Sprite
Sprites {}
AI AISystemint
activeSprite }
But with embedding we can access the fields of
GameObjectCommon
directly which is more
convenient.
:= Troll{}
emb .Translation = math.Vector3[float32]{1,1,1}
emb
:= TrollNoEmbed{}
noEmb .CommontStuff.Translation = math.Vector3[float32]{1,1,1} noEmb
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(*sprite.Renderer)
Render
() math.Vector3[float32]
GetTranslation() math.Vector3[float32]
GetRotation() math.Vector2[float32]
GetScale
(math.Vector3[float32])
SetTranslation(math.Vector3[float32])
SetRotation(math.Vector2[float32])
SetScale}
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 .GetScale() troll
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() {
.Println("Trolling")
fmt}
{}.Update() Troll
The GameObjectCommon method is still accessible if we need it, it’s just not the default. The following would print “Not Implemented”.
{}.GameObjectCommon.Update() Troll
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.