Saving the game

In this tutorial we’re going to get the game saving and loading. We’ll introduce a start menu for choosing between continuing and starting a new game, and we’ll hook into love.quit to save the game when we quit.

Introductions

Head to modules/game/gamestates and create a new file gamestartstate.lua.

local controls = require "controls"

--- @class GameStartState : GameState
--- @field display Display
--- @overload fun(display: Display): GameStartState
local GameStartState = spectrum.GameState:extend("GameStartState")

function GameStartState:__new(display)
   self.display = display
   self.save = love.filesystem.read("save.lz4")
end

We import controls, because we’ll use that a little later. All we do here is keep track of the display, and try to read the savegame from disk. We’re not saving it yet, so it won’t exist and that’s okay!

function GameStartState:draw()
   local midpoint = math.floor(self.display.height / 2)

   self.display:clear()
   self.display:print(1, midpoint, "Kicking Kobolds", nil, nil, nil, "center", self.display.width)

   self.display:print(1, midpoint + 3, "[n] for new game", nil, nil, nil, "center", self.display.width)

   local i = 0
   if self.save then
      i = i + 1
      self.display:print(1, midpoint + 3 + i, "[l] to load game", nil, nil, nil, "center", self.display.width)
   end

   self.display:print(1, midpoint + 4 + i, "[q] to quit", nil, nil, nil, "center", self.display.width)
   self.display:draw()
end

Now we draw a really simple main menu. We put the title of the game and a few options below.

function GameStartState:update(dt)
   controls:update()

   if controls.newgame.pressed then
      love.filesystem.remove("save.lz4")
      local builder = Game:generateNextFloor(prism.actors.Player())
      self.manager:enter(
         spectrum.gamestates.GameLevelState(self.display, builder, Game:getLevelSeed())
      )
   elseif controls.loadgame.pressed and self.save then
      local mp = love.data.decompress("string", "lz4", self.save)
      local save = prism.Object.deserialize(prism.messagepack.unpack(mp))
      Game = save
      self.manager:enter(spectrum.gamestates.GameLevelState(self.display, Game.level))
   elseif controls.quit.pressed then
      love.quit()
   end
end

return GameStartState

Lastly we’ll wire up our options to controls. New game will delete the old save and restart the game anew. Load will restore the save from disk and send us to our previous game. Quit quits the game.

One minor problem! We haven’t actually defined these controls yet, so let’s do that!

Controls

Head over to controls.lua and add the following controls.

newgame        = "n",
loadgame       = "l",

Cleaning up main

Okay we’ve got our start screen and new controls defined. Let’s get this actually showing up! Navigate over to main.lua.

Let’s replace our love.load() function with the following:

function love.load(args)
   if args[1] == "--debug" then
      local builder = prism.LevelBuilder()
      local function generator()
         Game:generateNegamextFloor(prism.actors.Player(), builder)
      end

      manager:push(spectrum.gamestates.MapGeneratorState(generator, builder, display))
   else
      manager:push(spectrum.gamestates.GameStartState(display))
   end

   manager:hook()
   spectrum.Input:hook()
end

This will push our start screen to the stack when the game loads up. Now we need to modify Game a bit to track a bit more state when saving/loading.

Modifying Game

Let’s modify the class definition and constructor of Game.

--- @class Game : Object
--- @field depth integer
--- @field lost boolean
--- @field level Level?
--- @overload fun(seed: string): Game
local Game = prism.Object:extend("Game")

--- @param seed string
function Game:__new(seed)
   self.depth = 0
   self.rng = prism.RNG(seed)
   self.lost = false
end

We’re also going to modify how we expose Game. At the bottom of the file replace the line where we return Game with the following:

_G.Game = Game(tostring(os.time()))

Next, find any instances of require("game") in your code and remove it. Here we’re creating a global variable that contains the gamestate and can be easily replaced during serialization.

Losing and levels

Now, let’s make a few changes to GameLevelState and GameOverState. We need them to track what level we’re on, and if we’ve lost the game.

First let’s head over to modules/game/gamestates/gameoverstate.lua`.

Add the following line to the top of GameOverState:update:

function GameOverState:update(dt)
   Game.lost = true
   ...
end

Now let’s head over to modules/game/gamestates/gamelevelstate.lua.

Add the following line to the top of GameLevelState:updateDecision:

function GameLevelState:updateDecision(dt, owner, decision)
   Game.level = self.level
   ...
end

Saving the game

We’re finally read to wire everything up. It’s time to save the game. Head back over to main.lua.

Add a require "game" to the top of the file.

require "debugger"
require "prism"
require "game"

Now let’s define a love.quit hook that will run when the game is stopped. We’ll check if we lost, and if so we’ll delete any existing save and return. If we haven’t lost we’ll save the current state of the game for the user’s next session.

function love.quit()
   if Game.lost then love.filesystem.remove("save.lz4") return end
   local save = Game:serialize()
   local mp = prism.messagepack.pack(save)
   local lz = love.data.compress("string", "lz4", mp)
   love.filesystem.write("save.lz4", lz)
end

In the next chapter

We’ll use serialization in a different way, creating prefabs and using them to spice up level generation!