In this tutorial we will implement our main gameplay loop. When our knight defeats the trolls in the level, more trolls will spawn and the game arena will become smaller making the game harder. The goal of our brave hero is to survive for as long as possible.
So far our game consisted of one level that we set up when the
game boots. Here we will introduce a level system to switch
between levels. We can do this in many ways. One way would be to
create game.Scene
instances and store them, either in
the game source (hacky but sufficient in a game prototype) or in
levels files (for example JSON dumps of
game.Scene
).
In this tutorial we will create a system that builds each level
programmatically, similarly to what procedurally generated games
do. Our levels will be created by the Level
type
defined as follows.
type Level struct {
int
level int
tiles int
trolls int
trollsAlive *Knight
player *game.Scene
Scene }
Level
keeps track of the current level (1,2,3
etc), the number of trolls in the level and the player. We convert
this information into a game.Scene
that runs in the
engine. This is done in the makeAScene
method.
func (l *Level) makeAScene() {
:= game.NewScene()
scene if l.player == nil {
.player = NewKnight(math.Vector2[float32]{500, 500})
l}
.AddGameObject(l.player)
scene
:= float32(50)
tilesize := 150 + float32(15-l.tiles)*(tilesize/2)
start := l.tiles * int(tilesize)
arenaSize
.AddGameObject(NewArena(
scene.Vector2[float32]{start, start},
math.tiles, l.tiles,
l.Vector2[float32]{tilesize, tilesize}),
math)
:= NewFloatText(fmt.Sprint("Level ", l.level),
scoreText .Vector2[int]{300, 50}, math.Vector3[float32]{20, 950, 0})
math.AddGameObject(scoreText)
scene
// spawn trolls randomly
for i := 0; i < l.trolls; i++ {
:= rand.Float32()*float32(arenaSize) + start
posX := rand.Float32()*float32(arenaSize) + start
posY
:= NewTroll(math.Vector2[float32]{posX, posY})
troll .AddGameObject(troll)
scene}
.trollsAlive = l.trolls
l// unload previous scene
if l.Scene != nil {
.Scene.Destroy()
l}
.Scene = scene
l}
This method does three things:
Arena
in the same way we saw in tutorial 22. Since the arena can
shrink, it’s created using the tiles
variable stored
in the level.The rule for changing the level is simple. On odd levels we decrease the arena size. On even levels we increase the number of trolls.
func (l *Level) Next() {
.level = l.level + 1
l
if l.level%2 != 0 {
// odd levels decrease the arena
.tiles = l.tiles - 1
l.makeAScene()
l
} else {
// even levels increase the number of trolls
.trolls = l.trolls + 1
l.makeAScene()
l}
}
In both cases we recreate the scene using
makeAScene
.
A level change is triggered when all trolls in the scene die.
We use the trollsAlive
level attribute to keep track
of this. When a troll dies we inform the level with the
TrollKilled
method:
func (l *Level) TrollKilled() {
.trollsAlive -= 1
l.Println("trolls alive:", l.trollsAlive)
fmtif l.trollsAlive == 0 {
.Next()
l}
}
func (t *Troll) Update(dt time.Duration) {
//...
if OnGround(t.hurtbox) {
.Walker.Update(dt, t)
t} else {
(t, dt)
Fallif t.GetScale().Length() < 5 {
.Level.TrollKilled()
Game.Destroy()
t}
}
//...
}
Once trollsAlive
reaches zero, we trigger the
transition to the next level.
This approach is fast and works well for our small demo game
but for larger games it could become confusing to keep track of
various level change conditions and where they are triggered. A
more scalable approach would be to search the scene on each update
and count the number of trolls. This wouldn’t require the
implementation of Troll
to worry about updating the
level itself1. A possible downside is that
searching the scene adds a small performance hit.
When we swap one scene for another we want to make sure all
game objects in the scene are destroyed. To ensure this, we call
l.Scene.Destroy()
at the end of
makeAScene
. This recursively calls
Destroy
on all game objects in the scene.
func (s *Scene) Destroy() {
for _, v := range s.root.GetChildren() {
.Destroy()
v}
}
To run our levels we add it to the global game struct (this was
a Scene
in previous tutorials).
var Game struct {
*sprite.Atlas
Atlas .Shader
Shader shaders*platform.InputState
Input .CollisionManagerAABB
Collisions game*Level
Level }
In our main loop we update and render the Level’s scene attribute:
func main() {
//...
.Level = NewLevel()
Game//...
for {
// game loop
//...
.Level.Scene.Update(dt)
Game.Level.Scene.Render(renderer)
Game.Render()
renderer}
}
Since Level
manages the scene internally, there is
no need to code anything here. When the scene is swapped, the game
loop will automatically update and render the new one.
This concludes the Knights vs Trolls tutorial. Our goal was to showcase core building blocks of making a game such as rendering and controlling characters, collisions, gameplay mechanics and levels. We created a basis from which we can build incrementally more complex games. The following are some suggestions for further work.
Scene
with
just a text label saying ‘Game Over’ and the current score. Or,
you can go fancy and add animations with the character dying, or
even roll credits.Most of these suggestions shouldn’t be too hard to implement and building multiple would transform this little demo game into an actual game!
A fancy way to put this is to say it reduces coupling.↩︎