I'm trying to reach good understanding of how to build scalable modular application structure. Recently I've been practicing with canvas and rewriting one of the popular snake game realizations. I've decided to try to split its code to modules. The codebase is not big and it seems to be a pretty nice exercise to me.
The game is a classic snake game - user controls the snake with arrow keys, when snake collides with food it grows, when the snake collides with screen border or itself it dies and the game's over.
The game involves a game
object, snake
object and food
object. I've tried to split the code to store each of these objects in a separate module and import all of these in the main application file, so I have the main main.js file, snake.js file, game.js and food.js.
snake.js represents the snake object and its behavior
import game from './game';
import food from './food';
let canvas = document.getElementById("game_field");
let context = canvas.getContext("2d");
let snake = {
size: canvas.width / 40,
x: null,
y: null,
color: '#0F0',
direction: 'left',
sections: [],
init: function () {
snake.sections = [];
snake.direction = 'left';
snake.x = canvas.width / 2 + snake.size / 2;
snake.y = canvas.height / 2 + snake.size / 2;
for (let i = snake.x + (5 * snake.size) ; i >= snake.x; i -= snake.size) {
snake.sections.push(i + ',' + snake.y);
}
},
move: function () {
let directions = {
'up': function () {
snake.y -= snake.size;
},
'down': function () {
snake.y += snake.size;
},
'left': function () {
snake.x -= snake.size;
},
'right': function () {
snake.x += snake.size;
}
};
directions[snake.direction]();
snake.checkCollision();
snake.checkGrowth();
snake.sections.push(snake.x + ',' + snake.y);
},
draw: function () {
for (let i = 0; i < snake.sections.length; i++) {
snake.drawSection(snake.sections[i].split(','));
}
},
drawSection: function (section) {
game.drawBox(parseInt(section[0]), parseInt(section[1]), snake.size, snake.color);
},
checkCollision: function () {
if (snake.isCollision(snake.x, snake.y) === true) {
game.stop();
}
},
isCollision: function (x, y) {
if (x < snake.size / 2 ||
x > canvas.width ||
y < snake.size / 2 ||
y > canvas.height ||
snake.sections.indexOf(x + ',' + y) >= 0) {
return true;
}
},
checkGrowth: function () {
if (snake.x == food.x && snake.y == food.y) {
game.score++;
if (game.score % 5 == 0 && game.fps < 60) {
game.fps++;
}
food.set();
} else {
snake.sections.shift();
}
}
};
export default snake;
food.js represents food for snake and its spawning
import game from './game';
import snake from './snake';
let food = {
size: null,
x: null,
y: null,
color: '#0FF',
set: function () {
food.size = snake.size;
food.x = (Math.ceil(Math.random() * 10) * snake.size * 4) - snake.size / 2;
food.y = (Math.ceil(Math.random() * 10) * snake.size * 3) - snake.size / 2;
},
draw: function () {
game.drawBox(food.x, food.y, food.size, food.color);
}
};
export default food;
game.js is responsible for the game itself:
import snake from './snake';
import food from './food';
let canvas = document.getElementById("game_field");
let context = canvas.getContext("2d");
let game = {
score: 0,
fps: 8,
over: false,
message: null,
start: function () {
game.over = false;
game.message = null;
game.score = 0;
game.fps = 8;
snake.init();
food.set();
},
stop: function () {
game.over = true;
game.message = 'GAME OVER';
},
// draws a square box with leg length of size around (x, y) point on canvas
drawBox: function (x, y, size, color) {
context.fillStyle = color;
context.beginPath();
context.moveTo(x - (size / 2), y - (size / 2));
context.lineTo(x + (size / 2), y - (size / 2));
context.lineTo(x + (size / 2), y + (size / 2));
context.lineTo(x - (size / 2), y + (size / 2));
context.closePath();
context.fill();
},
// places score to notification area
drawScore: function () {
message_area.innerHTML = game.score;
},
// places message to notification area
drawMessage: function () {
if (game.message !== null) {
message_area.innerHTML = game.message;
}
},
// clears the whole canvas region
resetCanvas: function () {
context.clearRect(0, 0, canvas.width, canvas.height);
}
};
export default game;
main.js is an entry point
import game from './game';
import snake from './snake';
import food from './food';
window.onload = function () {
let inverseDirection = {
'up': 'down',
'left': 'right',
'right': 'left',
'down': 'up'
};
let keys = {
up: [38, 75, 87],
down: [40, 74, 83],
left: [37, 65, 72],
right: [39, 68, 76],
start_game: [13, 32],
getKey: function (value) {
for (let key in this) {
if (this[key] instanceof Array && this[key].indexOf(value) >= 0) {
return key;
}
}
return null;
}
};
window.addEventListener("keydown", function (e) {
let lastKey = keys.getKey(e.keyCode);
if (['up', 'down', 'left', 'right'].indexOf(lastKey) >= 0
&& lastKey != inverseDirection[snake.direction]) {
snake.direction = lastKey;
} else if (['start_game'].indexOf(lastKey) >= 0 && game.over) {
game.start();
}
}, false);
var requestAnimationFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame;
function loop() {
if (game.over == false) {
game.resetCanvas();
game.drawScore();
snake.move();
food.draw();
snake.draw();
game.drawMessage();
}
setTimeout(function () {
requestAnimationFrame(loop);
}, 1000 / game.fps);
};
requestAnimationFrame(loop);
};
As you can see, the code is not split in fact. I currently need to import all the other modules to every module. I also need to get canvas context in almost all of them. This looks lame. What decisions, ideas and patterns could help me to make this truly split scalable and modular and to get better understanding of these things? What changes would you suggest?