A friend asked me to give an example of a game state model, I was referring to when I tried to explain him how I develop simple games and touch screen applications. Here I’ll be trying to explain this, by extracting some example code from The secrets of three paintings app.
Be it ActionScript or JavaScript, whenever I’m writing relatively simple games or touch screen applications, I always use these 3:
Game state is a basically a single class that implements all game logic, which in many cases represent the flow. For example:
// excerpt from State class function State() { this.painting = 0; // selected painting this.info = false; // true if inside a paintings info this.backEnabled = false; // true if back button is enabled this.buttonsEnabled = false; // true if rotate, zoom and info buttons are enabled this.signals = new StateSignals(); // game state signals } State.prototype = { // I write a setter method for everything that can change in the app setPainting: function(value) { // this is what I refer to as game logics: if (value != this.painting && // a painting can be selected only if it's not already selected (value === 0 || // and either we are exiting a painting this.painting === 0)) { // or no other painting is selected var prevValue = this.painting; this.painting = value; this.signals.changePainting.dispatch(prevValue); // notify all subscribed view elements } }, setZoom: function(value) { if (this.zoom != value) { this.zoom = value; if (value) { // if inside zoom, disable rotate, zoom and info buttons this.setButtonsEnabled(false); } this.signals.changeZoom.dispatch(this.zoom); // notify of the change all subscribed elements } }, setInfo: function(value) { if (this.info != value) { this.info = value; if (value) { this.setButtonsEnabled(false); } this.signals.changeInfo.dispatch(this.info); } }, setButtonsEnabled: function(value) { if (this.buttonsEnabled != value) { this.buttonsEnabled = value; this.signals.changeButtonsEnabled.dispatch(this.buttonsEnabled); } }, // it's easy to implement back button logic, all state is in one place goBack: function() { if (this.painting) { // if inside a painting if (this.zoom) { // if in zoom this.setZoom(false); // exit zoom } else if (this.info) { // if in info this.setInfo(false); // exit info } else { // else exit current painting this.setPainting(0); // exit painting } } } };
Game state events or signals, because I prefer using Robert Penner’s AS3 Signals or it’s javascript port for this. These are what connect the Game State to the view elements. As you saw before in the Game State source code, there is a signal dispatch for every change. I group all the signals together in a StateSignals class:
// excerpt from StateSignals class function StateSignals() { this.changePainting = new signals.Signal(); this.changeZoom = new signals.Signal(); this.changeInfo = new signals.Signal(); this.changeButtonsEnabled = new signals.Signal(); }
I always try to keep view classes atomic, so that they only care about themselves, and do not access child or parent elements. If a child element needs to implement any view logic or transitions, I create a separate class for it.
// excerpt from BackButton class function BackButton() { state.signals.changeBackEnabled.add(this._changeBackEnabled, this); this.on("click", this._click, this); } BackButton.prototype = { _changeBackEnabled: function(value) { this._enableOrDisable(value); }, _enableOrDisable: function(value) { this.mouseEnabled = value; if (state.painting) { TweenLite.to(this, 0.5, {alpha: value ? 1 : 0}); } } _click: function() { state.goBack(); } }; // excerpt from Painting class function Painting() { this.on("click", this._click, this); state.signals.changePainting.add(this._changePainting, this); state.signals.changeInfo.add(this._changeInfo, this); } Painting.prototype = { _click: function() { state.setPainting(this.painting); }, _changePainting: function(prevValue) { if (state.painting === this.painting || prevValue === this.painting) { this._removeClickEvent(); // disable click event while inside this.parent.setChildIndex(this, this.parent.numChildren - 1); // bring to front var self = this, onComplete = function() { if (!state.painting) { self._addClickEvent(); // add click event when outside } }; if (state.painting) { // transition into painting TweenLite.to(this, 1, { easel: {frame: this.timeline.resolve("in")}, ease:Linear.easeNone, onComplete: onComplete }); } else { // transition out of painting TweenLite.to(this, 1, { easel:{frame: this.timeline.resolve("out")}, ease:Linear.easeNone, onComplete: onComplete }); } } }, _removeClickEvent: function() { while (this._listeners.click) { // remove click event listener this.off("click", this._listeners.click[0]); } }, _changeInfo: function(value) { if (this.painting === state.painting) { if (value) { // transition into info TweenLite.to(this, 0.75, { easel: {frame: this.timeline.resolve("info")}, ease:Linear.easeNone }); } else { // transition out of info TweenLite.to(this, 0.75, { easel:{frame: this.timeline.resolve("in")}, ease:Linear.easeNone }); } } } };
I hope this gives a rough idea of how the game state and with that – game logic can be kept outside of any view code.