grid game unit movement javaScript example

So this week I started working on a new canvas example prototype, and the very first minor release of the prototype thus far strikes me as something good to write about as a simple stand alone javaScript example post. Thus far it is just a simple example of having a grid, and having a player unit move around in the grid when a player clicks on a given cell location. The basic idea that I have together thus far with it could be taken in a whole range of different directions when it comes to making it into something that is more of a game beyond that of what I have in mind for the canvas example prototype. So I thought I would copy and past the source code over to another location and maintain it as just a simple starting point for a grid type game that involves moving a unit around a simple grid.

I have made many projects in the past that involve the use of a grid in one form or another such as my grid defense canvas example, I also have another canvas example when it comes to creating and drawing grids in general with canvas. Shortly after I wrote this post for the first time I made another example where the aim is to make a grid module that can be used over and over again from one project to another rather than making a custom solution for a single project or examples such as the case with this example that I am writing about here. The problem with that as I see it s far is that making a grid module is something that I never seem to get just right, so I need to keep making new ones. So maybe some times it is a good idea to just create a custom grid module on a project by project basis rather than trying to make some kind of magic grid module that will work well in every possible project.

However in this one I have an idea that I have not done yet with grids, and would like to move forward with it. Also in this post I am touching base on a lot of other topics when it comes to starting a foundation to which I will build on top of when it comes to making a real project rather than just yet another simple javaScript code example. This is a cycle that I would very much like to break at some point in my life.

It may seem as a very simple, trivial example, and for a veteran javaScript developer I suppose it is. However there are still many topics that are covered when it comes to just getting to this simple starting point, and also even when it comes to being an experienced javaScript developer there is the topic of how to go about structuring a complex projects that might at one point in the future consist of thousands of lines of code. This is a topic that I still strugle with even though I have many years of experience thus far. So then this should prove to be a nice little starting point for a simple game that involves a player controlled unit, so lets take a look at the source code.

1 - Getting started and the utility module of this grid unti movement javaScript example

This is a post on a simple client side javaScript example, so then it should go without saying that this is not a getting started with javaScript type post. I assume that you have at least some background with the basics of javaScript as well as other client side web development languages which include HTML and CSS. With that out of the way in this section I will be going over just the utilities module of the example leaving the other various files for later sections of this post.

Full source code is also on Github

If you are on Github and wondering if there is a location on Github where I am parking the source code that I am write about in this post there such a place will be found here in my test vjs repository. This is also where I pack the source code examples for my many other posts on native javaScript features and examples.

1.1 - The utility module

So first off here is a utility module that has some stand alone static methods that I am going to be using in one or more additional modules moving forward with the rest of the code. This is just a standard practice of sorts when it comes to making any kind of canvas example, or complex javaScript project. A popular javaScript library that can be described as a general unity library would of course be lodash, however with these javaScript example posts I like to do everything from the ground up, making my own custom cut libraries. I have another utilities library javaScript example post in which it get into detail about making this kind of module that is packed with all kinds of usual suspect type methods that I will often park in a file such as this. However I often do make custom versions of this kind of library on a project by project, and example by example type basis.

One method that I have at the ready is a typical distance formula function. Thus far I am using this function in my map module now when it comes to figuring what the weight should be when preforming path detection. Moe on that later on in the post when it comes to the section on the map module.

In this javaScript example I have a simple gird where when a cell location is clicked a player object will move in the direction of that location by one cell at a time. So I have a angle to point method that can be used to get a direction from one position to another that will be used in my main game module. This method makes use of the Math.atan2 method which is useful for these kinds of situations that have to do with angles. I have also made yet another javaScript example where I am making an angles module that is based of a library on angles that I like called just simply angles.js.

Another method that I have here is useful for getting a canvas relative rather than window relative location when it comes to pointer events. I will not be getting into this subject in detail as I have wrote a post on this topic before hand, so if you want to learn more about this you can check that out if interested. In this javaScript example I will be using this method when it comes to my crude yet functional state machine in my main.js file that ties everything together. I will be getting into that module more so later in this post when it comes to the section on the main javaScript file of the example where I am using the method when working with event handlers.

