In the beginning
This is a recap of the first steps taken when I decided to make an RTS game. While I made numerous mistakes getting up and running, I can now explain the (best?) way to begin an HTML5 project.
A live demo of this is available here.
Step 1
Get a canvas going and get some JS objects to represent the game and the visual representation of the world. I add jQuery in but it’s hardly used even after one week’s coding. It’s like a safety blanket
<html>
<head>
<script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.5/jquery.min.js"></script>
<script type="text/javascript">
var map = {
};
var game = {
};
</script>
</head>
<body>
<canvas id="display"></canvas>
</body>
</html>
Step 2
Time for some game basics… A game is an interactive animation of sorts. While you could (as Ronco has done) use <DIV> tags to represent the map, the trees, the tanks – it’s going to be quite resource intensive having a DOM contain 100s of <DIV>s – Instead, I went with using <CANVAS> to render the scene… This means I need to worry about the internal game tick. Each tick / frame, the current frame needs clearing and any changes need to be rendered.
var game = {
ticker: {
timer: 0, // JS timer reference
interval: 40 // time in ms
},
init: function() {
this.ticker.timer = setInterval("game.run()", this.ticker.interval);
},
run: function() {
// main run method
},
end: function() {
clearInterval(this.ticker.timer);
},
};
If I call game.init() nothing will run every 40ms – perfect! game.end() stops the game running.
Step 3
Time to get something appearing on screen. I’m starting off with a blank map, this map – it’s just a large image using CnC’s own grass texture. It’s bigger than the canvas is going to be so that leads onto the next part – scrolling the map.
init: function() {
this.ticker.timer = setInterval("game.run()", this.ticker.interval);
// bind arrow keys
$(document).keydown(function(event) {
if (event.which == 37) {
event.preventDefault();
map.scroll(map.SCROLL_LEFT);
return;
}
if (event.which == 38) {
event.preventDefault();
map.scroll(map.SCROLL_UP);
return;
}
if (event.which == 39) {
event.preventDefault();
map.scroll(map.SCROLL_RIGHT);
return;
}
if (event.which == 40) {
event.preventDefault();
map.scroll(map.SCROLL_DOWN);
return;
}
});
}
This binds the arrow keys. Using some constants to define the direction of scroll and jQuery – when an arrow key is pushed, I call the scroll() function:
Before writing the scroll function, the viewport needs defining. This will be a small object inside the map object that holds information about which part of the map we’re looking at. Essentially the canvas is the viewport – but it never moves. So the viewport abstracts the scroll values. This is the viewport:
viewport : {
x : 0,
y : 0,
scroll_amount: 12
}
Another object I created was the dimensions container. This deals with the dimensions of the entire map:
dimensions: {
w: 983,
h: 700
}
And now the scroll() function:
scroll: function(dir) {
if (dir == this.SCROLL_LEFT) {
if (this.viewport.x == 0) {
return;
}
this.viewport.x -= this.viewport.scroll_amount;
}
else if (dir == this.SCROLL_UP) {
if (this.viewport.y == 0) {
return;
}
this.viewport.y -= this.viewport.scroll_amount;
}
else if (dir == this.SCROLL_RIGHT) {
if (Math.abs(this.viewport.x) + this.viewport.w >= this.dimensions.w) {
return;
}
this.viewport.x += this.viewport.scroll_amount;
}
else {
if (Math.abs(this.viewport.y) + this.viewport.h >= this.dimensions.h) {
return;
}
this.viewport.y += this.viewport.scroll_amount;
}
this.requires_redraw = true;
}
}
The last bit is to actually draw stuff. This means a draw() function. But since our scene could potentially be rendered every 40ms – this could result in a lot of extra work. To combat this – there is a boolen toggle requires_redraw – when this is true, the draw() function will draw – when it’s false it won’t. Simples!
draw: function() {
if (!this.requires_redraw) {
return;
}
// clear canvas
this.canvas.width = this.canvas.width;
this.context.drawImage(this.tile, this.viewport.x, this.viewport.y, this.viewport.w, this.viewport.h, 0, 0, this.viewport.w, this.viewport.h);
this.requires_redraw = false;
}
The entire thing is available here