Sprite Techniques

From Uzebox Wiki
Jump to navigation Jump to search

Introduction

This tutorial comes about from some questions asked by forum member Vertig0zone. Seems to me there are a few ideas floating around on how to make the most of your sprites, but not a lot of explanation in one place. None of these ideas are new or terribly complicated, hopefully I explain them well enough. Note that some ideas could be used for Mode 2, but primarily the examples will be assuming Mode 3. Please read Video_Mode_3 first if you have not already. Any areas I am missing, please PM me so this can be more complete. As always, you will be personally credited for contributions. Now onto the good stuff!


Terms

Just to set things straight I'll define some terms you'll find in this document. I don't mean to repeat things that are already documented (and that you should have read before hand :P Video_Modes) in several places, so I'll keep them brief.


  • Sprite - Just to be clear, for this document we will consider sprites to be an (8x8 or 6x8 for mode 2) entity that we can assign any pixels to via it's graphic index. This object is different from the standard tile because it can be placed at any (x,y) offset. In Mode 3 a sprite is NOT a ram tile. The sprite is a structure that we can assign a position, graphics tile, and horizontal flipping. The kernel then uses as many ram tiles as the single sprite requires (from 1-4) based on it's position and alignment on 8 pixel boundaries. Assigning, in your makefile, more sprites than than the ram tiles they will require is worthless and wastes ram and a bit of speed.


  • Mega Sprite - a "mega sprite" is a collection of individual sprites, often times directly side by side and above/below. As an example, the Mario/Jump Man sprite in Paul's Donkey_Kong is a mega sprite comprised of 4 sprites arranged in a square. We would call this a 2x2 (2 sprites high/wide) or a 16x16(actual pixels) mega sprite. This is a common size because going too much bigger gets very expensive when moving and un-aligning with the boundaries. By the way, I think it was Uze who coined this and it's a pretty standard term by now.


  • Sprite overflow/rotation/flickering are all terms in regard to having too few free ram tiles to draw a scene in it's entirety.


DK MegaSprite.png


Initial Design

This is the first thing you'll want to do, but to be confusing you'll need to understand all the following concepts before you start. What you'll want to do is visualize your game. If you are remaking an existing game I suggest you play it for a good while, with an eye on it's design. The easiest games are the ones who have some of these attributes:


  • Smaller mega-sprites (16x16 is pretty reasonable)
  • Sprites that are often aligned on one or both axis ie (8,64),(16,24)
  • Objects that could be "flickered" without being disastrous, that is, some objects are clearly more important than others.
  • Games that wont require a lot of variables. Lets you have more free ram for extra ram tiles.


Another important thing, in my opinion, is to roll your own sprite handling routines. The kernel provides good functionality for basic mega sprites, but it is setup for generic cases that might not match your specific needs.


Sprite Rotation

This technique is also referred to as flickering, and is currently used in such games as Donkey_Kong(barely noticeable during play) and Adventures_Of_Lolo. Basically what you want to do is "gain" extra sprites by "time-sharing" them. To do this, you'll draw some sprites one frame and other sprites on another. Personally I think this is the way to go considering the limited ram and the desire to make graphically impressive games. It does cause some noticeable "irritants" to the display, but this can be minimized by designing your system intelligently based on whats most important for your game. It does allow you to display extensively more sprites with very reasonable results.

This simplest way to go about this, given a set of objects, is to draw them first to last then alternate from last to first. Here you should draw the most important sprite in the middle, since they will always be drawn. If you need specific objects to be on top of others, then this method might not be the best. As example, in Lolo when a shot hits an enemy or an enemy and Lolo overlap they will briefly flicker under and over each other. In practice it is of little consequence, barely noticeable, and I consider it be a good and fast solution. In games where there is a depth element such that lower objects must always be drawn over a "higher" object, this simple solution will not work(see the MegaBomber example below).

As example, imagine the NES game Contra. If you were going to design a simple sprite rotation system for this, you'd first want to see what the most common sprite is under a situation that requires flickering. Likely it would be the player's projectiles because of shotguns/etc, and conveniently they are also not nearly as important as anything else. During play you want to know where your character is, where the enemy is, and what they're shooting at you. Your own shots are no danger to you, they are fast, and you know where they are going anyways. In a situation like this I would draw projectiles flickering on and off all the time irregardless, gives them an interesting "shimmer", and when rotation is needed it doesn't look much different anyways. Now all you have to worry about is too many enemies on the screen. Likely you'll want all enemies to be drawn somewhat, but you would probably determine that enemies who are closest to you are the most important and are drawn for longer periods of time. A lot of possibilities, for this particular type of game, you'd definitely need to explore them all.