I then also have a crude yet effecting deep clone method that makes use of JSON to quickly deep clone objects. Thus far I am just using this in my map module as a way to quickly create a copy of a grid, and then use this copy of a grid to preform path detection. I can not recommend that this is a good solution for all situations in which one will need to deep clone objects though. For more on this topic you might want to check out my post on the lodash clone deep method.

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
// UTILS
var utils = {};
// distance
utils.distance = function (x1, y1, x2, y2) {
return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
};
// angle from one point to another
utils.angleToPoint = function (x1, y1, x2, y2, scale) {
scale = scale === undefined ? Math.PI * 2 : scale;
var aTan = Math.atan2(y1 - y2, x1 - x2);
return (aTan + Math.PI) / (Math.PI * 2) * scale;
};
// get a point relative to a canvas element rather than window
utils.getCanvasRelative = function (e) {
var canvas = e.target,
bx = canvas.getBoundingClientRect();
return {
x: Math.floor((e.touches ? e.touches[0].clientX : e.clientX) - bx.left),
y: Math.floor((e.touches ? e.touches[0].clientY : e.clientY) - bx.top),
bx: bx
};
};
// deep clone using JSON
utils.deepCloneJSON = function (obj) {
try{
return JSON.parse(JSON.stringify(obj));
}catch(e){
console.log(e.message);
console.log(obj);
return {};
}
};

2 - The map module that will be used to create the grid.

In order to get this example working I will need a grid in which to place the player object that will move on each grid location click. I could just have everything together in one module when it comes to a game project like this, but I am thinking ahead with this one and have decided to pull this part of the example into its own map module. For now I am going to be trying my best to keep this map module as simple as I can, however I am still going to want to add things like path detection so it is not going to be all that simple. With that said it has a few public methods that I may or may not expand on event more is I keep working on this project, but I am not sure there is much more I would want to add with this module at least.

There is of course a create method that I will be calling in my game module that I will be getting to in a later section in this post. When it comes to creating even a simple grid module there are all kinds of formats for the grid object that come to mind. Some developers might prefer some kind of format that involves an array of arrays, but as of late I prefer a solution that involves a single array and then using a formula to get or set the proper cell location.

After the create public method I have two methods that can be used to get a cell location in the map. One is just the basic get method that can get a cell by an index value, or an x and y cell location. The other method is what I will be using to get a cell location by way of a canvas relative pixel location.

