Modern platforms like unity have a lot of built-in editors and tooling that manage a lot of the pipeline for you, and the more powerful target platforms (ie your PC) can afford to work with a lot more options of data formats. But on old systems like the NES, it can be a lot of work getting your assets into a format that is usable and efficient.
For example, most Atari games store their sprite graphics upside-down in ROM. This lets you save a few cycles in your rendering kernel (because on the 6502 counting down to 0 is faster than counting up to an arbitrary number, so you count backwards when rendering most things). You can manually enter your sprite graphics backwards by hand, or you can draw your graphics in a user-friendly tool, and let it take care of turning them upside-down.
For the NES, there's a huge range of ways that people manage the asset pipeline. I know folks who design things on paper, then manually enter hex data into their editor. I know a guy who built an editor entirely as a NES game itself so that you can just press save in the editor, and everything is ready in a NES-friendly format.
My philosophy is this: I want all my assets to be saved in the most easy-to-edit and user-friendly format. That way I can work with them easily during development. Then I want the build process to automatically convert them all into the right format for the game when I compile. I specifically DON'T want to deal with a separate export step during editing -- that usually ends up with a situation where you forget to save or export something, and your exported version differs from the original asset, and you aren't sure which is correct. Instead, I want the original asset to always be the authoritative version. So any conversion or export steps must be automatically done at build time.
So, let's get into examples. Text data is probably the simplest pipeline. I have a number of dialog boxes in the game. I want to be able to write my dialog text in any text editor, and save it as a text file that's tracked in git. So I have a separate text file for each dialog script.
|One text file for each dialog
At build time, my makefile runs a python script to convert each of these into the format that my game wants: It converts each character to the value of the graphic tile id that displays that character. It converts special control characters (like line-breaks, special non-ascii glyphs like button icons, etc) to the correct codes. Then it generates symbol tables of both the address of that script, and the text length of the script. So if I want to change any text, I only have to change the text file and recompile, and I'm good to go.
Now, let's get into the more substantial example: backgrounds, metatiles, and map data.
NES background graphics are made of 8x8 pixel tiles of 3 colors plus a solid background color. Different palettes can be applied to those 3 tiles (with all sorts of rules that I won't get into here), but at the core, NES graphics can be represented by a 4-color image. Frankengraphics (the artist I'm working with) likes to use a program called NES Screen Tool for making the graphics. Screen Tool generally saves data in a NES-native graphics format, which most people really like. This goes against my core philosophy though, as it's a hard-to-use format. You can't see a preview of the graphics in your operating system's window manager, or pop open any old graphics tool and make quick changes. It doesn't help that Screen Tool is windows-only, and feels REALLY buggy and janky running in WINE on Linux. So when I get graphics from her, I immediately load them into Screen Tool once, and export them to 4-color indexed png format. During the build process, I have a python script that goes back and converts the png format to the NES-native graphic format (as well as generates address symbols based on the file name) This has the added benefit of making it easier to have arbitrarily-sized chunks of graphic data that I can refer to directly.
|NES Screen Tool. A lot of people swear by this thing.
Then, Halcyon makes maps out of 32x32 pixel "metatiles" -- logical map tiles built out of 16 graphic tiles. So I need a way to define those. Since there aren't standard ways of representing metatiles (and since each engine has its own requirements for what data is bundled with metatiles), I decided to use a format that's both human readable and easy to program against: json. I built a simple (ugly) graphical editor for building metatiles out of graphics tiles. It edits json files, so standard tools (text diffs, git merges, etc) can easily work with the output of my metatile editor. In my engine, metatiles contain 16 graphical tiles, as well as collision and palette data for the 4 quadrants of the metatile. These are also saved with the json file. At build time (do you see the pattern here?) a python script converts these json files to striped arrays of binary data (and will also put them into various rom banks to ensure that I don't overflow the amount of space I have in any given bank). The other resulting file is a temporary graphical file that re-assembles the actual graphical tiles into the metatiles, which I use for designing maps. This graphics file is never used in the final build -- it's only a convenience to make map design easier.
|Speaking of janky, my metatile editor is JANK-O-RIFFIC. But it gets the job done.
So now, on to maps. While there's not one single standard file format or tool for maps, there is a well-known open source program that I'm a big fan of: Tiled. Tiled is a general purpose tile-map editor. It's fairly flexible and configurable, and the author is a really friendly guy who's very invested in making it a well-run open source project. Tiled saves and loads maps in a simple easy-to-read text format (either based on xml or json), which is perfect for my philosophy of assets.
So I load a tileset graphics file into Tiled and build my maps, saving them in Tiled's native format. At build time, a python script processes all of my map files, and converts them all to the right format for Halcyon. (which is a big array of properties about each map, with pointers to arrays of the actual map data, and arrays of enemy spawn information). Then the script builds a giant index of all my maps, and makes a big lookup table that lets me find the address of a room based on its X,Y coordinates.
|Tiled is the best thing ever.
Now that I've talked about HOW I do the asset pipeline for levels, it's worth giving a few more reasons for WHY I do it this way. I mentioned a few above, but I like to repeat myself, so:
- Editing is MUCH easier when you use text formats as much as possible, and
standard well-known formats for non-text
- Version control (ie git) is a lot more useful with text files, as you can
easily see a diff of what changed
- Building from the original source asset at compile-time prevents the "out of
sync" problems you can get with manual exports
- Using a script at build time lets you change the engine's expected format
without having to touch each of your assets again. For example, at one point I made an optimization in my sprite rendering that required sprites to be stored offset by 32 pixels. To accommodate this, I only had to change my build script without touching each sprite definition. Or if I go back and decide to add compression on graphics, I can do it without having to manually handle each graphics file.
- Using a script lets you automatically build secondary derived assets from
your assets. For example, I automatically generate a minimap based on my actual maps without having to manually build a minimap to match the actual map. I also automatically build some reference notes for myself -- a text file that includes notes about the location of every powerup and the id's that I've assigned to them. I love not having to manage that notes file by hand.
I could talk about all the other types of assets (enemy definitions -- which I've blogged about previously, music, palettes, etc), but I'd start to sound pretty redundant I think. I just do my best to folow the guiding philosophy: store in the most editor-friendly format, and convert everything at build time. Manual exports are NOT allowed.