Ok, since this is a rant, I'll give another example based on Vertig0zone's game idea. Let's consider a fighting game like Street Fighter 2,3,Alpha,whatever. In the early design process you'll see there is no way to use that big of sprites. So after shrinking them down you'll notice some frames of the mega sprites are wider or taller than others. In situations where both fighters were using big frames and perhaps throwing fireballs, you might not have enough ram tiles. So here again we'll prioritize the issues. The fireballs can hurt a player, but they follow a straight line so there is no chance that their position will vary much each frame. Also we get the free shimmer effect mentioned before, and it really doesn't look bad for a fireball(check out SF2 in slow-mo, you maybe never noticed it). Right there we've helped out the worst case scenario, but we could still be in trouble considering we do have to draw the fireballs at some point. So in the case that it is inevitable to take sprites from the player, we need to decide which sprites would be less jarring to take away. Since the biggest frames are probably attacking frames, there will probably be some sprites that have more transparent pixels or are otherwise less important. Perhaps a non attacking foot, the edge of a knee, whatever you decide makes a sprite more or less important. They are only adding a little bit to the final image, and therefore are the best candidates to skip.

Building on this concept, how can our game know which are the least important sprites? This is why I recommended building your own sprite setup. You can add a table to store priorities for each sprite in a mega sprite frame. Although it will require some extra work,space,and speed, this gives you the best possible fine tuning to make your game look the best it can under any situation. Once you have setup your rendering to handle all this, you can add new frames for objects without recoding anything simply by adding the frames attributes to your tables. Finally to make this all useful, we'll need a way to know how many ram tiles will be required for our scene versus what is available. This is easily accomplished though does take extra time. All you'll need to do is to run through each object that is to be drawn, while taking into consideration it's width,height, and 8 pixel alignment. Using that information, you can calculate how much we will "overflow". Depending on how you declared your priorities specific to your game, you might choose not to draw sprite below some calculated threshold based on the severity of the relative overflow. This is the specific part that needs to be setup to match your game and the situations that will occur in it. Hope that all makes sense, I don't have any nice looking code to show you yet. TODO!


  • Mode 2 note - This mode has a lot of sprites available, so the kernels flickering for more than 5 sprites per line is pretty sufficient. If you really wanted to, the same concepts could apply at least to not draw sprites as often(when line overflow will occur) which are less important. Just a thought anyways, not much practical experience with Mode 2.

Overlapped Sprites

As the name implies, this is the process of overlapping sprites that are part of the same mega sprite. Probably a bit complicated to understand at first(in words), this is only useful for a mega sprite that does not occupy all the pixels of it's individual sprites. You know that you could draw 32 different sprites, all at (16,16), and it would only require 1 ram tile. You also know that just 1 sprite overlapping even 1 pixel over the 8 pixel boundaries will require extra ram tiles, and with this technique we will minimize this situation as much as possible. Because a sprite overlapping another is blit on top of the SAME ram tile, we can avoid using an extra ram tile in more situations since the edge will not overlap another boundary. Of course we will have to setup our graphics with this in mind, really the only issue that requires much work. This might be hard to understand in words, let's look at a picture.


AggregatedSprites.PNG AggregatedSprites2.PNG


The mega sprite in the first picture is 16x32 pixels or 2x8 tiles. In this example you'll see on the left a whole vertical column is filled with transparency. This adds nothing to the screen, but still takes extra ram. On the right side you'll see we have shifted half of the object to the left into that transparent part, essentially making this 15x32. Now when we go to draw it, we simply move it over by one pixel to line it back up. Although this doesn't seem like much in this example, now we can offset it by 1 from an 8 pixel boundary and still not need extra ram tiles(since it still fits in the 16 wide grid, think about it). Other graphics could see an even bigger improvement, this stuff does add up!

Now we notice on the second picture, we see the actual graphics data on the left side. The top is the normal method and the bottom has had it's graphics offset so it displays correctly when we offset the sprite into the main body a couple pixels. Since we will the void with transparency, no one will be the wiser for our little trick but we have just made our mega sprite 21 pixels wide instead of 24. Now I hope the picture is fairly self explanatory, what you have basically accomplished is being able to offset this mega sprite up to 3 pixels horizontally(from 8 pixel boundaries) and still not require extra ram tiles since it is only 21 wide. Perhaps one way to look at it, there is a 3/8 "chance" that it will no longer require extra ram tiles. So during the intensive phases of a game, where many objects are all doing their thing, this chance can make a big deal especially if other objects have these same chances. It could be the difference, in the worst case scenarios, between a stream of images that are reasonably acceptable or a stream of severally lacking graphics.