In addition with a basic core set of methods to create and work with a map object, I have also added a number of methods that have to do with match detection. This code that I have for this is based off of what it is that I have worked out for my post on path detection. So now not only can I create a map, and get references to cells by a index or pixel location, but I can also get paths from one cell position to another. With that said by default all cells have a walkable property that by default is set to true. When it comes to my game module that will make use of this map module that is where I will want to set the walkable value of cells true and false as needed.

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
var mapMod = (function () {
// create Cells helper
var createCells = function (map) {
var cells = [];
var len = map.w * map.h,
i = 0;
while (i < len) {
cells.push({
i: i,
x: i % map.w,
y: Math.floor(i / map.w),
walkable: true,
closed: false,
data: {},
unit: null // reference to current unit here or null if empty
});
i += 1;
}
return cells;
};
// PUBLIC API
var api = {};
// create a new map object
api.create = function (opt) {
opt = opt || {};
var map = {
w: opt.w || 9,
h: opt.h || 7,
cellSize: 32,
margin: {
x: opt.marginX == undefined ? 5 : opt.marginX,
y: opt.marginY == undefined ? 5 : opt.marginY
},
cells: []
};
map.cells = opt.cells || createCells(map);
return map;
};
// return a cell at the given position, or false for out of bounds values
api.get = function (map, xi, y) {
if(arguments.length === 2){
return map.cells[xi];
}
if (xi < 0 || y < 0 || xi >= map.w || y >= map.h) {
return false;
}
return map.cells[y * map.w + xi];
};
// get a cell in the current map by way of
// a canvas relative x and y pixel pos
api.getCellByPointer = function (map, x, y) {
var cx = Math.floor((x - map.margin.x) / map.cellSize),
cy = Math.floor((y - map.margin.y) / map.cellSize);
return api.get(map, cx, cy)
};
/***
PATHS
***/
// sort a list of open nodes
var sortOpen = function (opened) {
return opened.sort(function (nodeA, nodeB) {
if (nodeA.weight < nodeB.weight) {
return 1;
}
if (nodeA.weight > nodeB.weight) {
return -1;
}
return 0;
});
};
// set weight for a node
var setWeight = function (endNode, neighbor) {
return utils.distance(endNode.x, endNode.y, neighbor.x, neighbor.y);
};
// build a path based an parent property
var buildPath = function (node) {
var path = [];
while (node.parent) {
path.push([node.x, node.y]);
node = node.parent;
}
//path.push([node.x, node.y]);
return path;
};
// for Each Neighbor for the given grid, node, and open list
var forNeighbors = function (grid, node, endNode, opened) {
//var neighbors = grid.getNeighbors(node);
var neighbors = mapMod.getNeighbors(grid, node);
var ni = 0,
nl = neighbors.length;
while (ni < nl) {
var neighbor = neighbors[ni];
// if the neighbor is closed continue looping
if (neighbor.closed) {
ni += 1;
continue;
}
// set weight for the neighbor
neighbor.weight = setWeight(endNode, neighbor);
// if the node is not opened
if (!neighbor.opened) {
neighbor.parent = node;
opened.push(neighbor);
neighbor.opened = true;
}
ni += 1;
}
};
api.getPath = function (grid, sx, sy, ex, ey) {
// copy the given grid
//var grid = Grid.fromMatrix(givenGrid.nodes),
var grid = utils.deepCloneJSON(grid),
//var grid = utils.deepClone(grid, {
// forRecursive: function(){ return {} }
//}),
nodes = api.chunk(grid),
path = [],
opened = [],
node;
// set startNode and End Node to copy of grid
var startNode = nodes[sy][sx];
endNode = nodes[ey][ex];
// push start Node to open list
opened.push(startNode);
startNode.opened = true;
startNode.weight = 0;
// start walking
while (opened.length > 0) {
// pop out next Node from open list
node = opened.pop();
node.closed = true;
// if the node is the end node
if (node === endNode) {
return buildPath(node);
}
// loop current neighbors
forNeighbors(grid, node, endNode, opened);
// sort the list of nodes be weight value to end node
sortOpen(opened);
}
// return an empty array if we get here (can not get to end node)
return [];
};
// get a chunk form of a grid
api.chunk = function (grid) {
var arr = [],
row,
i = 0;
while (i < grid.cells.length) {
row = grid.cells.slice(i, i + grid.w);
arr.push(row);
i += grid.w;
}
return arr;
};
// return true if the given x and y position is in bounds
api.isInBounds = function (grid, x, y) {
return (x >= 0 && x < grid.w) && (y >= 0 && y < grid.h);
};
// is the given cell location walkable?
api.isWalkable = function (grid, x, y) {
if (api.isInBounds(grid, x, y)) {
return api.get(grid, x, y).walkable; //grid.nodes[y][x].walkable;
}
return false;
};
// get the four Neighbors of a node
api.getNeighbors = function (grid, node) {
var x = node.x,
y = node.y,
neighbors = [];
if (api.isWalkable(grid, x, y - 1)) {
//neighbors.push(this.nodes[y - 1][x]);
neighbors.push(mapMod.get(grid, x, y - 1));
}
if (api.isWalkable(grid, x, y + 1)) {
//neighbors.push(this.nodes[y + 1][x]);
neighbors.push(mapMod.get(grid, x, y + 1));
}
if (api.isWalkable(grid, x - 1, y)) {
//neighbors.push(this.nodes[y][x - 1]);
neighbors.push(mapMod.get(grid, x - 1, y));
}
if (api.isWalkable(grid, x + 1, y)) {
//neighbors.push(this.nodes[y][x + 1]);
neighbors.push(mapMod.get(grid, x + 1, y));
}
return neighbors;
};
// return the public API
return api;
}
());

3 - The game module

In this javaScript example the main module will be the state machine object that I will be getting to later in this post. However the game module is still a major component that will contain everything that has to do with the state of the game, rather than the application as a whole. This means the state of the map as well as the units that will be located in the map as well.

Here in the game module I have a create method that will be used to create a new game state that will contain at least one instance of a map object for starters, and helper methods that can be used to create the player object. So the game module is the main state object for the state of the actual game in terms of the state of the map, and any display objects that might be in the map, or out of it actually. For now it is just the player object, as well as just simple wall units that I am concern with, and in time as I develop this project much of the code here will be pulled into another module that has to do with object pools, and units in general when and if I get to it.

So for now I have all of my unit methods and various related helper functions at the top of this game module helper. I then have a create base unit helper that is used to create a unit object with all properties that the unit of any kind should have. As of revision 3 the only real properties of interest with a unit would be the sheetIndex, and currentCellIndex properties.

