Game state

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:

  1. A game state model
  2. Game state events (signals)
  3. A view class for each element on screen

Game state model

Game state is a basically a single class that implements all game logic, which in many cases represent the flow. For example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 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 (signals)

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:

1
2
3
4
5
6
7
// 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();
}

 

View classes

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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 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
                });
            }
        }
    }
};

 

Summary

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.