Let's consider a couple situations when this would be useful, and when it is undue effort. The first thing I would immediately consider this for, is free moving mega sprites. Here you are encountering worst case scenarios often, and this is really the most likely to have severe "overflow" problems. So you could apply this method for both axis' such as this picture:


OverlappedBall.PNG


Secondly in situations where the sprites are generally aligned on one axis, this is only useful in the direction of travel. Take for example this mega sprite(2x3):


WhiteBomber.PNG


You see that the left frame benefits from from this, whereas it is worthless in the right. So you might find it easier to make this frame normally and use (0,0) offsets for the sprites. Even though this demonstrates that this is not always are even usually useful, I'd like to point out an idea you probably had at some point. Given the sprite limits, to make the most of them the graphics should preferably fill up as much of the frame as possible. This is not realistic in a lot of graphics and the effect seems to be smaller sprites though they still cost the same amount. Now at least there is a benefit for sprites not requiring the whole frame, and you needn't feel compelled to use graphics fitting to this. Make the graphics how they should look naturally and reap the slight rewards when things get hectic.


To wrap it all up, this method wouldn't be difficult to implement once you understand it. Even at that, I consider it pretty hard core since this would work best be coupled with the previously mentioned robust sprite rotation system. I don't have an implementation to show you, nor have I seen an example of it yet. So you might consider this experimental/theoretical as to how effective it would be in action, but logically it seems like the way if you are going all out on a cutting edge Uzebox title. Certainly no draw backs other than the extra effort, and I'd be surprised if these methods didn't pay off big time in a game with "too many" sprites. As for implementation you probably have your own ideas as for your needs and your personal taste for ease of use. Just for a lame example, I'm using something like the following in a demo I'm working on. This is just how I do it, I'm not claiming it's elegant or clever but seems fast enough. You have many reads from flash and extra space usage, but the whole basis is that better control of sprites is worth it. This simple example does not implement the overflow and priority conditions described in Sprite Rotation, since it can be way more complicated than the whole example. I leave that for you to design to your needs.


  const char FrameTable[] PROGMEM = {
  
  //Frame 0 White Bomber Facing up(mirrored)
  
  0,0,//top left tile index,top right
  1,1,//middle row...
  2,2,//bottom...
  
  1,0,//mirror reference,offset reference - these indicate which mirror/offset frame to use
  //etc...
  };
  const char MirrorTable[] PROGMEM = {//mirror reference(in frame table) indicates which mirror map to use for horizontal mirroring
  0,0,//Index 0 - no mirror
  0,0,
  0,0,
  0,1, //mirror the right side
  0,1,
  0,1,
  //etc...
  };
  const char OffsetTable[] PROGMEM = {


  };


  • Now to make this information useful, something like this should suffice:...I'll get some code up in a while

Letter Box

This is documented in Ram_Tile_Effects_Primer (and of course brought up by Uze originally) but I will cover it anyways. In correction to my previous error, you can use this in conjunction with scrolling. This is how you set it up:


  KERNEL_OPTIONS += -DSCREEN_TILES_V=26 -DFIRST_RENDER_LINE=28


Using the switch in your make file, you are specifying that the vram array is 26 tiles tall(28 is default) and that the first line drawn(first line of the top tile) starts 28 pixels from the top of the physical screen. You can decrease 26 to whatever you need, but you'll need to adjust the first render line accordingly if you want your image centered vertically. (TODO INCREMENTS HAVE TO BE MULTIPLES OF 2?) What you'll gain from this:


  • for each 2 rows you shrink vram, you basically get an extra ram tile.
  • for each 2 rows, ~14,000 extra cycles to run your code and pre-process ram tiles. More than enough time to make 30+ ram tiles possible even at a conservative 26.


Pretty big advantages if your game can implement this well. On the default setup of 28 rows with complicated game logic, you will often run out of time before you utilize and blit the available ram tiles. So this is probably the more important part of it, as it would allow a robust rotation system enough time to run. If you already have your game drawing to the screen, you will now need to make sure that it doesn't draw outside of the new vram window. So in the above example, SetTile(0,26,tileIndex) would write into memory that is no longer vram which will cause problems. Below is an example of this technique in action in an early game prototype well suited to it. It uses 24 rows and 31 ram tiles with sound channel 4 turned off(tons of time still available for extra gameplay logic). The mega sprites are 3x3 and sprite flickering is barely an issue, the extra time allows a good sprite rotation system(accounting for y-ordering also). Not to mention Ram Tile Effects on the sprites...muahaha :D


MegaBomberSprites.PNG


Conclussion

For now I'm considering this tutorial usable and near complete, though I'd love to know how to improve it. So, as with any of my tutorials, questions are appreciated and I'll try to add an example based on them. Thanks for reading, hopefully this helps someone design the next big Uzebox game!