Beginner JavaScript Game Tutorial For Professional Use
I have seen many tutorials for creating HTML5 JavaScript games, and while most do a fine job introducing the base level technologies, most also introduce bad practices that would become big problems if you actually wanted to make a real game.
Writing a game engine from scratch is a good test for any developer, not unlike writing your own operating system. You have no existing framework on which to rely, so you learn a tremendous amount about things you took for granted, making you a better developer in the process. I have seen experienced developers learn all the necessary game technologies, cobble things together and fail because they ignored some basic programming principles.
I will show you in this tutorial how to develop the structure of a game engine, using good development practices, that will put you on the path to successful game development.
The Basics
I'm assuming a basic level of object oriented programming proficiency for this tutorial. I'm going to use an object oriented approach in JavaScript mimicking strict object oriented programming languages. There won't be any frameworks used, just raw JavaScript. You should take a look at my JavaScript Class Quick Reference to see the kind of class syntax for JavaScript I use.
As for a development environment, any web browser that has developer tools will work fine. I use Brackets for a text editor, but anything you are comfortable with will work.
HTML Canvas Setup
JavaScript games are possible because of the canvas HTML element, which allows JavaScript to draw to the screen. You are not restricted to just the canvas; you can still manipulate the DOM however you want. You can imagine a game UI consisting of a combination of standard HTML elements and a canvas (or multiple canvases) for the main game play area. For the sake of this tutorial, we will do all the drawing in one canvas for simplicity.
With that in mind, create a canvas in your HTML file and link to a JavaScript file where your program will reside.
<canvas id="game-layer" width="200" height="200"></canvas>
<script src="game.js"></script>
Or, if you like, you can just put the JavaScript within the script tag.
Drawing To The Canvas
To start with, let's just fill the canvas with a color. Drawing to a canvas is very simple. Get the canvas from the DOM, get a drawing context from the canvas and draw using the context. Notice that you can specify a 2D context which uses the 2D drawing commands or a 3D context which uses WebGL commands.
var canvas = document.getElementById("game-layer");
var context = canvas.getContext("2d");
context.fillStyle = "gray";
context.fillRect(0, 0, canvas.width, canvas.height);
As far as the graphics, the program we are going to create is just going to draw boxes on the canvas. It's a very simple example, but it is enough to illustrate the proper way to create a game engine.
First things first, let's draw a few more boxes.
var canvas = document.getElementById("game-layer");
var context = canvas.getContext("2d");
context.fillStyle = "gray";
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = "red";
context.fillRect(5, 5, 10, 15);
context.fillStyle = "blue";
context.fillRect(25, 25, 20, 20);
context.fillStyle = "green";
context.fillRect(50, 50, 20, 40);
That is the most basic aspect of drawing to a canvas and all we need for this tutorial. Now let's get into the actual game engine design. First I will go through the standard tutorial, mistakes and all, then I will show why this is wrong and how to correct it.
Game Engine Design (The Wrong Way)
Since this is a game, let's create Player and Enemy objects. The Player will be a larger blue box, and the enemies smaller red boxes. Each object will have a draw function to clean up the messy code above.
function Player(x, y) {
this.x = x;
this.y = y;
this.width = 20;
this.height = 20;
}
Player.prototype.draw = function () {
context.fillStyle = "blue";
context.fillRect(this.x, this.y, this.width, this.height);
};
function Enemy(x, y) {
this.x = x;
this.y = y;
this.width = 10;
this.height = 10;
}
Enemy.prototype.draw = function () {
context.fillStyle = "red";
context.fillRect(this.x, this.y, this.width, this.height);
};
var player = new Player(100, 175);
var enemy1 = new Enemy(20, 25);
var enemy2 = new Enemy(80, 25);
var enemy3 = new Enemy(160, 25);
context.fillStyle = "gray";
context.fillRect(0, 0, canvas.width, canvas.height);
player.draw();
enemy1.draw();
enemy2.draw();
enemy3.draw();
Now we have something resembling actual code and not just a canvas drawing sample. All games are driven by a loop that updates the state of the game world and the graphics every frame. We will use window.requestAnimationFrame()
to drive our game loop. requestAnimationFrame()
takes a callback function that we will call frameUpdate()
. Also add a function called update()
to each object and make it move the object in a direction.
var canvas = document.getElementById("game-layer");
var context = canvas.getContext("2d");
function Player(x, y) {
this.x = x;
this.y = y;
this.width = 20;
this.height = 20;
this.direction = -1;
}
Player.prototype.draw = function () {
context.fillStyle = "blue";
context.fillRect(this.x, this.y, this.width, this.height);
};
Player.prototype.update = function () {
this.y = this.y+this.direction;
if( this.y <= 0 || this.y+this.height >= canvas.height ) {
this.direction *= -1;
}
};
function Enemy(x, y) {
this.x = x;
this.y = y;
this.width = 10;
this.height = 10;
this.direction = 1;
}
Enemy.prototype.draw = function () {
context.fillStyle = "red";
context.fillRect(this.x, this.y, this.width, this.height);
};
Enemy.prototype.update = function () {
this.y = this.y+this.direction;
if( this.y <= 0 || this.y+this.height >= canvas.height ) {
this.direction *= -1;
}
};
var player = new Player(100, 175);
var enemy1 = new Enemy(20, 25);
var enemy2 = new Enemy(80, 25);
var enemy3 = new Enemy(160, 25);
function frameUpdate() {
canvas = document.getElementById("game-layer");
context = canvas.getContext("2d");
context.fillStyle = "gray";
context.fillRect(0, 0, canvas.width, canvas.height);
player.update();
player.draw();
enemy1.update();
enemy1.draw();
enemy2.update();
enemy2.draw();
enemy3.update();
enemy3.draw();
window.requestAnimationFrame(frameUpdate);
}
frameUpdate();
Analysis
If you have looked at other JavaScript game tutorials online, you should recognize everything above. It's a pretty standard way of introducing these concepts. However, notice what is wrong - we have combined storage of data, acting upon that data and display of that data all into one object. This is simply too much for one object to be doing and completely disregards the Model-View-Controller pattern.
For instance, try to follow the rendering code. It jumps between three different objects. A bug in one place will have drastic consequences somewhere else. As you add more objects, it only gets more complicated and harder to manage. This can be absolutely maddening to debug. It also greatly limits the ability to optimize the drawing code or do any number of special drawing effects that require information about other objects to work.
Perhaps less obvious is what is wrong with the update() function. It seems natural to update the object's position at that point, but consider handling object collision. Where would that be handled? Since each object has to move first to even know if there is a collision, it becomes necessary to split the movement code into one area to move the objects and another to resolve collisions. Talk about hard to find bugs.
The Fix
The solution is to organize related functions together into self contained objects. This is, in fact, the whole point of Object Oriented Programming.
I like to think about it like this: the game entities exist purely in the game world and are focused entirely on the game world logic. They describe where they are in the game world, what their properties are and what they are doing. The update() function will essentially be their AI.
A Renderer object is like a painter. It observes the game entities and paints the scene. Only the Renderer knows what the graphical representation is going to be, whether a 3D model or 2D sprite. The game entity itself doesn't know and doesn't care. The game entities will have publicly visible state information that the renderer will use to do the drawing.
A Physics object will handle all the motion. Think of applying gravity. The game entity isn't moving itself down to the ground, gravity is acting upon it pulling it towards the ground. When there is a collision, the physics dictate what happens to which objects. The game entities are notified of these events so they can update their behavior accordingly.
A Game object will pull all of this together. It will run the main game loop, handle things like scoring and game logic, loading levels, etc.
Splitting these functions apart now allows an amazing amount of flexibility and reusability in the future. If you want to change the game's graphics into 3D, the only section of code changed is the Renderer object. If you want more complicated physics, you are only changing the Physics object. If you want a different scoring system, you only change the Game object. This is the benefit of using a modular design and why it is crucial to object oriented programming.
Modular Implementation
// Game Entities
// Player Object
function Player(x, y) {
this.x = x;
this.y = y;
this.width = 20;
this.height = 20;
this.direction = -1;
}
Player.prototype.update = function () {
if( this.y <= 0 || this.y+this.height >= game.gameFieldHeight() ) {
this.direction *= -1;
}
};
// Enemy Object
function Enemy(x, y) {
this.x = x;
this.y = y;
this.width = 10;
this.height = 10;
this.direction = 1;
}
Enemy.prototype.update = function () {
if( this.y <= 0 || this.y+this.height >= game.gameFieldHeight() ) {
this.direction *= -1;
}
};
The Game Entity objects (Player and Enemy) are the same except they no longer move themselves. Notice that they are insulated from the canvas by asking the game object for the height of the game field.
As discussed above, we are going to create three new objects, the Renderer, Physics and Game objects. These will be singletons. Generally speaking, the Game Entities, Physics and Renderer should be largely reusable, with specific extensions and modifications, while the Game object will be very specific to each game you make.
// Renderer Object
var renderer = (function () {
function _drawEnemy(context, enemy) {
context.fillStyle = "red";
context.fillRect(enemy.x, enemy.y, enemy.width, enemy.height);
}
function _drawPlayer(context, player) {
context.fillStyle = "blue";
context.fillRect(player.x, player.y, player.width, player.height);
}
function _render() {
var canvas = document.getElementById("game-layer");
var context = canvas.getContext("2d");
context.fillStyle = "gray";
context.fillRect(0, 0, canvas.width, canvas.height);
var i,
entity,
entities = game.entities();
for (i=0; i < entities.length; i++) {
entity = entities[i];
if( entity instanceof Enemy ) {
_drawEnemy(context, entity);
}
else if( entity instanceof Player ) {
_drawPlayer(context, entity);
}
}
}
return {
render: _render
};
})();
Rendering is really quite simple. It gets the Game Entities from the Game object and draws based on the type of entity. This is obviously a very simple renderer. In a more complicated one, it would be loading textures and sprites, depth sorting, etc.
// Physics Object
var physics = (function () {
function _update() {
var i,
entities = game.entities();
for( i=0; i<entities.length; i++) {
entities[i].y += entities[i].direction;
}
}
return {
update: _update
};
})();
The Physics object operates in the same way. It gets the Game Entities from the Game object and applies the motion. Obviously this is also highly simplistic, but you will easily place real physics code in this spot.
// Game Object
var game = (function () {
var _gameFieldHeight = 200;
var _entities = [];
function _start() {
_entities.push(new Player(100, 175));
_entities.push(new Enemy(20, 25));
_entities.push(new Enemy(80, 25));
_entities.push(new Enemy(160, 25));
window.requestAnimationFrame(this.update.bind(this));
}
function _update() {
physics.update();
var i;
for( i=0; i<_entities.length; i++) {
_entities[i].update();
}
renderer.render();
window.requestAnimationFrame(this.update.bind(this));
}
return {
start: _start,
update: _update,
entities: function () { return _entities; },
gameFieldHeight: function () { return _gameFieldHeight; }
};
})();
game.start();
The Game object brings it all together. It is the owner of all the other objects and where the game loop is processed. In the update function you would add things like checking for the win and lose conditions and the logic unique to the particular game.
Conclusion
So there you have it, the most basic, yet correctly structured, game engine I could think of. I really hope you found this tutorial helpful.
To demonstrate an actual game using this framework, I wrote a tutorial series that creates a Space Invaders clone from scratch.
Space Invaders Tutorial Series | |
---|---|
Part 0 | Beginner JavaScript Game Tutorial For Professional Use |
Part 1 | Math Classes and Game Entity Structure |
Part 2 | User Input |
Part 3 | Enemy Behavior |
Part 4 | Collision Detection and Projectiles |
Part 5 | Sprites and User Interface |
Part 6 | Optimization |
Part 7 | 3D Renderer |
Part 8 | Events and Audio |
I have packaged the code for the full tutorial series for anyone interested in downloading it.