Here's an idea for reducing the number of if-statements used to transition between game states. It's not a new idea. In fact, it's an old design pattern from the GoF Design Patterns book.
Toggalight, my second game, is small in scope, but has the usual need of presenting different UI elements, and responding to user interaction differently depending on whether the game is running, paused, or over.
When I started on this project, I decided I wasn't going to stick myself into an if-statement quagmire again. So I broke out the trusty State pattern and went to work.
Overview
The game has four buttons that cause state changes: Start, Pause, Resume, Start Over. The GameScene class implements a library of functionality that is available to both the view controller and the various state classes.Note: I'm using a python-like pseudo-code, and will be abstracting most of the details so we can focus on the state structure and transitions. The game is actually written for iOS in Objective-C, using SpriteKit.
BaseGameScene
This is the base class for all game states, and defines the interface expected by the GameScene.abstract class SceneState
property gameScene
// state management - empty base class methods that don't do anything
method enterState()
method exitState()
// Events - empty base class methods that don't do anything
method onFrameUpdate()
method onLightTouched()
method onStartButton()
method onPauseButton()
method onResumeButton()
method onStartOverButton()
RunningState
I'll spare you the details of all the states, but here's the gist of what's in the most complex state, the one that's active while the game is being played.class RunningState : SceneState
method enterState()
// set up UI for state
gameScene.pauseButton.show()
gameScene.startButton.hide()
gameScene.startOverButton.hide()
gameScene.resumeButton.hide()
gameScene.helpButton.hide()
gameScene.calibrateButton.hide()
gameScene.highScoreLabel.hide()
// Physics are different between running and game over states
gameScene.setGameRunningPhysics()
// If this is a new game (not a resume), layout a new field
// and reset the clock. Yes, I know it's a nasty if statement.
// I'll probably refactor this in the next release.
if (gameScene.shouldReset)
gameScene.generateNewField()
// Unpause the scene
gameScene.frozen = false
method exitState()
// Do nothing. The base class defines an empty method for this
method onFrameUpdate()
// Respond to device motion (accelerometer and gyroscope)
motion = os.getCurrentMotion()
gameScene.physics.gravity = Vector(motion.roll, motion.pitch)
// Check time remaining
// Yup, more refactoring needed here, too. onClockExpired would be
// a good event to handle at the state level.
if gameScene.clock.isExpired()
gameScene.transitionToState(gameScene.gameOverState)
method onLightTouched(light)
// Toggle it off/on
light.on = not light.on
gameScene.updateScore()
// If all the lights are turned on, layout a new field
// This if-statement seems appropriate, actually.
if light.container.allOn()
gameScene.generateNewField()
method onPauseButton()
gameScene.transitionToState(gameScene.pausedState)
You get the idea. The other states follow the same pattern, doing what they're supposed to do when the events are triggered.
GameScene
Here are the relevant parts of the GameScene class.class GameScene
property preStartState
property runningState
property pausedState
property gameOverState
property currentState
method init()
// Lots of code to set up the scene.
preStartState.gameState = self
runningState.gameState = self
pausedState.gameState = self
gameOverState.gameState = self
transitionToState(preStartState)
method transitionToState(newState)
// Objective-C lets me get away with not checking for
// null first. Lazy? yes.
currentState.exitState()
currentState = newState
currentState.enterState()
method onStartButton()
currentState.onStartButton()
method onStartOverButton()
currentState.onStartOverButton()
method onPauseButton()
currentState.onPauseButton()
method onResumeButton()
currentState.onResumeButton()
// This method is called by the "os"
method frameUpdate()
currentState.onFrameUpdate()
There you have it. PreStartState goes to RunningState. RunningState goes to PausedState and GameOverState. PausedState goes to RunningState, setting the shouldReset flag or not depending on whether the Resume or Start Over button was pushed. GameOverState goes to RunningState.
Each state sets up the UI according to its context. In the case of this game on iOS, touch events are translated by the OS into the "onButton" events in the view controller, which delegates the events to the GameScene, which in turn delegates them to the current SceneState, which is where the knowledge of what to do is programmed.
That's the essence of the State design pattern applied to my game architecture. It's not necessarily revolutionary, but it's made a world of difference in the way I track down problems and solve them.