Managing Game State – SceneManager

We have a bunch of scene definitions now, but how do we keep track of them and transition between them?  A solution is the SceneManager class, which maintains a stack of Scenes and handles adding/removing/updating them.  Here’s what the stack might look like, with each layer being a Scene:

sm_exampleAt each step, only the top Scene on the current stack will be updated and drawn.  As Scenes are added to the stack, they are initialized, and as they are taken off, they will be cleaned up.  The interface for the SceneManager will be as follows:

  • pushScene(scene) – add a new scene to the top of the stack
  • popScene() – remove the top scene
  • update() – should be called every loop iteration, calls update() on the top Scene

Here’s the code:

SceneManager = {}

--stack of current scenes
local scenes = {}

local showDebugLayer = true

First we declare the SceneManager class. Since there can only ever be one scene manager, I’m making this static and just creating it as a global table. There is also a “scenes” table to store the actual stack. This is declared local and it should not be accessible outside of this file, so it is essentially a private member variable. Finally we have a flag to determine whether we show the debug layer that each Scene has. This makes it a little bit easier to toggle all debug information on or off.

Next we have a helper function to set the current scene to be the one that is drawn.

local function setScene(scene)
  local layers = {}
  if scene then
    layers = scene:getLayers()

    if showDebugLayer == true then
      table.insert(layers, scene.debugLayer)
    end
  end

  if layers then
    MOAIRenderMgr.setRenderTable(layers)
  else
    MOAIRenderMgr.setRenderTable({})
  end
end

This checks the current scene, grabs its layers, and passes it to the MOAIRenderMgr. It also checks the debug flag – if it is set to true, it will also add the scene’s debugLayer to the list.

Finally we have the interface that you can call:

function SceneManager.pushScene(scene)
  table.insert(scenes, scene)
  scenes[#scenes]:initialize()
  setScene(scenes[#scenes])
end

function SceneManager.popScene()
  scenes[#scenes]:cleanup()
  table.remove(scenes, #scenes)
  if #scenes > 0 then
    setScene(scenes[#scenes])
  else
    setScene(nil)
  end
end

function SceneManager.showDebugLayer(show)
  showDebugLayer = show
  setScene(scenes[#scenes])
end

--calls update on the visible scene
function SceneManager.update()
  if #scenes > 0 then
    scenes[#scenes]:update()
  end
end

Remember #scenes is the number of items in a table.  Since Lua tables are 1-indexed, scene[#scenes] will be the last (top) Scene.  Every time we add a Scene to the top, we initialize it, and when we take it off the top, we clean it up. showDebugLayer() will take either true or false, set the flag, and then update the drawn layers with the new information.  You need to make sure you call update() every simulation loop so it knows to pass it on to the active Scene.

This is an example of how the SceneManager can be used in your main.lua file. This is assuming all the Moai setup has already been done. We initialize it with a Scene instance, and then call update() every loop. When we detect that after an update() is finished and there are no Scenes left, we exit out and assume the game is over.

SceneManager.pushScene(PhysicsTestScene.new())

local gameOver = false

local mainLoop = MOAICoroutine.new ()

mainLoop:run (
  function ()
    while not gameOver do
      SceneManager.update()

      if SceneManager.sceneCount() == 0 then
        gameOver = true
      end

      coroutine.yield ()
    end
    print("Game Over!")
    os.exit()
  end
)

There is still some room for improvement here:

  • If there is a Scene that is on top of another scene but doesn’t take up the entire screen, then we need to still render the one below it.  To address this we can add a transparency property to Scenes that should be see-through, and then make sure we grab the layers from the next Scene down when we set it up.  An example would be a pause menu that only takes up a small portion of the screen – you’d still want to see the action going on underneath.  You wouldn’t want to call update() though since the pause menu should be the only one responsive.
  • If there is pause functionality, we need to add pause() and unpause() functions to Scene, which would stop/start all MOAIAction related items (Box2D simulations, timers, animations).  SceneManager would have to call these when necessary.
  • If the start scene takes up a lot of memory or resources, we may not want to hold it in memory while the gameplay is happening.  Instead of just adding on to the stack, we can pop off the start menu scene before adding on the gameplay one.  We would have to remember to add it back on once the user wants to go to the main menu.  We would also lose any state the main menu has, since the user is going to a brand new instance of it.

Leave a Reply