With path detection now added to the map module as revision 3 of the example the place unit method will not set the walkable property of a cell to true when a unit is located in the cell, as well as set the value back to false when moving the unit to a new cell location. Because of this and any additional factors of concern moving forward the place unit method should always be used when moving any unit from one location to another or placing a new unit into a map.

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
var gameMod = (function () {
/********** **********
TO MAP OBJECT
*********** *********/
// get to index helper use to get the map index to go to for the game.toMap object
var getToIndex = function(game){
var toIndex = null,
p = game.player,
map = game.maps[game.mapIndex],
pCell = api.getPlayerCell(game),
mwx = game.mapIndex % game.mapWorldWidth, // map world x and y
mwy = Math.floor(game.mapIndex / game.mapWorldWidth );
// if player cell x equals 0 ( left side )
if(pCell.x === 0){
var x = mwx - 1;
x = x < 0 ? game.mapWorldWidth - 1 : x;
toIndex = mwy * game.mapWorldWidth + x;
}
// if player cell x equals map.w - 1 ( right side )
if(pCell.x === map.w - 1){
var x = mwx + 1;
x = x >= game.mapWorldWidth ? 0 : x;
toIndex = mwy * game.mapWorldWidth + x;
}
// if player cell y equals 0 ( top side )
if(pCell.y === 0){
var y = mwy - 1;
y = y < 0 ? game.maps.length / game.mapWorldWidth - 1 : y;
toIndex = y * game.mapWorldWidth + mwx;
}
// if player cell y map.h - 1 ( bottom side )
if(pCell.y === map.h - 1){
var y = mwy + 1;
y = y >= game.maps.length / game.mapWorldWidth ? 0 : y;
toIndex = y * game.mapWorldWidth + mwx;
}
return toIndex;
};
// Is a given cell at a corner? Used to get adjust goto point for game.toMap object
var isAtCorner = function(game, cell){
var map = game.maps[game.mapIndex],
w = map.w - 1,
h = map.h - 1;
return (cell.x === 0 && cell.y === 0) ||
(cell.x === w && cell.y === h) ||
(cell.x === 0 && cell.y === h) ||
(cell.x === w && cell.y === 0);
};
// get a toMap object that can be set to the game.toMap propery
var getToMap = function(game){
var toMap = {};
var map = game.maps[game.mapIndex];
var pCell = api.getPlayerCell(game);
var mi = toMap.index = getToIndex(game);
// at corner?
if(isAtCorner(game, pCell)){
if(pCell.y === map.h - 1){
toMap.x = pCell.x;
toMap.y = 0;
}else{
toMap.x = pCell.x;
toMap.y = map.h - 1;
}
}else{
// not at corner
toMap.x = pCell.x === 0 ? map.w - 1 : pCell.x;
toMap.y = pCell.y === 0 ? map.h - 1 : pCell.y;
toMap.x = pCell.x === map.w - 1 ? 0 : toMap.x;
toMap.y = pCell.y === map.h - 1 ? 0 : toMap.y;
}
return toMap;
};
/********** **********
MOVEMENT PATHS
*********** *********/
// get a move path in the from of a path created using mapMod.getPath that is cut
// based on the maxCellsPerTurn value of the unit in the given start cell if any
var getMovePath = function(game, startCell, targetCell){
// get current map
var map = game.maps[game.mapIndex],
unit = startCell.unit || null;
// get the raw path to that target cell
var path = mapMod.getPath(map, startCell.x, startCell.y, targetCell.x, targetCell.y);
// get a slice of the raw path up to unit.maxCellsPerTurn
if(unit){
path = path.reverse().slice(0, unit.maxCellsPerTurn);
}
// return the path
return path;
};
// get an arary of cell index values
var getMoveCells = function(game, startCell, targetCell){
var map = game.maps[game.mapIndex];
return getMovePath(game, startCell, targetCell).map(function(pos){
var cell = mapMod.get(map, pos[0], pos[1]);
return cell.i;
});
};
// get enemy move cells options
var getEnemeyMoveCells = function(game, eCell){
var pCell = api.getPlayerCell(game),
map = game.maps[game.mapIndex];
// get neighbor cells of the player unit
var pCellNeighbors = mapMod.getNeighbors(map, pCell).filter(function(cell){
return cell.walkable;
});
// get an array of path options
var mtcOptions = pCellNeighbors.map(function(cell){
return getMoveCells(game, eCell, cell)
}).filter(function(mtcOptions){
return mtcOptions.length > 0;
});
// rteurn first path or empty array
return mtcOptions[0] || [];
};
/********** **********
UNITS
*********** *********/
// create a base unit
var createBaseUnit = function () {
return {
// current unit stats
maxHP: 1, // max number of hit points for the unit
maxCellsPerTurn: 0, // the max number of cells a unit can move
// current values
HP: 1,
weaponIndex: 0,
sheetIndex: 0,
type: null,
meleeTarget: null, // cell index to attack in 'melee' processTurn state
moveCells: [], // array of cells to move in 'move' processTurn state
currentCellIndex: null,
active: false
}
};
// create a player unit
var createPlayerUnit = function () {
var player = createBaseUnit();
player.type = 'player';
player.active = true;
player.maxCellsPerTurn = 3;
player.sheetIndex = 2; // player sheet
player.maxHP = 30;
return player;
}; // create a player unit
var createEnemyUnit = function () {
var enemy = createBaseUnit();
enemy.type = 'enemy';
enemy.active = true;
enemy.maxCellsPerTurn = 2;
enemy.sheetIndex = 3;
enemy.maxHP = 5;
return enemy;
};
// create a player unit
var createWallUnit = function () {
var wall = createBaseUnit();
wall.type = 'wall';
wall.active = true;
wall.sheetIndex = 1;
return wall;
};
// place a unit at the given location in the current map
var placeUnit = function (game, unit, x, y) {
var map = game.maps[game.mapIndex];
var newCell = mapMod.get(map, x, y);
if (newCell) {
// any old cellIndex that may need to have walkable
// set back to true?
if (unit.currentCellIndex != null) {
var oldCell = map.cells[unit.currentCellIndex];
oldCell.walkable = true;
// set unit ref back to null
map.cells[unit.currentCellIndex].unit = null;
}
// set new cell to not walkable as a unit is now located here
newCell.walkable = false;
// set current cell index for the unit
unit.currentCellIndex = newCell.i;
// place a ref to the unit in the map cell
map.cells[unit.currentCellIndex].unit = unit; // map ref to unit
}
};
// place player helper that is called when setting up a new game, and when the player
// moves to a new map
var placePlayer = function(game){
var map = game.maps[game.mapIndex],
toMap = game.toMap,
toCell = null,
i = map.cells.length;
// get a toCell ref if we have a pos in game.toMap
if(toMap.x != null && toMap.y != null){
toCell = mapMod.get(map, toMap.x, toMap.y);
}
// if we have a toCell
if(toCell){
if(!toCell.unit){
placeUnit(game, game.player, toCell.x, toCell.y);
game.toMap = getToMap(game);
return;
}
}
// if we get this far just find a spot
while(i--){
var cell = map.cells[i];
if(cell.unit === null){
placeUnit(game, game.player, cell.x, cell.y);
game.toMap = getToMap(game);
return;
}
}
};
// move a unit by way of any cell index values in unit.moveCells
var moveUnit = function(game, unit){
if(unit.moveCells.length > 0){
var ci = unit.moveCells.shift();
var moveToCell = mapMod.get(game.maps[game.mapIndex], ci);
// if no unit at the move to cell
if(!moveToCell.unit){
placeUnit(game, unit, moveToCell.x, moveToCell.y);
}
// !!! might not hurt to do this for all units
// also this might not belong here but where this method
// is called for the player unit maybe
if(unit.type === 'player'){
game.toMap = getToMap(game);
}
}
};
/********** **********
MAP HELPERS
*********** *********/
// get an array of cell objects by a given unit type string in the given map
var getCellsByUnitType = function(map, type){
return map.cells.reduce(function(acc, cell){
if(cell.unit){
if(cell.unit.type === type){
acc.push(cell);
}
}
return acc;
},[]);
};
/********** **********
SETUP GAME
*********** *********/
// setUp game helper with game object, and given maps
var setupGame = function (game, newGame) {
newGame = newGame === undefined ? true : newGame;
var playerPlaced = false,
startMapIndex = 0;
game.mapIndex = 0;
// set player HP to max
game.player.HP = game.player.maxHP;
if(newGame){
game.remainingEnemies = 0;
}
// set up maps
game.maps = game.maps.map(function(map, mi){
var mapStr = game.mapStrings[mi] || '';
game.mapIndex = mi;
map.cells = map.cells.map(function(cell, ci){
var cellIndex = parseInt(mapStr[ci] || '0'),
x = ci % map.w,
y = Math.floor(ci / map.w);
if(cellIndex === 0 && newGame){
var cell = mapMod.get(map, ci);
cell.unit = null;
cell.walkable = true;
}
// wall blocks set for new games and not
if(cellIndex === 1){
var wall = createWallUnit();
placeUnit(game, wall, x, y);
}
// player always set
if(cellIndex === 2){
playerPlaced = true;
startMapIndex = mi;
placeUnit(game, game.player, x, y);
}
// enemy
if(cellIndex === 3 && newGame){
game.remainingEnemies += 1;
var enemy = createEnemyUnit();
enemy.HP = enemy.maxHP;
placeUnit(game, enemy, x, y);
}
return cell;
});
return map;
});
// if player is not palced then place the player unit
// at a null cell
if(!playerPlaced){
placePlayer(game);
}
game.mapIndex = startMapIndex;
game.toMap = getToMap(game);
};
/********** **********
gameMod.create PUBLIC METHOD
*********** *********/
var api = {};
// create a new game state
api.create = function (opt) {
opt = opt || {};
//var mapStrings = opt.maps || ['2'];
var game = {
// mode: 'map', // not using game.mode at this time
turn: 0,
turnState: 'wait',
maps: [],
mapIndex: 0,
mapWorldWidth: 3, // used to find toIndex
toMap: {
index: null,
x: null,
y: null
},
mapStrings: opt.maps || ['2'],
player: createPlayerUnit(),
remainingEnemies: 0
};
game.mapStrings.forEach(function(){
game.maps.push(mapMod.create({
marginX: opt.marginX === undefined ? 32 : opt.marginX,
marginY: opt.marginY === undefined ? 32 : opt.marginY,
w: opt.w === undefined ? 4 : opt.w,
h: opt.h === undefined ? 4 : opt.h
}));
});
setupGame(game, true);
return game;
};
/********** **********
gameMod.update PUBLIC METHOD
*********** *********/
var processMeele = function(game, unit){
var targetCellIndex = unit.meleeTarget,
map = game.maps[game.mapIndex];
if(targetCellIndex != null){
var targetCell = mapMod.get(map, targetCellIndex),
tUnit = targetCell.unit;
if(tUnit){
tUnit.HP -= 1;
tUnit.HP = tUnit.HP < 0 ? 0 : tUnit.HP;
// enemy unit death check
if(tUnit.HP <= 0 && tUnit.type === 'enemy'){
targetCell.walkable = true;
targetCell.unit = null;
}
}
unit.meleeTarget = null;
}
};
// get remaining Enemies helper used to update game.remainingEnemies in 'end' process turn state
var getRemainingEnemies = function(game){
return game.maps.reduce(function(acc, map){
var eCells = getCellsByUnitType(map, 'enemy');
return acc + eCells.length;
}, 0);
};
// process turn method used in gameMod.update
var processTurn = function(game, secs){
var map = game.maps[game.mapIndex],
pCell = api.getPlayerCell(game),
eCells = getCellsByUnitType(map, 'enemy');
// do nothing for 'wait' state
if(game.turnState === 'wait'){
return;
}
// starting a new turn
if(game.turnState === 'start'){
// let enemy units figure paths
eCells.forEach(function(eCell){
var d = utils.distance(eCell.x + 16, eCell.y + 16, pCell.x + 16, pCell.y + 16);
if( d <= 1.5){
// in melee range player
eCell.unit.meleeTarget = pCell.i;
}else{
// not in melee range of player
eCell.unit.moveCells = getEnemeyMoveCells(game, eCell);
}
//console.log(eCell.unit.moveCells);
});
game.turnState = 'move';
}
// move state
if(game.turnState === 'move'){
// move player unit
moveUnit(game, game.player);
eCells.forEach(function(eCell){
moveUnit(game, eCell.unit);
});
var eCells = getCellsByUnitType(map, 'enemy');
// if moveCells array length of all units === 0 the move state is over
if(game.player.moveCells.length === 0 && eCells.every(function(eCell){
return eCell.unit.moveCells.length === 0;
})){
game.turnState = 'melee';
}
}
// melee attack
if(game.turnState === 'melee'){
// process any player melee attack
processMeele(game, game.player);
// process melee attacks for enemy units
var eCells = getCellsByUnitType(map, 'enemy');
eCells.forEach(function(eCell){
processMeele(game, eCell.unit);
});
game.turnState = 'end';
}
// for end state step game.turn and set game.turnState back to wait
if(game.turnState === 'end'){
game.turn += 1;
game.turnState = 'wait';
// check for player death
if(game.player.HP <= 0){
// !!! for now just call setupGame
pCell.unit = null;
pCell.walkable = true;
setupGame(game, false);
}
// check for all enemies dead
game.remainingEnemies = getRemainingEnemies(game);
if(game.remainingEnemies === 0){
setupGame(game, true);
}
}
};
// update a game object
api.update = function (game, secs) {
// just call process turn for now
processTurn(game, secs);
};
// get player cell
api.getPlayerCell = function(game){
var p = game.player,
map = game.maps[game.mapIndex];
return map.cells[p.currentCellIndex];
};
// preform what needs to happen for a player pointer event for the given pixel positon
api.playerPointer = function(game, x, y){
var clickedCell = mapMod.getCellByPointer(game.maps[game.mapIndex], x, y),
map = game.maps[game.mapIndex],
pCell = api.getPlayerCell(game);
// if we have a cell
if (clickedCell) {
// if player cell is clicked and there is a toIndex value
if(clickedCell === pCell && game.toMap.index != null){
game.mapIndex = game.toMap.index;
game.toMap = getToMap(game);
pCell.unit = null;
pCell.walkable = true;
game.player.currentCellIndex = null;
placePlayer(game);
return;
}
// if cell has a unit on it
if(clickedCell.unit){
var unit = clickedCell.unit;
if(unit.type === 'enemy'){
// set meleeTarget index
game.player.meleeTarget = clickedCell.i;
game.turnState = 'start';
return;
}
}
// default action is to try to move to the cell
game.player.moveCells = getMoveCells(game, pCell, clickedCell);
game.turnState = 'start';
}
};
// return the public API
return api;
}
());

