Zalo DS Blog

Sunday, July 31, 2016

Game Boy Development - Tips and Tricks (II)

5. The game loop

Most of the examples I've seen contain a main loop similar to this

void main() {
<game initializations>
while(1) {
wait_vbl_done();
...
<process input>
<your frame update>
}
}

this is a good starting point. Waiting for the vblank interruption prevents your game from running faster than it  should. If you start with this and keep coding you'll eventually notice a frame rate slowdown. A high slowdown actually, and that is because your frame update at some point takes more time than what is available between 2 vblanks, but you are still waiting an extra vblank.

1. Skipping vblank wait when we don't need it

One way to improve the previous code will be:

void vbl_update() {
vbl_count ++;
}

void main() {

//Subscribe vbl_update to vblank interruption
disable_interrupts();
add_VBL(vbl_update);
set_interrupts(VBL_IFLAG);
enable_interrupts();


while(1) {
if(vbl_count == 0)
wait_vbl_done();
vbl_count = 0;
...


}
}

in this new code we have declared an interruption that will store in a var the number of vblanks that have happened during the current frame. If there are more than one we no longer need to wait to the vblank and we can continue processing. Bear in mind this can duplicate your framerate up to x2 specially in situations where your processing takes more than one vblank just for a few instructions

2. Frame skipping

This is a good improvement, but it can be improved even more by using frame skipping. As its name says it consists of skipping the painting of some frames to save processing time and this way keeping the game logic running without suffering an slow down. So let's say your game is running at 60Hz meaning your fixed update time is 0.016 secs aprox, in normal conditions you will update your logic 0.016 secs, paint, update 0.016 secs, paint... and so on. Then at some point you realize that updating your logic takes more than 0.16 secs and in order to prevent an slow down  you start dropping frames like: update 0.016 secs, update 0.016 secs, paint, update 0.016 secs, update 0.016 secs...

In the Game Boy the painting happens automatically and it takes 0 processing time from your main loop. The only thing you need to do is prepare the OAM table and update the scroll coordinates and it will just happen. So just doing two consecutive updates during one frame iteration will not help up us at all

Once thing we can do though is processing several updates together. Instead of updating 0.016 secs and then update 0.016 secs we can do one single update of 0.032 secs which basically means multiplying all your updating calculations by 2 (which is a very simple operation as we saw), like this:

...
while(1) {
if(vbl_count == 0)
wait_vbl_done();
delta_time = vbl_count ? 1 : 0;
vbl_count = 0;
...
}
...

so now we have a var called delta_time that is 1 or 0 depending on having to do frame skipping or not. If for example in some of your updates you were doing
 sprite.x += 1;
you just need to change it to:
 sprite.x += 1 << delta_time;
x <<  0 is the same as multiplying by 1 and x << 1 is the same as multiplying by 2

On this implementation we are just skipping one frame, doing 2 will be complicated because that cannot be done with a shift operator. Skipping 3 will be posible but If you need to do that you should probably take a look at your calculations and try to optimize a little bit (I mean your update is taking 4 times what it should...)

3. Keeping the scroll movement smooth

Even though your game is not running at 60Hz you can still do your scroll being updated at 60Hz if you update  the scroll position registers during the vblank interruption. In order to do this you should store your scroll position in a variable during your update process and then refresh the scroll position registers during vblank. But instead of doing

SCX_REG = scroll_x;

update them like this:

if(old_scroll_x < scroll_x)
old_scroll_x ++;
else if(old_scroll_x > scroll_x)
old_scroll_x --;
SCX_REG = old_scroll_x;

where scroll_x is the current x position of the scroll and old_scroll_x is the position in the previous frame. If your scroll was moving faster than one pixel per frame sometimes you can improve this with an small spring interpolation

if(old_scroll_x < scroll_x)
old_scroll_x += (scroll_x - old_scroll_x + 1) >> 1;
else if(old_scroll_x > scroll_x)
old_scroll_x -= (old_scroll_x - scroll_x + 1) >> 1;
SCX_REG = old_scroll_x;

And you should do the same for the y axis. You can check the full implementation here

6. Funny things in the world of 8 bits

Here is a list of some of the things that made me waste some time because I am not very used to work with these kind of limitations

- printf implementation only works with 16bits values. Funny when this is the only method you have to get some debug info and you realize you are printing all that info incorrectly. If you want to print the values of 8bits vars ensure you do a casting:
printf("signed:%d - unsigend %u", (INT16)signed_8_x, (UINT16)unsigned_8_x)

- lcc optimizes array indexing using ints, which in 8bits limits array access to 128 as stated in the documentation. It also says that for statically allocated arrays this doesn't apply. At some point of the implementation of the SpriteManager I started seeing some weird behaviour and it took me a while to discover that actually when accessing sprite[10] the 10 was calculated as sizeof(struct sprite) * 10 instead of sizeof(struct sprite) * (UINT16)10 giving me te wrong address of sprite 10 and causing some weird behaviour

- As seen above it is actually a very bad idea to randomly acess an element of an array of structs because it contains a multiplication. Instead it is a good idea to create an array of pointers to the elements of the array on startup and use this array instead

- I've read in lots of places that structs have a lot of problems and you shouldn't use them... as far as I have seen there are no problems using them (except for the array accessing issue which actually makes sense for optimization purposes)

- Also, I've read in lots of places that you cannot use pointers to functions. I have successfully used pointers to functions of 0 and 1 parameters. And the GDBK uses this for interruption handler!

- There are a lot of tutorials telling you how to do things... you'll be surprised to see some of them contain errors and you should use them as reference but never as the absolute truth. Once again check this post from AntonioND

- As a final conclussion... is the GBDK a mess like lots of people are saying on lots of forums and blogs? I have been using it for a month and I can tell you that is a lie. I am not gonna say working with it is easy but that's not the GBDK's fault alone. First there is the SDCC which is the compiler the GBDK is using and probably the one people should be blaming. There is a new version of the GDBK using the latest version of SDCC here, I didnt' have the time to test it. But most of the things people seem to complain about are more related to a lack of C knowledge (and again, this is not a good enviroment for learning it) and the limitations of having to work with and 8-bit machine. The creators of the GBDk are perfectly aware of this as you can see here

0 Comments:

Post a Comment

<< Home