ARCHIVE FILE
The Making of Zombietron 2048
The 31st Ludum Dare was held this weekend. Ludum Dare is a 48 hour game competition with a few simple rules: make a game from scratch within 48 hours after the official theme has been announced. The theme for this round was "Entire game in one screen".
I interpreted that as making a game with a classic arcade game feel, and here's my entry: Zombietron 2084. If you play it, please rate it at the Ludum Dare voting page.
The game is based on my favorite arcade game from the 80s, Robotron 2084. My aim was to make a game that played like Robotron, but with a Zombie theme.
Zombies are cool.
Sprites
I started out by making a few sprites. Here are the main sprites:
[image omitted from archive]
You'll probably notice that spacing is different. This is down to my intial decision of going with 16x16 sprites and then discovering I was using only 8 pixels for the width. I removed the extra space in the last two, but due to time pressure I didn't do that with the first two sets.
A look at the code
Zombietron 2084 is written in JavaScript. The code is open source and available on github with a permissive ISC license. The code is slightly cleaned up for this blog entry, but there is still a few of loose ends in there. This can be a bit confusing, and I'm sorry. I'm going to clean it up in the develop branch and merge it in with the master as soon as the voting period ends.
I'm using ImpactJS for the game engine, and while that is open source as well, it requires a license to download, so it's not included in the repo. The minified version in the public folder includes ImpactJS, so you're able to play the game without picking up ImpactJS.
I'm also using PlusPlus by collinhover. This is an extension of ImpactJS that I've been meaning to check out for over a year, but I've found it hard to integrate with existing games. Starting fresh with a game like Zombietron proved to be a great opportunity to finally get started with PlusPlus as well.
The advantage of using PlusPlus is that it comes bundled with a lot of sensible plugins and predefinitions, allowing you to get started making your game with less boilerplate code.
The main loop
ImpactJS loads a game as a named module with a list of files it needs. This is lib/game/main.js. It pulls in PlusPlus, the player entity, a UI overlay, the title screen, six levels, and a level-change entity used as a door into the next map.
// setup the main module
ig.module(
'game.main'
)
.requires(
'plusplus.core.plusplus',
'game.entities.player',
'plusplus.ui.ui-overlay',
'game.levels.title',
'game.levels.level1',
'game.levels.level2',
'game.levels.level3',
'game.levels.level4',
'game.levels.level5',
'game.levels.level6'
,'plusplus.entities.levelchange'
)
The game class extends PlusPlus's GameExtended. The fields are the HUD font, three timers (intro, level transition, restart), a flag for whether you are dead, and a tiny save of level and score. shapesPasses is PlusPlus lighting: use the collision map as shadow casters, ignore climbable tiles, drop the inner boundary.
.defines(function() {
"use strict";
var _c = ig.CONFIG;
var game = ig.GameExtended.extend({
backgroundMap: null,
clearcolor: '#ffffff',
scorefont: new ig.Font(_c.PATH_TO_MEDIA + 'fonts/kenpixel.futuresquare.8px.png'),
startTimer: new ig.Timer(2),
transitionTimer: new ig.Timer(2),
restartTimer: new ig.Timer(4),
tiles: new ig.Image('media/tiles.png'),
playerDead: false,
gameData: {
gameStarted: false,
level: 0,
points: 0
},
shapesPasses: {
lighting: {
ignoreClimbable: true, discardBoundaryInner: true
}
},
init does not show a menu. It loads the title as a real level, so the first screen already teaches you how to move. inputStart rebinds the arrows to shooting and R to restart. PlusPlus already uses those keys for walking; this overrides them. Walking stays on WASD, which PlusPlus predefines.
init: function () {
this.parent();
this.loadLevel(ig.global.LevelTitle);
},
inputStart: function () {
this.parent();
ig.input.bind(ig.KEY.UP_ARROW, 'shoot-up');
ig.input.bind(ig.KEY.DOWN_ARROW, 'shoot-down');
ig.input.bind(ig.KEY.LEFT_ARROW, 'shoot-left');
ig.input.bind(ig.KEY.RIGHT_ARROW, 'shoot-right');
ig.input.bind(ig.KEY.R, 'restart');
},
update is the 48-hour part. R resets death and jumps to level 1. If getPlayer() is missing, you are dead and a black overlay fades in. If you are alive and the intro timer has elapsed, the loop counts zombies. One zombie left means wait. Zero zombies means spawn EntityLevelchange on the player, named LevelLevelN. After level 6 it wraps to 4 — a jam hack so the game never runs out of maps.
update: function () {
this.parent();
if (ig.input.pressed('restart')) {
this.playerDead = false;
Zombotron.gameData.level = 1;
ig.game.loadLevel(LevelLevel1);
}
var startNewGame = false;
this.playerDead = "undefined" == typeof ig.game.getPlayer();
if (!this.playerDead) {
if ("undefined" == typeof ig.game.getPlayer()) {
this.playerDead = true;
var dimmer = ig.game.spawnEntity(ig.UIOverlay, 0, 0, {
layerName: 'ui', fillStyle: '#000000',
alpha: 0
});
var overlay = new ig.UIOverlay();
dimmer.fadeTo(0.75);
}
if (ig.game.startTimer.delta() > 0) {
for (var i = 0; i < ig.game.entities.length; i++) {
if (ig.game.entities[i].name == "zombie") {
ig.game.transitionTimer.set(0.2);
return;
}
startNewGame = true;
}
if (startNewGame) {
if (ig.game.transitionTimer.delta() > 0) {
var player = ig.game.getPlayer();
Zombotron.gameData.level = Zombotron.gameData.level + 1;
if (Zombotron.gameData.level > 6) Zombotron.gameData.level = 4; // hack
ig.game.transitionTimer.set(2000);
console.log("LevelLevel" + Zombotron.gameData.level);
ig.game.spawnEntity(EntityLevelchange, player.pos.x,
player.pos.y, {
"size": { "x": 16, "y": 32 },
"levelName": "LevelLevel" + Zombotron.gameData.level,
"playerSpawnerName": "roderick"
});
}
}
}
}
},
draw is the HUD I should have done with PlusPlus UI and did not, because I could not get it working in time: title, score, game over, press R. inputEnd undoes the arrow bindings. ig.main starts the canvas at 60 fps from the config file.
draw: function () {
this.parent();
this.scorefont.draw("ZOMBIETRON 2084 - SCORE: " + ig.game.gameData.points,
ig.system.width / 2, 10, ig.Font.ALIGN.CENTER);
if (this.playerDead) {
this.scorefont.draw("Game Over", ig.system.width / 2,
ig.system.height / 2 - 10, ig.Font.ALIGN.CENTER);
this.scorefont.draw("Press R to restart", ig.system.width / 2,
ig.system.height / 2 + 10, ig.Font.ALIGN.CENTER);
}
},
inputEnd: function () {
this.parent();
ig.input.unbind(ig.KEY.UP_ARROW, 'shoot-up');
ig.input.unbind(ig.KEY.DOWN_ARROW, 'shoot-down');
ig.input.unbind(ig.KEY.LEFT_ARROW, 'shoot-left');
ig.input.unbind(ig.KEY.RIGHT_ARROW, 'shoot-right');
ig.input.unbind(ig.KEY.R, 'restart');
}
});
ig.main('#canvas', game, 60, _c.GAME_WIDTH, _c.GAME_HEIGHT, _c.SCALE,
ig.LoaderExtended);
});
The entities
I've defined a few entities in lib/game/entities. Some of them are boilerplate from the PlusPlus repo and are not used in the game. The ones I'm using are: player, zombie, big-zombie, people, torch and laser. The entity files share a few common features, such as defining a sprite sheet, setting the animation for various stages of movement and a initial property setting for adding stuff like glowing and shooting.
Since this is the player, AKA you, I need to add logic to handle the input. These are defined in the handleInput function. Usually you want to shoot where you're facing, but I wanted to emulate the Robotron system where you're free to shoot in one direction while walking in another, so I made a shoot-left, shoot-right, shoot-up and shoot-down method.
An interesting property that the player.js file doesn't have, but the enemies and people have are the initTypes function. These four lines do a lot. Coupled with canWanderX they turn on A* so a zombie patrols until it is close enough to run at you. DAMAGEABLE means it can die. preyGroup FRIEND means it hunts the civilians in people.js. group ENEMY means zombies do not hurt each other. checkAgainst FRIEND means they collide with and damage anyone not in that group.
// zombies seeks friend group
_ut.addType(ig.EntityExtended, this, 'preyGroup', "FRIEND", "GROUP");
// zombies can be damaged
_ut.addType(ig.EntityExtended, this, 'type', "DAMAGEABLE");
//_ut.addType(ig.EntityExtended, this, 'checkAgainst', "PLAYER");
// zombies are in enemy group and will not collide with or hurt each other
_ut.addType(ig.EntityExtended, this, 'group', "ENEMY", "GROUP");
// zombies will collide and hurt any character not in their group
_ut.addType(ig.EntityExtended, this, 'checkAgainst', "FRIEND");
Further development
Zombietron is a barebones game at this point. It has 6 levels, and it's so hard you probably won't make it to the last level. Here are the things I'd like to do if I wanted to spruce it up:
- Polish the graphics and add background graphics
- Better animation when chasing the player
- More levels/procedurally generated
- Add sound
- Implement 8-directional shooting
Though seeing as I'm actually developing a RPG at the moment, I'm not sure I can spare the time. Learning how to use PlusPlus was intriguing, so I'm thinking about resetting the development of my RPG and starting fresh with that in the core.