State Machine canvas example

For todays canvas example I made a state machine that helps to keep code broken down into many independent state objects. For simple canvas examples and projects a state machine is not needed, but if I am starting to make a serious project the use of a state machine becomes more important as a way to keep things better organized.

Say you want to make a project that is fairly complex and there are many states that the project needs to preform before it can even be used. For example content needs to be downloaded, and then some objects need to be initialized before the project can be started at which point the user can interact with it. In addition even when it is up and running there are many menus that the user can navigate between before starting a main game state. Once a game is over there are often two kind of outcomes to the end of the game, and how they should be treated when updating a game save. So in that kind of situation some way to compartmentalize all these different states of sorts needs to be implemented and such a thing if often referred to as a state machine.

Many frameworks such as phaser will have a state machine as part of the functionality of the framework. When it comes to making a canvas framework I often think that a state machine should be a part of such a framework, but I guess it does not have to be when it comes to making such a project. In any case this post will be on just one way to go about making a state machine by itself without much more beyond that.

1 - The State Machine module for canvas examples

In this section I will be going over the source code of the state machine module that I worked out for this post, and might use in future canvas examples and projects as is, or in a custom mutated form. The module makes use of the IFFE pattern and returns a public API as a single function that creates a state machine object. Once I have a state machine object I can then call the load method as a way to start defining state objects.

1.1 - The start of the module, and a parse container argument helper

So I start out the module with the begging part of an IIFE that will close at the end of the module. At the very top of the IIFE I have a parse container helper method. When the main public function is called I can pass a container argument as the first argument. The argument can be an object which is assumed to be a container element, it can also be a string that is assumed to be an id to an element to use. All other possible values including undefined will result in the body element being used as the container element to attach to.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var Machine = (function () {
// PARSE arguments
// Parse a container argument
var parseContainer = function (container) {
// if object assume element that is to be used as the container
if (typeof container === 'object' && container != null) {
return container;
}
// if string assume id
if (typeof container === 'string') {
return document.getElementById(container);
}
// if we get this far return document.body
return document.body;
};

1.2 - Create canvas helper

Here I have a helper that is used to create and append the canvas element to the given state object. This method assumes that a container element is attached to the state machine instance so that value should be parsed before this method is called.

1
2
3
4
5
6
7
8
9
10
11
12
13
// CANVAS
// create a canvas for the given state machine
var createCanvas = function (sm, w, h) {
sm.canvas = document.createElement('canvas');
sm.ctx = sm.canvas.getContext('2d');
sm.container.appendChild(sm.canvas);
sm.canvas.width = w || 320;
sm.canvas.height = h || 240;
// fill black for starters
sm.ctx.fillStyle = 'black';
sm.ctx.fillRect(0, 0, sm.canvas.width, sm.canvas.height);
};

For starters it draws a plain black background to the canvas, and the width and height can also be set via the arguments that are passed along from the arguments of the main public function that I will be getting to later.

1.3 - Get canvas relative position

This helper method just returns a canvas relative position from an event object that was gained from within an event hander. Without this I would end up with a window relative position which is the default for event handlers that have to do with mouse and touch events.

1
2
3
4
5
6
7
8
9
10
11
var getCanvasRelative = function (e) {
var canvas = e.target,
bx = canvas.getBoundingClientRect();
var x = (e.changedTouches ? e.changedTouches[0].clientX : e.clientX) - bx.left,
y = (e.changedTouches ? e.changedTouches[0].clientY : e.clientY) - bx.top;
return {
x: x,
y: y,
bx: bx
};
};

I made it so that it should give a desired result regardless if it is a mouse or touch event. The method seems to work okay as far as I have tested it.

1.4 - Attach event handers to a canvas element

This is the method that I worked out for attaching events to the canvas for event types like mouse down. The state manager instance should be passed as the first argument, and the canvas element should be created and append before this method is used.

In the body of the hander that is attached for this given DOM event type the get canvas relative position helper that I wrote about earlier in this section is used to get a canvas relative position for the event. This position will be passed to the actual hander defined in the state object as the first argument for that kind of hander, along with the state machine instance and the original event object.

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
// attach a canvas event
var attachCanvasEvent = function (sm, DOMType, smType) {
sm.canvas.addEventListener(DOMType, function (e) {
var pos = getCanvasRelative(e),
stateObj = sm.states[sm.currentState],
handler,
mode;
// prevent default
e.preventDefault();
// call top level if there
if (stateObj.userPointer) {
handler = stateObj.userPointer[smType];
if (handler) {
handler(pos, sm, e);
}
}
// call for current mode if there
if (stateObj.modes && sm.currentMode) {
mode = stateObj.modes[sm.currentMode];
if (mode.userPointer) {
handler = mode.userPointer[smType];
if (handler) {
handler(pos, sm, e);
}
}
}
});
};
// attach canvas events for the given state machine
var attachAllCanvasEvents = function (sm) {
attachCanvasEvent(sm, 'mousedown', 'start');
attachCanvasEvent(sm, 'mousemove', 'move');
attachCanvasEvent(sm, 'mouseup', 'end');
attachCanvasEvent(sm, 'touchstart', 'start');
attachCanvasEvent(sm, 'touchmove', 'move');
attachCanvasEvent(sm, 'touchend', 'end');
};

1.5 - The public function that creates a state machine object

So now it is time for the public function that is used to create the state machine instance when making a project with this state machine module.

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
// create a new state machine
return function (container, w , h) {
// state machine Object
var sm = {
currentState: null,
currentMode: null,
game: {},
draw: {},
states: {},
canvas: null,
container: parseContainer(container),
ctx: null,
load: function (stateObj) {
// just reference the object for now as long as
// that works okay
sm.states[stateObj.name || 'game'] = stateObj;
if (stateObj.bootState) {
sm.currentState = stateObj.name;
}
},
start: function (stateName) {
sm.currentState = stateName || sm.currentState;
var init = sm.states[sm.currentState].init || null;
if (init) {
init(sm);
}
loop();
}
};
// create canvas and attach event handlers
createCanvas(sm, w, h);
attachAllCanvasEvents(sm);
// main loop
var loop = function () {
requestAnimationFrame(loop);
var stateObj = sm.states[sm.currentState] || {};
// call top level tick
if (stateObj.tick) {
stateObj.tick(sm);
}
// call mode tick
if (stateObj.modes && sm.currentMode) {
var mode = stateObj.modes[sm.currentMode];
if (mode.tick) {
mode.tick(sm);
};
}
};
return sm;
};
}
());

