Yes, the title is a pun. Today I’m going to talk about sprites in The Immortal. And to do so, I have some art (diagrams).
What is a Sprite
Sprites are quite interesting, because they fundamentally differ between consoles and computers of the time. On a comparable game console like the Super Nintendo for example, sprites are handled partially by the software, and partially by a bit of dedicated hardware on the PPU. Essentially, the SNES has dedicated memory for sprite data. For the gfx, it has Video RAM, a dedicated portion of memory on the PPU that is converted directly into the screen layer (baring any HDMA shenanigans). For the properties (relative x, y, etc.), it has OAM, Object Attribute Memory. By contrast, on the Apple IIGS, there is no dedicated video memory per se, it’s simply a buffer in RAM that is assigned to be the screen memory. And there is no OAM, but this is because there is also no fundamental limit on sprites. This is where they start to differ. A console will have a certain amount of OAM, because sprites are applied to the screen by the hardware, not software. The game tells the PPU where on the screen it wants a given sprite, and the OAM data tells the PPU how to arrange the gfx data. But from there, the application onto the screen layer is done behind the scenes. This is unlike the Apple IIGS, where instead the game must perform both functions. It must handle the general sprite arrangement, and it must also handle the screen ‘munging’ to apply the sprite to the layer. This is a tradeoff, as the game gets to set its own limit on sprites, based on the rest of the game processing. However it must balance this with the processing needed to actually render the sprites.
To clarify, let’s start by looking at the screen:
On the SNES, the PPU is responsible for taking the sprites and flattening them into the layer, based on priority levels in OAM and transparencies. On the Apple IIGS however, there are several routines in Driver.GS dedicated to ‘munging’ the screen together. Ie. irreversibly changing the screen buffer by applying the sprites onto the direct byte data.
As a result, sprite data itself is a little bit different between consoles and computers, as it is forced to be more standardized on the console side. With the computer the methodology is, strictly speaking, up to the programmer.
What is a Sprite in The Immortal
Okay, so now the question is, what is that methodology in The Immortal? Well let’s start by sorting out a methodology ourselves first.
The first question towards that goal is, what does a sprite need to do? Fundamentally, all a sprite needs to do is display a bitmap at a point on the screen. Let’s start with that:
– X, Y
But sprites have another characteristic as well, they tend to (not always) be animated. This is where it gets a lot more tricky. There are many ways to animate a sprite. We could start with:
– X, Y
– *bitmapFrame0, *bitmapFrame1
Although now we need a way to know which frame we are on, so really we need:
– X, Y
– *bmpF0, *bmpF1
Hmmm. But what if the bmp for one frame needs to be offset compared to a different frame? Then we would need relative point data for each frame entry. At that point we might as well make frame a separate data type:
– rX, rY
– X, Y
– Frame f0, Frame f1
Okay, that seems fair enough. So let’s take a look at what The Immortal sprite data structure looks like:
Alright, some of this looks familiar. We have an X and a Y, and a frame index. However we also have something called ‘num’, as well as a file pointer, something called ‘on’, and a priority value.
I will save this post the trouble of tracing certain values backwards, but I will also mention that this is one of the reasons that I mentioned I am trying to translate in layers. If you start at Sprite, you don’t know where the file reference came from, or what the purpose of num is. But if you start earlier than that, you are introduced to those in the sprite_list.GS file, where you see how they are put together. For instance, the file reference comes from a table that is put together as files are loaded in. This table is then used by quite a few different routines. Num is also tricky, because it is not clearly defined by its name. It is, in fact, an index used to find sprite data. However which index, is the question. So to skip ahead, I sorted through all of the data references, and we can see just how much more there is to a sprite other than what we might first assume.
For starters, we know that it starts as a bitmap in a file, and ends at the screen layer. We also know there is a munging stage before that:
Bitmap -> … -> Munge -> Screen
But to handle the animation and which bitmap from what file in which order etc etc, we have a few extra steps:
Bitmap -> Frame -> DataSprite -> Cycle -> Sprite -> Priority Sorting -> Munge -> Screen
Just a few extra things in between…
Frame and DataSprite (the name I gave it, it does not have a name in the source) will make sense shortly, but Cycle is the one that looks strange from the outside. You see what Cycle is from the outside, is some kind of data structure that gets processed, has a file reference, a sprite index reference, and a series of ints that are referenced as frames. However, if we look at DataSprite, we will also see a series of frame references, a file reference, and a sprite index. For that matter, everything seems to have a file and sprite reference. So what’s really going on?
Well, the short answer is that the file and index references are necessary for the sake of space, but end up creating a very convoluted web of indirect references to each other. Let’s define each of these data types now:
Bitmap: The actual byte data of a given sprite (not compressed)
Frame: One instance of a sprite image. Contains a reference to the bitmap it uses, relative offsets, and the size of the rectangle (the size of the sprite data).
DataSprite: In the source, this is actually data that is copied from the file and stored in the heap, but then modified after the fact as if it were normal RAM sprite data. It is in fact, attributes of a sprite. You can think of it as the interface between the ROM sprite data and the RAM sprite instance. It contains centre X and Y values (these are what get modified by the game itself), a list of references to frames, and the total number of frames. Crucially, it does not contain an index to the current frame.
Cycle: What I originally thought of as an over-arching animation system, I believe is better expressed as simply, the index to the current frame, of a data sprite. But with lots of overhead.
Sprite: This is what is actually used for drawing, and can only have N amount in memory at once. A sprite has an X and Y relative to the viewport, an index to the current frame of the datasprite, a priority level, a flag for if it is active or not (the ‘on’ from before), and a reference to the datasprite.
All of this can be expressed as such:
The red lines representation data movement, and the blue lines show the path from the file all the way to the sprite data in RAM. They also reveal what I was getting at before, which is that Cycle is weird. Cycle is functionally just an index for the animation frame, but the source treats it practically like an object type. It is conceptually distinct from the rest, as though it were an over-arching animation, but in reality there is a ‘Cyc’ for each sprite in memory, and it is really more of an extension of any given sprite, that allows the sprite to change which frame index the datasprite points to at any given time. However, Cycles are also defined by the Story record, as opposed to the object that gets animated. Implying it is, or was supposed to be, a more generalized method of cycling through animation frames of anything, as anything could be given a Cyc. This is just speculation though, in the game we have it is used exclusively to iterate the animation frames of sprites, and also get the data sprite for them some times. Wait what? Why would the cycle be responsible for the data sprite? And why does it have a reference to it when the sprite already has one? Great questions! I am still asking that same question myself. Maybe I’ll come back to this post and explain it better one day. Or maybe it’s just redundant. Or maybe, it’s a relic of a potentially more powerful and interesting animation system. Perhaps we will never know for sure.
Lastly, let’s take a look at which the data types actually look like in practice for our purposes:
Alright, until next time!