#10265 - hnager - Mon Sep 01, 2003 2:52 am
Wondering if anybody has had any luck with an OOP approach toward handing sprite data...I'm currently working with 256 color sprites and copying all of my sprites in to memory in one fell swoop. I would rather have each sprite set in a seperate header file with it's own 16 color palette and not have to know specifically where in the sprite memory a given sprite is...and also only have the sprites that are needed copied in to memory. Anyhow - I'd love to see what any body has come up with...thanks! Howard
#10271 - sajiimori - Mon Sep 01, 2003 7:45 am
If I'm interpreting correctly, you're saying you want a system for automatically allocating blocks of character RAM, presumably in C++.
Forgive my C++; it might be a little rusty. The following is not guaranteed to be bug-free, or even to compile.
Code: |
// Number of sprite characters in VRAM
#define NUM_CHARS 1024
// Blocks must be allocated in multiples of 16
#define MIN_ALLOC_SIZE 16
// Macro for rounding length up to a multiple of 16
#define ROUND_TO_MIN_SIZE(x) (x & 15 ? (x >> 4) + 16 : x >> 4)
// Couldn't possibly have more than this many seperate free blocks.
// One extra for end marker.
#define FREE_BLOCK_ENTRIES (NUM_CHARS / MIN_ALLOC_SIZE + 1)
struct CharacterBlock
{
int start;
int length;
};
// I'm inlining everything for simplicity.
// Don't do it in real life.
class CharacterRAM
{
private:
// Map of available character blocks.
// Last one is marked by setting its length to 0.
// Being static, they are all 0 initially.
static CharacterBlock free_blocks[FREE_BLOCK_ENTRIES];
public:
// Gotta call this first.
// There aren't static constructors in C++, right?
static void Init()
{
free_blocks[0].length = NUM_CHARS;
}
// Return start of first free block of requested length.
static int Allocate(int length)
{
// Round up to multiple of 16
length = ROUND_TO_MIN_SIZE(length);
CharacterBlock* b = free_blocks;
// length of 0 means no more free blocks
while(b->length)
{
// Have we found one?
if(b->length >= length)
{
// Got it!
int found = b->start;
// Bite chunk off beginning of free block.
b->length -= length;
b->start += length;
// Is the block all used up?
if(b->length == 0)
{
// Move the rest of the blocks down.
do
{
++b;
(b-1)->start = b->start;
(b-1)->length = b->length;
} while(b->length);
}
// Done.
return found;
}
// Haven't found it, try the next one.
++b;
}
// No more room...
return -1;
}
// Free is much harder.
static void Free(int start, int length)
{
// Just to be sure. Maybe start should be
// checked too...
if(length == 0)
return;
CharacterBlock* b = free_blocks;
int temp;
// Find first free block that is after the
// block to be freed.
while(b->start <= start && b->length)
++b;
// If the block before the freeing block
// ends where the freeing block begins...
if(b > free_blocks &&
(b-1)->start + (b-1)->length == start)
{
// Lengthen the block before.
(b-1)->length += length;
// If the freeing block also ends where the
// block after starts, then the block before
// and the block after need to be merged.
if(start + length == b->start)
{
// Increase the block before to cover
// the space of the block after.
(b-1)->length += b->length;
// Move the rest of the blocks down one.
while(b->length)
{
++b;
(b-1)->start = b->start;
(b-1)->length = b->length;
}
}
}
// Either there isn't a free block before the
// block to be freed, or it doesn't end where
// the block to be freed begins.
else
{
// If the block after begins where the
// freeing block ends, move the beginning
// of the block after to cover the freed
// block (if there is a block after).
if(start + length == b->start && b->length)
{
b->start -= length;
b->length += length;
}
// If neither end can be merged in any way,
// create a new free block and move the
// rest up one.
else
{
do
{
temp = b->start;
b->start = start;
start = temp;
temp = b->length;
b->length = length;
length = temp;
++b;
} while(length);
}
}
}
};
// Palettes are arrays of 16 unsigned 16-bit ints.
// Here, they can be allocated one at a time, and
// freed all at once. You can store an unlimited
// library of palettes (elsewhere), and use up to 16
// of them at a time.
class SpritePalettes
{
private:
// Pointers to the palettes currently in use.
// Make sure the palettes have a permanent home
// (i.e. ROM or static arrays, but not on the
// stack), or this list will quickly become useless.
static u16* palettes_used[16];
public:
static int Allocate(u16* palette)
{
for(int i = 0; i < 16; ++i)
{
// Is it already loaded?
if(palettes_used[i] == palette)
return i;
// Is this spot free?
if(palettes_used[i] == 0)
{
palettes_used[i] = palette;
// <Insert code here to copy palette to
// hardware at appropriate address>
return i;
}
}
// If we got here, there was no room for another
// palette.
return -1;
}
static void FreeAll()
{
for(int i = 0; i < 16; ++i)
palettes_used[i] = 0;
// Clear hardware palettes here if you want,
// but you probably don't need to.
}
};
// Only need one of these for each kind of sprite.
struct SpriteType
{
// Pixel data, probably in ROM
u8* character_data;
// Total characters for all frames
int num_chars;
// Pointer to preferred palette
u16* palette;
// Number of the first character in VRAM.
// Make it -1 or something if it's not loaded.
int character_start;
// Hardware palette number.
int palette_number;
};
// Example
class Goblin
{
private:
static SpriteType type;
static int num_goblins;
public:
Goblin()
{
++num_goblins;
if(type.character_start == -1)
{
type.character_start =
CharacterRAM.Allocate(type.num_chars);
if(type.character_start == -1)
; // handle out-of-space error
type.palette_number =
SpritePalettes.Allocate(type.palette);
// should handle palette error, too.
}
}
~Goblin()
{
--num_goblins;
if(num_goblins == 0)
{
CharacterRAM.Free(type.character_start,
type.num_chars);
type.character_start = -1;
// palette isn't freed... gotta free
// them all from time to time, or make
// a more robust system.
}
}
};
|
Let me know if there are errors.
To those who don't like my style/indentation/whatever, bite me. ;-)
Edit: Master Mofo's post just reminded me that I forgot to actually copy the char data to VRAM in the example. The copy would be performed just after CharacterRAM.Allocate() is called. Waiting for vblank shouldn't really be necessary in this case, because it can be assumed that nobody is using those characters yet anyway. Of course, the usual rules about the OAM still apply.
Last edited by sajiimori on Mon Sep 01, 2003 8:11 am; edited 1 time in total
#10272 - Master Mofo - Mon Sep 01, 2003 7:45 am
Yes it is possible. you can make a sprite class that has member methods for handeling sprite and palette, so you could only call these methods and pass them the data. the idea is in making sure to write in the OAM and Char memories in the right time. try the VBlank period for example.
_________________
"You have to finish what you have started"
#10279 - hnager - Mon Sep 01, 2003 2:18 pm
sajiimori! thanks for the replay - I'll copy that all down and give it a go...
#10282 - hnager - Mon Sep 01, 2003 3:51 pm
It looks as if it would be relatively easy to swap in 256 color sprites by plugging in a different SpriteType. I know that it is possible to have both 256 and 16 color sprites at the same time (with some tricky palette management) but I could set a flag or use a factory to provide the proper SpriteType...anyhow, it seems as if I may want to have SpriteType be a class aswell so that I can allocate the correct number of frames in the spriteFrame array and assign them their values based on the type of sprite.
For example, a 256 color sprite would be:
Code: |
spriteFrame[0] = character_start;
spriteFrame[1] = character_start + 8;
spriteFrame[2] = character_start + 16; |
and 16 colors:
Code: |
spriteFrame[0] = character_start;
spriteFrame[1] = character_start + 4;
spriteFrame[2] = character_start + 8; |
Aswell as have the copy to OAM of sprite data as part of that class...again, changeable based on 16 or 256 color sprites.
Let me know if that doesn't sound correct...thanks again.
#10283 - hnager - Mon Sep 01, 2003 4:21 pm
Are you using the Goblin class as the actual Sprite or is it to just manage the sprite data? either way, where would you set type.num_chars and type.palette? or would it make more sense to set all of that up outside of the Goblin class and have Goblin point to the instance of the SpriteType? again, to easily swap between 256 and 16 color sprites.
On the subject, what else is there beside how the data is stored and copied to memory that differenciates 16 and 256 color sprites? Are there things that can be done with one and not the other? rotation? hflip? etc.
#10285 - hnager - Mon Sep 01, 2003 4:39 pm
compiling errors - I'm going to do some looking, but why would I get:
main.o: In function `CharacterRAM::Init()':
main.o(.text+0xdc): undefined reference to `CharacterRAM::free_blocks'
main.o: In function `CharacterRAM::Allocate(int)':
main.o(.text+0x160): undefined reference to `CharacterRAM::free_blocks'
with the provided code?
#10288 - sajiimori - Mon Sep 01, 2003 7:15 pm
re: Compiler errors (technically linker errors): I didn't actually declare the static data anywhere. In a .cpp file somewhere:
Code: |
// I think this is the syntax...
CharacterBlock CharacterRAM::free_blocks[FREE_BLOCK_ENTRIES];
|
This is necessary in C++ because the static data cannot be created in the header (CharacterRAM.h or whatever) because then it would be created once for every time the header was included, giving "multiple definition" errors instead of "no definition" errors. It's the price to pay for truly seperate compilation. I don't think it's worth it. ;-)
Because of the dynamic nature of the palette manager, I would recommend against using it while mixing 16 color and 256 color sprites. The palette manager is made to abstract the process of allocating 16 color palettes, so you don't have to think about where the palettes are located in VRAM. Consequently, it would become difficult to predict which colors should be used in a 256 color image, as the order of the colors could potentially change between different runs, or when small changes are made to the order that characters are loaded. If you want to mix 4-bit and 8-bit sprites, come up with a permanent palette layout and ditch the palette manager.
But yes, it's trivial to use 256 color sprites with the CharacterRAM class and SpriteType. Just double num_chars in the SpriteType.
The Goblin class is made to illustrate how one might keep track of both individual sprites and shared data (such as sprite characters). The idea is that when the first Goblin is created, the characters and palette are allocated, and when the last Goblin is destroyed, the characters are freed (but the palette isn't because the palette system is fairly weak; easily remedied with some effort).
So the static data includes num_goblins and a SpriteType. The instance data should then include perhaps a pointer to an OAM (copy) entry, and logical data such as hit points. Maybe you could change the constructor to Goblin(int x, int y) to create them at particular locations. I'll leave the instance stuff to you.
As far as where to put SpriteTypes, you could make them global, like this:
Code: |
SpriteType GoblinType =
{
goblin_char_ptr,
goblin_num_chars,
palette_ptr,
-1,
-1
};
|
You could also move character_start and palette_number from SpriteType to Goblin, leaving the rest of SpriteType constant. Declaring it 'const' would then locate it in ROM.
Oh, and Goblin only really needs a pointer to the SpriteType, so you can change that if you want.
I don't know of any differences between 16 color and 256 color sprites other than palette stuff and how much space they take up. Check the usual docs to be sure.
#10298 - hnager - Mon Sep 01, 2003 10:22 pm
I've since remedied the linker errors I was getting - and at least partial implementation is working! (as a singleton):
int b;
b = CharacterRAM::Instance()->Allocate(4);
sprintf(tracebuffer,"%d\n",b);
trace(tracebuffer); //traces out to the logging screen in VBA - traces 0
b = CharacterRAM::Instance()->Allocate(4);
sprintf(tracebuffer,"%d\n",b);
trace(tracebuffer); // traces 16
but I'm actually a little confused by what I get back from this, here is how I was copying sprite data in before:
DMA_Copy(3,(void*)spriteData,(void*)OAMData,128,DMA_16NOW);
for a single 256 color sprite.
how would that translate in to what we're doing with the CharacterRAM controller? Thanks again for all your help.
#10300 - hnager - Mon Sep 01, 2003 10:43 pm
oh BTW:
what are we looking to have returned with this macro?
Code: |
// Macro for rounding length up to a multiple of 16
#define ROUND_TO_MIN_SIZE(x) (x & 15 ? (x >> 4) + 16 : x >> 4) |
SHould ROUND_TO_MIN_SIZE(16) result in '1' or in '16' ? I'm getting strange results depending on what I pass it.
For example:
ROUND_TO_MIN_SIZE(32) //2 which seems to be correct
ROUND_TO_MIN_SIZE(33) //18 which seems to be incorrect (I'd expect '3')
thanks.
#10303 - sajiimori - Tue Sep 02, 2003 12:53 am
That macro was totally, utterly incorrect. ;-) I have no idea how I came to the conclusion that it was anywhere near what we want.
Expected results:
1 -> 16
16 -> 16
17 -> 32
32 -> 32
33 -> 48
etc.
I think this is more like it:
Code: |
#define ROUND_TO_MIN_SIZE(x) (x & 15 ? (x & ~15) + 16 : x)
|
CharacterRAM.Allocate() returns a character "name", which is an offset from the beginning of character RAM. Multiply the offset by 32 to get the offset in bytes (shifting left by 5 is faster).
Code: |
#define CHAR_MEMORY (u8*)0x6010000
#define NUM_CHARS 4
int b = CharacterRAM::Instance()->Allocate(NUM_CHARS);
DMA_Copy(
3, // I guess this is the DMA channel?
spriteData,
CHAR_MEMORY + (b << 5),
NUM_CHARS * 16, // 16 words per character
DMA_16NOW);
|
#10304 - hnager - Tue Sep 02, 2003 1:04 am
I'll plug in the new macro and see what I get - thanks for looking - right after I posted that it appeared to be working I looked again and realized that it actually was giving me odd values.
Excuse my confusion, but why is the result multiplied by 32?
#10309 - sajiimori - Tue Sep 02, 2003 6:15 am
Because there are 32 bytes per character (or 64 for 8-bit sprites).
#10316 - col - Tue Sep 02, 2003 11:43 am
sajiimori wrote: |
That macro was totally, utterly incorrect. ;-) I have no idea how I came to the conclusion that it was anywhere near what we want.
Expected results:
1 -> 16
16 -> 16
17 -> 32
32 -> 32
33 -> 48
etc.
I think this is more like it:
Code: |
#define ROUND_TO_MIN_SIZE(x) (x & 15 ? (x & ~15) + 16 : x)
|
... |
I'd suggest that what you want is:
Code: |
#define ROUND_TO_MIN_SIZE(x) ( (x+15) & (~15) )
|
cheers
col
#10319 - hnager - Tue Sep 02, 2003 12:25 pm
col
I'll see where that gets me - so far everythign seems to be working as expected except for one unrelated (to the macro) problem...more testing should narrow it down, but it seems as if the call to Free() isn't acting as expected...if i allocate a single sprite and then free it it doesnt seem to work - there may be other issues...i'll do some testing later on. thanks again.
#10331 - sajiimori - Tue Sep 02, 2003 6:32 pm
Pretty nice, col. Your version looks like it should be several cycles faster. I'm pretty sure both are correct, though.
hnager, I just compiled and ran my code (for the first time), and noticed that I forgot to round the length up when freeing. After adding that, I ran some sparse tests and things looked ok.
In other words, the remaining bugs should be subtle and hard to track down. ;-)
#10333 - hnager - Tue Sep 02, 2003 6:38 pm
ah 'easter egg bugs' - I don't have the file on me now, where should the length round up be?
#10334 - sajiimori - Tue Sep 02, 2003 6:59 pm
Any time before length is used, e.g. the first line of Free.
#10487 - hnager - Sat Sep 06, 2003 2:07 pm
Is this another potential hidden bug:
Code: |
// Gotta call this first.
// There aren't static constructors in C++, right?
static void Init()
{
free_blocks[0].length = NUM_CHARS;
} |
wouldn't you want to have all blocks assigned an initial length of 1024?
#10497 - sajiimori - Sat Sep 06, 2003 4:59 pm
No. The first block with a length of zero marks the end of the free blocks. Look at it this way: when the memory is empty, there is only one big free block.