Making an Indie Desktop Game, Part I
Part I of an unknown number of insights I glean while working on my own indie Mac desktop simulation game.
A few weeks ago I decided I wanted to make a game. I’ve been a professional programmer for the past 8 years, and have played games all my life, so it seemed natural to me that I should try and make my own.
I wanted a game that was open ended, and could be played at my own pace. I’ve been playing a lot of RogueLikes (NetHack, Dwarf Fortress) and wanted to make something in that spirit, but without the cognitive overhead of interpreting ASCII symbols and memorizing keyboard commands.
This blog will focus on my experience over the next few years while researching and building my own game, from the ground up.
The Game
I really like Dwarf Fortress. Enough that I put up with the not-so-great Mac support and use some third party apps to help me manage the immense complexity that is Dwarf Fortress. I like that there’s really no game in that game. You’re just building, trying not to die. You just have a map and some little Happy Faces. I just never get good enough at managing that fucking game.
So I figured someone has to have made a clone that was a bit more simple. Turns out a lot of folks have, but most of them are Windows only. I’ve been a Mac user since I was a kid, and don’t like having to boot to a new OS just to play games. So, what the hell, I figured, I’m a decently smart programmer. I could figure this out.
I didn’t want to totally rip Dwarf Fortress, so I decided my game would revolve around modern day humans trying to survive after an apocalypse. Maybe the zombie apocalypse. I haven’t really decided yet. But I knew enough to get started.
Starting Point
So I knew what I was making, and that I wanted the platform to be Mac. I decided that, because this was a learning experience, I wouldn’t use any game engines like Unity. I figured I’d just use the Cocoa API’s from Apple and hack together something I could enjoy.
So, I fired up XCode and got to work. Apple had just announced their latest OS, Mavericks, and their sweet new set of APIs, which include SpriteKit. That seemed like a good starting place for me. I read through their SpriteKit tutorial (which I’m not allowed to talk about) and got an idea for how Objective-C and Cocoa work, and how their SpriteKit engine works. After running through their sample app, I figured I was ready to start building mine.
Mapping
I figured I couldn’t do much until I had a map. The internet pointed me to an app called Tiled. Tiled lets you create maps and tilesets to export into XML. There are quite a few different libraries to handle the .tmx file format (which is really just XML) in Objective-C, but I figured it couldn’t be too hard to roll my own. Holy shit was I wrong.
Eventually I want the game to use dynamic maps, generating a new one for every game. And I want the game to have fairly massive maps, so that you have plenty of room to build your survivor camp into a massive fortress. But in the interest of learning and exploration, I started with a simple 32x24 tile map, with tiles at 32x32 (this and 16x16 seem to be standard tile-sizes in the gaming world). I found a link to OpenGameArt.com, downloaded some free art and loaded it into Tiled. I made a map made of mostly grass and a few walls and exported it.
After skimming the XML that Tiled output, I hit my first road block almost immediately. SpriteKit uses a coordinate system with {0, 0} being in the lower left corner of the screen. Tiled uses a linear system. It tells you the dimensions of your map, and then lists each tile starting with the top left at index zero, and continuing until you’re out of tiles (in my case to tile 767). I hadn’t even started trying to parse the XML yet.
Data Structures
Previous blogs I’ve read about making RogueLikes suggest storing your map in a two-dimensional array, which makes accessing any single point fairly straight-forward, just do:
(Tile *) getTileForX: (NSInteger) x y: (NSInteger) y
{
return self.map[x][y];
}
assuming map is the name of your array. The major pro here was access; the con was the difficulty in creating the nested array structure from the Tiled data.
But the Tiled XML seemed to suggest another structure, just an array with a single dimension. Insertion would be easy; just parse the XML file and insert tiles as you come across them. Retrieval becomes more difficult though:
(Tile *) getTileForX: (NSInteger) x y: (NSInteger) y
{
int index = (y * self.mapNumTilesAcross) + x;
return self.map[index];
}
That’s really not so bad. For better or worse, that’s the format I chose.
A Point is a Point… Right?
So, now I’ve stored my data. Or at least figured out how I want to store it. The next challenge is getting all those tiles to appear on the screen in the right order, and in the right spot.
The biggest pain is translating an index in the XML file to a point on the screen. Like I mentioned earlier, Tiled gives me the tiles on the map as a big long list. Tile 0 should show up in the top left of the screen. So how do I know where the top left is? I could just construct that point like this:
CGPoint tilePosition = CGPointMake(0, self.window.size.height);
But that doesn’t help me figure out where tile number 456 should go. But really, what that construction is saying is;
Take the current tile index, and move the index * the width of a single tile to the right. Then take the index % the number of tiles in a column (Tiled gives you the number of tiles in a column directly, but if it didn’t you could figure it out by doing full map width / tile width), and multiply that by a single tile’s height. That will give you the “offset” from the top, so you can figure out the y component by taking your maps height and subtracting the offset.
- (CGPoint) pointForNodeNumber:(NSInteger)number
{
NSInteger row = (floor(number / self.size.width));
NSInteger column = (number % (int)self.size.width); CGFloat x =
column * self.tileSize.width + (self.tileSize.width / 2);
CGFloat y =
self.pixelSize.height —
(row * self.tileSize.height + (self.tileSize.height / 2));
return CGPointMake(x, y);
}
You might notice I add an offset of 1/2 the tile’s width or height; that’s because SpriteKit places Sprites based on their center point.
Reading the XML
Cocoa comes with an XML parsing library. It works with using a delegate. The file is parsed, and as the parser passes certain “milestones” (ie, elementDidBegin or elementDidEnd), it hands off data to the delegate. The clear advantage here is the memory savings; the file doesn’t have to all be stored in memory all at once.
But because I come from a Ruby background, I chose instead to use a third party library, TouchXML, that acts like libraries I’m more familiar with. It loads the entire parsed file into memory, and allows you to query it:
NSString *path =
[[NSBundle mainBundle] pathForResource:fileName ofType:@”tmx”];NSData *data = [NSData dataWithContentsOfFile: path];CXMLDocument *doc =
[[CXMLDocument alloc] initWithData: data options:0 error:nil];NSArray *nodes = NULL;
nodes = [doc nodesForXPath:@"//map" error:nil];
I plan on removing this added library and switching to the Cocoa XML parser eventually, I just haven’t gotten around to it.
Tiles
The last thing I’ll talk about in this post is rendering individual tiles with SpriteKit. In SpriteKit, Apple has tried to abstract away a lot of the mess of rendering images and sprites. So I tried to use their classes as much as possible. I made a class, HSTile that inherits from SKSpriteNode.
SKSpriteNode includes support for loading a single image that contains all of your tiles, and then telling it to only render part of that image at a time. Apple calls this “textures”.
Using this API was pretty easy. First you define your texture, then you initialize the node with that texture, then you tell it what the frame for the texture is.
The “frame” part is weird. Once a texture is applied, the API starts treating the texture as if it were two pixels tall, and one pixel high, so you have a grid with {0,0} in the center, {-1, 1} in the top left and {1, 1} in the bottom right. I wrote a helper method to figure this out:
- (CGRect) rectFromGid:(NSInteger)gid
{
CGSize size = self.textureSize;
CGFloat width = self.tileSize.width / size.width; CGFloat height = self.tileSize.height / size.height;
NSInteger tilesAcross = size.width / self.tileSize.width;
NSInteger row = floor(gid / tilesAcross) + 1;
NSInteger column = (gid % tilesAcross) — 1; CGFloat x = (column * self.tileSize.width) / size.width;
CGFloat y =
(size.height — (row * self.tileSize.height)) / size.height; return CGRectMake(x, y, width, height);
}
Just a note, “gid” is what Tiled calls the “position” in the image map. It’s a linear sequence… so the first tile is the tile in the top left, and the last tile is the tile in the bottom right.
Next Time
I’ve already done quite a bit of work since I implemented the mapping system. It’s not that great, but it’s working for now. I’ll need to refactor it quite a bit.
Next time I’ll talk about Pathfinding and Job Queueing, and all the joys that come from those two =D
So where does this leave me? Well… I don’t have anything that even remotely resembles a game yet. If I had to guess, I’d say I’m still about a year or more away from having anything playable. But so far, I’m happy with what I’ve got.