Just in time for the holidays, here are final versions of Chuckie Egg for DS and PSP, and an almost-final version for the Dreamcast:
The PSP has a huge screen. Well, relative to the DS; maybe not so huge if you compare it to a modern phone. What do you do with all that space if you’re remaking a game that fits in 256x192 pixels? The obvious thing to do is double up the pixels and make the graphics twice the size. But wait; at 272 pixels tall the PSP’s screen is only 70% larger vertically than the Dragon 32, which means we can’t double up the resolution without losing 30% of the game’s display. I imagine it’s possible to wrap the game’s bitmap onto a quad and get the GPU to stretch it, but I eventually decided just to centre the game at its native resolution.
The PSP’s screen is 480 pixels wide but its default framebuffer is 512 pixels wide. Trying to change the width of the framebuffer to match the screen does not work (it results in the display being smeared horizontally, which always means “the dimensions of your bitmap do not match the layout of memory”). I had to add in the concept of “usable” vs “available” framebuffer space (“physical” vs “logical”, I suppose) and base the layout of the various screens on that.
It’s possible to push the PSP’s “home” button and quit from a game back to the launcher. Making this work entails creating a background thread and wiring up a callback that the OS can use to signal to the game that it needs to quit. It’s very simplistic; if the game doesn’t implement the callback then the system just hangs forever. The OS doesn’t appear to forcibly quit rogue processes. I’m not even sure that it’s really a multi-process system. I imagine Sony handled errors like this in their QA process so they didn’t bother with anything more elaborate to catch badly-implemented games.
Different geographic regions have different PSP button layouts. I don’t quite know what Sony thought the advantage of swapping the O and X buttons was, but it means well-behaved homebrew has to work around it. Games typically either allow the buttons to be remapped or come in different builds for different layouts. Official games were region-locked so that wasn’t an issue; it was probably fixed during localization. I’m too lazy to deal with it.
Getting sound working was troublesome. The basic PSPSDK libraries offer a way - I think - to craft PCM data and play it back, which really doesn’t help if you just want to play a WAV and not put any thought into it. I ended up using libmikmod, which has some peculiarities. Its
MikMod_Update() function must be called regularly in order for sounds to work, but that can be stuffed into another background thread and forgotten about. It offers two ways to play back samples:
Sample_Play(), which will automatically choose a channel to play back on, and
Voice_Play() which lets you choose the channel. I found that
Sample_Play() always chose exactly the wrong channel and ended up stopping sounds that I wanted to continue playing, so I switched to the alternative function.
Voice_Play() doesn’t have any useful documentation. Inspecting the library’s code reveals that the trick is to set the volume, frequency and panning of the voice each time a new sample begins:
Voice_Play(channel, sound, 0);
Voice_SetVolume(channel, sound->volume << 2);
The DS has a second screen. What do you do with two screens when remaking a game designed for one? I had a few ideas:
- Level editor;
- Chuckie Egg title screen;
- Simian Zombie logo;
- Dragon 32 image.
In the end the SZ logo was simplest, so I went with that. Otherwise, the DS is my default platform, so there aren’t any surprises here.
It appears that calling
snd_sfx_stop() on a channel playing a stereo WAV file will only stop one of the channels, which explains why sounds weren’t behaving themselves in the beta. Switching to mono files fixed it. Another problem was caused by limitations of the audio hardware: the Dreamcast can only play back the first 65534 samples in a WAV file, which is far shorter than several of the sounds in the game. To work around this I halved the sample rates of the longer samples. That fixed most of them, but the title song and Spectrum loading sound were still too long. I converted those to MP3 and used the streaming API, but that created problems of its own. Sounds still cut out or don’t play, and the emulator I’ve been using to help development crashes whenever I call
mp3_stop(). There’s still some work to do there.
The DC has a similar problem to the PSP: I’m using the 320x240 NTSC graphics mode, but a good portion of that is off-screen on my TV. The “usable” framebuffer space concept helped out here.
The DC feels noticeably faster than the other two platforms, which is odd, as I’d expect them to run at identical framerates. Perhaps it’s just because it runs on such a massive display.
The Dreamcast’s reversed ARGB ordering means I’ve had to convert bitmaps into both DS and DC sourcecode and use #ifdefs to choose the right version of the data for the current target platform at compile time.
What was most surprising was how little effort I had to put into Chuckie Egg in order to make it performant on these different platforms. The effort that went into Hanky Alien presumably helped a lot. I was testing HankyAlien with up to ~140 views on screen at once and up to ~45 of those updating simultaneously. In contrast, despite superficially being a more complex game, Chuckie Egg has ~45 views on screen at once and only ~10 updating simultaneously. (It’s interesting to note that, while both the DS and DC struggled with the HankyAlien test scenario and their framerates suffered, the PSP sauntered through it without a hitch. I really must write something specifically for the PSP in future.)
Writing object-oriented C is fun, but sometimes I look at the amount of boilerplate necessary to create a new “class” and decide to do things another way, particularly if I’m creating a base class. The most simple class requires:
- A static metaclass struct, which forms a linked list back through the chain of superclasses (it enables the program to determine the type of an object at runtime).
- A static callback struct that includes pointers to equality/comparison/hash/copy/dealloc functions.
- Declarations of those functions.
- Implementations of those functions.
- A struct with the member variables of the class.
- A typedef that defines a pointer to the struct, allowing the struct to be hidden.
- A constructor that calls the allocator and initializer methods of the class.
- An initializer function that sets up the members of class instances and calls back to the superclass’ initializer.
- A convenience release method that wraps the basic release method.
If the class is intended to be subclassed it must also:
- Provide an additional private header including:
- A callback struct definition that nests the superclass’ callback struct and enables “methods” to be “overridden” (really it’s the template pattern with function pointers).
- The struct itself, so subclasses can directly access properties of the struct and include the struct as the first item in their own structs.
- Call the functions in the callback struct when appropriate (including those for the superclass).
Every time I create a new class I do so by copying-and-pasting an existing class and modifying it rather than start it from scratch and have to retype all of the boilerplate. Sometimes I’ll think about the work involved in setting one of these things up, pine for C++ or Objective-C, and then use a different structure that’s less “correct” but will still get the job done without smelling too badly. The grain and eggs, for example, should really inherit from a single “collectible item” class, but the correctness of that design doesn’t in any way compensate for the amount of work involved in setting it all up.
Keeping a strict separation between the model (game logic), the views (the stuff you see on screen) and the view controller (delegate of the model; updates the views to match incoming events) results in some beautifully clean code but makes prototyping difficult, especially early on when none of the infrastructure is in place. I did most of the early prototyping in the view controller before moving it into a separate model.
Each level renders its static content (ladders, platforms and bird cage) as a bitmap when it begins. It passes that up through the model to the view controller, which uses the bitmap as the background view for the game. That eliminated the need to create dozens of views to represent tiles that never change during the course of a level.
There are now 134,000 lines of code in the project, but a sizable number of those are included in bitmaps that have been converted to C.