4 - The draw module

So now that I have all the modules that can be used to create a main game object state, I am going to want to have a way to create a view for this state object. So I will then need module that can be used to draw to a canvas element which will be this draw.js file. With this example this far I just have a few draw methods one of which is to just draw a simple static background, another is to draw the state of the map as a whole, and I have another that just draws some basic state info.

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
var draw = (function () {
// public api
var api = {};
// unit colors
var unitColors = ['green', 'gray', 'blue', 'red'];
/********** **********
HELPERS
*********** *********/
// draw an hp bar for the given cell, if it has a vaild unit
var drawHPBar = function(sm, cell){
var unit = cell.unit;
var ctx = sm.ctx;
var map = sm.game.maps[sm.game.mapIndex];
var cs = map.cellSize;
var x = map.margin.x + cell.x * cs;
var y = map.margin.y + cell.y * cs;
if (unit) {
if(unit.type == 'player' || unit.type === 'enemy'){
// hp bar back
ctx.fillStyle = 'gray';
ctx.beginPath();
ctx.rect(x, y, cs, 5);
ctx.fill();
ctx.stroke();
// current hp
var per = unit.HP / unit.maxHP;
ctx.fillStyle = 'lime';
ctx.beginPath();
ctx.rect(x, y, cs * per, 5);
ctx.fill();
ctx.stroke();
}
}
};
// draw a cell helper
var drawCell = function(sm, cell){
var map = sm.game.maps[sm.game.mapIndex];
var ctx = sm.ctx;
var cs = map.cellSize;
var x = map.margin.x + cell.x * cs;
var y = map.margin.y + cell.y * cs;
// draw base cell
ctx.fillStyle = unitColors[0];
// if we have a unit
if (cell.unit) {
ctx.fillStyle = unitColors[cell.unit.sheetIndex];
}
ctx.beginPath();
ctx.rect(x, y, cs, cs);
ctx.fill();
ctx.stroke();
drawHPBar(sm, cell);
};
/********** **********
PUBLIC API
*********** *********/
// draw background
api.back = function (sm) {
var canvas = sm.canvas,
ctx = sm.ctx;
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
};
// draw the map
api.map = function (sm) {
var map = sm.game.maps[sm.game.mapIndex],
i = 0,
len = map.cells.length;
while (i < len) {
drawCell(sm, map.cells[i]);
i += 1;
}
};
// draw info
api.info = function (sm) {
var ctx = sm.ctx,
pos = sm.input.pos,
pCell = gameMod.getPlayerCell(sm.game),
canvas = sm.canvas,
dy = 14;
// text style
ctx.fillStyle = 'yellow';
ctx.font = '12px courier';
ctx.textBaseline = 'top';
// draw current pointer position
ctx.fillText('pos: ' + pos.x + ',' + pos.y, 5, 5 + dy * 0);
// player cell pos
ctx.fillText('player pos: ' + pCell.x + ',' + pCell.y, 5, 5 + dy * 1);
// to map values
var tm = sm.game.toMap;
ctx.fillText('toMap: mi:' + tm.index + ', x: ' + tm.x + ', y: ' + tm.y, 5, 5 + dy * 2);
// turn number and turnChange bool
ctx.fillText('turn:' + sm.game.turn + ', turnState: ' + sm.game.turnState, 5, 5 + dy * 3);
// enemies
ctx.fillText('enemies:' + sm.game.remainingEnemies, 5, 5 + dy * 4);
// version number
ctx.fillText('v' + sm.ver, 1, canvas.height - 11);
};
// return the public api to draw variable
return api;
}
());

