We’re getting there. 18 hours in and I have the first hints of what might actually be a game…
(I’ll include a demo at the bottom of the post)
Basically, I went with allowing each box to be controlled individually by the keyboard. There will be some problems with having that many people on the keyboard at once, but we’ll deal with that later (if we can).
One additional thing I wanted (and mostly figured out) is somewhat ’loose’ controls. Basically, rather than explicitly dealing with moving only when a key is down, we’ll accelerate when the key is down, preserving velocity even after keys are raised. There will be some small amount of friction as well, to make sure that eventually pieces will slow down.
It’s actually not too hard to implement a system like this:
First, load in the keybindings from the interface you can see in the screenshot above:
// Load key bindings
var loadKeyBindings = function() {
keys = {};
console.log('Loading key bindings...');
$('#controls table').each(function(i, eli) {
var player = parseInt($(eli).attr('data-player'));
console.log('loading controls for player ' + player);
$(eli).find('input').each(function(j, elj) {
var command = $(elj).attr('name');
var key = $(elj).val();
keys[key] = [player, command, false];
});
});
};
This will put everything into the key
array, indexed by key name and storing the player it refers to, the direction you are going (one of left
, right
, up
, or down
), and if that key is currently active (pressed down). If I can, I may add additional key bindings (such as rotation or powerups), otherwise, that’s pretty good for the moment.
Next, we’ll add a function to tell when keys are active:
var onkey = function(event) {
switch (event.keyCode) {
case 37: key = 'LEFT'; break;
case 38: key = 'UP'; break;
case 39: key = 'RIGHT'; break;
case 40: key = 'DOWN'; break;
case 97: key = 'NUM1'; break;
case 98: key = 'NUM2'; break;
case 99: key = 'NUM3'; break;
case 100: key = 'NUM4'; break;
case 101: key = 'NUM5'; break;
case 102: key = 'NUM6'; break;
case 103: key = 'NUM7'; break;
case 104: key = 'NUM8'; break;
case 105: key = 'NUM9'; break;
default: key = String.fromCharCode(event.keyCode).toUpperCase();
}
if (key in keys) {
if (event.type == 'keydown') {
keys[key][2] = true;
} else if (event.type == 'keyup') {
keys[key][2] = false;
}
}
};
Longer than I wanted, but it correctly deals with both the numpad and arrow keys, which is kind of necessary if you want to support 4 human players all at the same time. Perhaps I’ll implement AIs, but until I do, we’re going to have to allow for a bunch of players…
Okay, so what do we do with all of this information? Well, just like before, we have a tick
function:
var tick = function(event) {
$.each(keys, function(i, el) {
var player = el[0];
var command = el[1];
var active = el[2];
$game = $('#tiles');
$tile = $('#tiles *[data-player="' + player + '"]');
// Update velocity
...
// Use friction to slow each box down over time
...
// Cap velocity so we don't go too fast
...
// Update the current position based on velocity
...
// Bounce off the edges of the screen
...
// Finally, update the position
$tile.css({'top': top, 'left': left});
});
if (running) {
setTimeout(tick, 1000/30);
}
};
Oof. That’s a heck of a function. Luckily, the individual parts aren’t that bad. First, we want to update the velocity. This is where the active
parameter (the third in each key definition) comes into play:
// Update velocity
if (active) {
if (command == 'up') {
vel[player][1] -= PER_TICK_ACCELERATION;
} else if (command == 'down') {
vel[player][1] += PER_TICK_ACCELERATION;
} else if (command == 'left') {
vel[player][0] -= PER_TICK_ACCELERATION;
} else if (command == 'right') {
vel[player][0] += PER_TICK_ACCELERATION;
}
}
That’s simple enough. As before, we have to decide that up
and down
are inverted (they almost always are when it comes to computers), but once you’ve decided that’s easy enough.
Now, outside of that black, the next thing we’ll do is apply friction. This way the boxes will slow down over time, forcing players both to pay attention and to let them bounce around like madmen.
// Use friction to slow each box down over time
// If we're close enough to zero that friction will accelerate us, just stop
if (Math.abs(vel[player][0]) < PER_TICK_FRICTION) {
vel[player][0] = 0;
} else {
vel[player][0] += (vel[player][0] > 0 ? -PER_TICK_FRICTION : PER_TICK_FRICTION);
}
if (Math.abs(vel[player][1]) < PER_TICK_FRICTION) {
vel[player][1] = 0;
} else {
vel[player][1] += (vel[player][1] > 0 ? -PER_TICK_FRICTION : PER_TICK_FRICTION);
}
// Cap velcity so we don't go too fast
vel[player][0] = Math.min(VELOCITY_CAP, Math.max(-VELOCITY_CAP, vel[player][0]));
vel[player][1] = Math.min(VELOCITY_CAP, Math.max(-VELOCITY_CAP, vel[player][1]));
Also at the end there, we make sure we don’t keep accelerating indefinitely. That both helps keep the game a little easier to play and prevents edge cases (such as moving further in one tick than we’re allowed).
Next, we can finally update the position:
// Update the current position based on velocity
var left = $tile[0].offsetLeft + vel[player][0];
var top = $tile[0].offsetTop + vel[player][1];
// Bounce off the edges of the screen
if (left < 0) {
left = 0;
vel[player][0] = Math.abs(vel[player][0]);
} else if (left > $game.width() - $tile.width()) {
left = $game.width() - $tile.width();
vel[player][0] = -1 * Math.abs(vel[player][0]);
}
if (top < 0) {
top = 0;
vel[player][1] = Math.abs(vel[player][1]);
} else if (top > $game.height() - $tile.height()) {
top = $game.height() - $tile.height();
vel[player][1] = -1 * Math.abs(vel[player][1]);
}
Once again, we want to clip the positions. This time though, we’re actually going to use the velocities we have rather than zeroing them out. Instead: bounce! It’s nice, because it makes the game feel more ‘realistic’ (for some definitions of the word).
And that’s about it. With that, we can have the boxes moving around and interacting as they did last night. We’re actually starting to get a game going here. One other tweak is the control code:
var tiles = new Tiles();
var controls = new Controls();
var MS_PER_GAME = 60 * 1000;
var startTime = new Date().getTime();
var running = true;
$(function() {
controls.run();
});
function tick() {
var soFar = new Date().getTime() - startTime;
var remainingSec = Math.floor((MS_PER_GAME - soFar) / 1000);
if (remainingSec > 0) {
$('#tiles #countdown').text(remainingSec + ' sec remaining');
} else {
stop();
}
if (running) {
setTimeout(tick, 1000/30);
}
}
function run() {
tiles.run();
controls.run();
startTime = new Date().getTime();
running = true;
tick();
return false;
}
function stop() {
tiles.stop();
controls.stop();
startTime = new Date().getTime() - MS_PER_GAME;
running = false;
$('#tiles #countdown').text('game over');
return false;
}
Technically, it’s not a gameloop, since everything is done asynchronously via setTimeout
(and make absolutely sure that you don’t use setInterval
…), but it’s close enough. What this does give us though is a strict time before the game ends. Otherwise, the boxes will eventually fill up, and where’s the fun in that? (Although that might be an interesting alternative end condition).
After that, all I have to figure out is scoring. And I have another 6 hours until the one day mark. If I can make it by then, I’ll feel pretty good–and can use all of the rest of the time for polish. I’m thinking some simple music, sound effects, a title screen (initial letters in sand?). Of course, I still have to figure out the scoring algorithm..
Same as yesterday, the entire source (warning: still ugly) if available on GitHub: jpverkamp/sandbox-battle
Demo:
Player 1 - Blue | |
---|---|
Up | |
Left | |
Right | |
Down |
Player 2 - Red | |
---|---|
Up | |
Left | |
Right | |
Down |
Player 3 - Green | |
---|---|
Up | |
Left | |
Right | |
Down |
Player 4 - Pink | |
---|---|
Up | |
Left | |
Right | |
Down |
I’m sure there are bugs… And I’m working on it right at the moment. If you have any questions or comments though, feel free to drop me a line below.