2 - Simple use case example

So here is a simple use case example of the state machine module in action. This results in a very basic clicker type game example where some resources are given when the canvas is clicked, as well as just over time.

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
var sm = Machine('gamearea');
sm.load({
name: 'game',
bootState: true,
init: function (sm) {
var g = sm.game;
g.manual = 0;
g.perManual = 0.25;
g.auto = 0;
g.autoTickRate = 3000;
g.perAutoTick = 1;
g.lt = new Date();
},
tick: function (sm) {
var g = sm.game,
ctx = sm.ctx,
now = new Date(),
t = now - g.lt;
if (t >= g.autoTickRate) {
var ticks = t / g.autoTickRate;
g.auto += ticks * g.perAutoTick;
g.lt = now;
}
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, sm.canvas.width, sm.canvas.height);
ctx.fillStyle = 'white';
ctx.fillText('manual: ' + g.manual.toFixed(2), 10, 20);
ctx.fillText('auto: ' + g.auto.toFixed(2), 10, 30);
},
userPointer: {
start: function (pt, sm, e) {
console.log(e.type, pt.x, pt.y);
sm.game.manual += sm.game.perManual;
}
}
});
sm.start();

3 - Modes use case example

I wanted to make another example that makes use of my modes feature that I put together. This is a way to have a state within a state sort of speak. When a mode is active additional logic that is to happen when the mode is active will happen on top of the logic that will always run for the state.

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
var sm = Machine('gamearea', 640, 480);
sm.load({
name: 'game',
bootState: true,
init: function (sm) {
var g = sm.game;
g.ship = {
x: sm.canvas.width / 2,
y: sm.canvas.height / 2,
heading: 0
};
g.userDown = false;
},
tick: function (sm) {
var g = sm.game,
ctx = sm.ctx;
// set mode to nav conditions
sm.currentMode = null;
if (g.userDown) {
if (new Date() - g.userDownST >= 1000) {
sm.currentMode = 'nav';
}
}
// draw
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, sm.canvas.width, sm.canvas.height);
ctx.fillStyle = 'white';
ctx.fillText(g.userDown, 10, 20);
ctx.strokeStyle = 'white';
ctx.beginPath();
ctx.arc(g.ship.x, g.ship.y, 5, 0, Math.PI * 2);
ctx.stroke();
},
// global user pointer
userPointer: {
start: function (pt, sm, e) {
sm.game.userDown = true;
sm.game.userDownST = new Date();
},
end: function (pt, sm, e) {
sm.game.userDown = false;
}
},
// modes for this state
modes: {
nav: {
// what to do for each tick, when nav mode is active
tick: function (sm) {
var g = sm.game,
ship = g.ship;
// move ship based on current heading
ship.x += Math.cos(ship.heading) * 2;
ship.y += Math.sin(ship.heading) * 2;
// boundaries
ship.x = ship.x < 0 ? sm.canvas.width : ship.x;
ship.y = ship.y < 0 ? sm.canvas.height : ship.y;
ship.x = ship.x > sm.canvas.width ? 0 : ship.x;
ship.y = ship.y > sm.canvas.height ? 0 : ship.y;
},
// user pointer just for nav
userPointer: {
// mouse move event can change heading now
move: function (pt, sm, e) {
var g = sm.game,
ship = g.ship;
ship.heading = Math.atan2(pt.y - ship.y, pt.x - ship.x);
}
}
}
}
});
sm.start();

4 - Conclusion

So with this canvas example I have together what looks like a somewhat useful state machine module. I have not battle tested this though, so I would not really go about using this just yet aside from a hobby project maybe. If I get around to it I might get to sining some more time into this one though, and also pull this together with a whole bunch of other components that I am working out to make my own canvas framework or sorts that I might use to make a few games with. Do not hold your breath with that one though, I have way to many competing ideas when it comes to what I want to focus on.