5 - The main.js file and the start of a State machine

So now it is time to get to my main.js file for this javaScript example where I will make use of all the modules that I have put together for this example. Here I create the canvas element that I will be using, and set up a simple state machine object that for the same of this example is not really much of a state machine but just a place holder for such a thing. Also here in the main.js file I have my main application loop that is typically for any of my canvas examples.

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
(function () {
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d'),
container = document.getElementById('canvas-app') || document.body;
container.appendChild(canvas);
canvas.width = 320;
canvas.height = 240;
ctx.translate(0.5, 0.5);
// disable default action for onselectstart
canvas.onselectstart = function () { return false; }
var sm = {
ver: '0.7.0',
fps: 12,
lt: new Date(),
game: gameMod.create({
marginX : 14,
marginY : 7,
w: 9,
h: 7,
maps: [
// TOP
'111111111' +
'100000000' +
'103030000' +
'100000000' +
'100000000' +
'100000000' +
'100000001',
'111111111' +
'000000000' +
'000000300' +
'000000000' +
'000000000' +
'000000000' +
'100100000',
'111111111' +
'000000001' +
'000000301' +
'000000001' +
'000000301' +
'000000001' +
'000000301',
// middle
'100000001' +
'103000001' +
'100000000' +
'100000001' +
'100000001' +
'103000001' +
'100000001',
'100100000' +
'100111010' +
'020001010' +
'100101030' +
'100100111' +
'100100000' +
'100100000',
'000000001' +
'000000001' +
'000000031' +
'000111111' +
'111100031' +
'000000001' +
'000000001',
// bottom
'100000001' +
'100000001' +
'100000001' +
'103000000' +
'100000000' +
'103030000' +
'111111111',
'100100000' +
'100000000' +
'100000000' +
'000000000' +
'000000000' +
'000000300' +
'111111111',
'000000001' +
'000000001' +
'000000001' +
'000000301' +
'000000001' +
'000000301' +
'111111111'
]
}),
canvas: canvas,
ctx: ctx,
input: {
pointerDown: false,
pos: {
x: 0,
y: 0
}
}
};
var pointerHanders = {
start: function (sm, e) {
var pos = sm.input.pos = utils.getCanvasRelative(e);
if(e.type === 'touchstart'){
e.preventDefault();
}
sm.input.pointerDown = true;
// call player pointer method in gameMod
gameMod.playerPointer(sm.game, pos.x, pos.y);
},
move: function (sm, e) {
sm.input.pos = utils.getCanvasRelative(e);
},
end: function (sm, e) {
sm.input.pointerDown = false;
}
};
var createPointerHandler = function (sm, type) {
return function (e) {
pointerHanders[type](sm, e);
};
};
canvas.addEventListener('touchstart', createPointerHandler(sm, 'start'));
canvas.addEventListener('touchmove', createPointerHandler(sm, 'move'));
canvas.addEventListener('touchend', createPointerHandler(sm, 'end'));
canvas.addEventListener('mousedown', createPointerHandler(sm, 'start'));
canvas.addEventListener('mousemove', createPointerHandler(sm, 'move'));
canvas.addEventListener('mouseup', createPointerHandler(sm, 'end'));
// loop with frame capping set by sm.fps value
var loop = function () {
var now = new Date(),
secs = (now - sm.lt) / 1000;
requestAnimationFrame(loop);
if(secs >= 1 / sm.fps){
gameMod.update(sm.game);
draw.back(sm);
draw.map(sm);
draw.info(sm);
sm.lt = now;
}
};
loop();
}
());

6 - Conclusion

So for now I have a decent starting point for a game, but there are all ready thins that I might choose to do differently when it comes to this basic starting point. Like many javaScript projects that make use of canvas I have a main update loop that is calling requestAnimationFrame over and over again. I decided to keep that, but the thought did occur that I might want to make this project completely event driven rather than having an update loop fire all the time. Also when it comes to keeping it there are many little subtle improvements that are needed that I have not got to yet, but have done in other projects.

Still for now I just wanted to get this the point where I am just moving a player object around in a grid, and that is it. I had it set in my mind as to what the first step is, and I completed that. There are going to be additional steps that involve making various invisible improvements that do not really change the behavior, or looks an feel of the project but that was not what I wanted to get done for the moment.