Knight vs Trolls Part 3: Levels
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.
Level System
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 {
level int
tiles int
trolls int
trollsAlive int
player *Knight
Scene *game.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() {
scene := game.NewScene()
if l.player == nil {
l.player = NewKnight(math.Vector2[float32]{500, 500})
}
scene.AddGameObject(l.player)
tilesize := float32(50)
start := 150 + float32(15-l.tiles)*(tilesize/2)
arenaSize := l.tiles * int(tilesize)
scene.AddGameObject(NewArena(
math.Vector2[float32]{start, start},
l.tiles, l.tiles,
math.Vector2[float32]{tilesize, tilesize}),
)
scoreText := NewFloatText(fmt.Sprint("Level ", l.level),
math.Vector2[int]{300, 50}, math.Vector3[float32]{20, 950, 0})
scene.AddGameObject(scoreText)
// spawn trolls randomly
for i := 0; i < l.trolls; i++ {
posX := rand.Float32()*float32(arenaSize) + start
posY := rand.Float32()*float32(arenaSize) + start
troll := NewTroll(math.Vector2[float32]{posX, posY})
scene.AddGameObject(troll)
}
l.trollsAlive = l.trolls
// unload previous scene
if l.Scene != nil {
l.Scene.Destroy()
}
l.Scene = scene
}
This method does three things:
- On the first level, it spawns our player Knight and keeps a local reference so the player can persist between levels.
- It creates an
Arena
in the same way we saw in tutorial 22. Since the arena can shrink, it’s created using thetiles
variable stored in the level. - Finally, it spawns the number of trolls stored in the level. The trolls are spawned at random locations on the arena.
Changing 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() {
l.level = l.level + 1
if l.level%2 != 0 {
// odd levels decrease the arena
l.tiles = l.tiles - 1
l.makeAScene()
} else {
// even levels increase the number of trolls
l.trolls = l.trolls + 1
l.makeAScene()
}
}
In both cases we recreate the scene using
makeAScene
.
Triggering a Level Change
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() {
l.trollsAlive -= 1
fmt.Println("trolls alive:", l.trollsAlive)
if l.trollsAlive == 0 {
l.Next()
}
}
func (t *Troll) Update(dt time.Duration) {
//...
if OnGround(t.hurtbox) {
t.Walker.Update(dt, t)
} else {
Fall(t, dt)
if t.GetScale().Length() < 5 {
Game.Level.TrollKilled()
t.Destroy()
}
}
//...
}
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.
Emptying the Scene
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() {
v.Destroy()
}
}
Running the Level
To run our levels we add it to the global game struct (this was a
Scene
in previous tutorials).
var Game struct {
Atlas *sprite.Atlas
Shader shaders.Shader
Input *platform.InputState
Collisions game.CollisionManagerAABB
Level *Level
}
In our main loop we update and render the Level’s scene attribute:
func main() {
//...
Game.Level = NewLevel()
//...
for {
// game loop
//...
Game.Level.Scene.Update(dt)
Game.Level.Scene.Render(renderer)
renderer.Render()
}
}
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.
Further Work
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.
- Add an end game screen. This can be a
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. - Add sound and visual effects for when the player hits trolls and when the trolls push the player.
- Add a stats to the player and the trolls. An easy one is to increase the pushback on the player’s swing when they defeat trolls. You can go crazy here and add hit points, armor or build an entire RPG system.
- Add obstacles, such as walls, to the Arena. Unmovable blocks with a collider can act as walls. Maybe you can add traps that cause the player to take damage.
- Add drops, such as coins and sculls. If you added hit points to the player then maybe defeating trolls causes hearts to drop that replenish the Knight’s HP.
- Add a bow and shoot arrows. Make an archer protagonist.
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.↩︎