Retro Game Programming Tips, Part 2: Double Buffering with Page Switching
Introduction
In this second part of our retro game programming series, we'll look at implementing double buffering with page switching. Double buffering is a technique where you draw your game screen into a buffer, and then quickly copy the entire buffer onto the screen to prevent flickering. If the computer supports multiple video memory pages then your buffer is the next page and the switch is implemented in hardware, costs no additional CPU cycles, and is very fast.
In this article, we'll assume two pages and the ability to tell the system which page is currently being drawn to and which page is currently displayed. We'll then alternate between the drawing page and the display page to implement the switching.
Platform Specifics
Let's start our journey by defining our platform-specific functions.
The function gsetpage(pg_type, pg_no)
sets the current page. Valid values for pg_type
are PG_DISPLAY
for the page currently displayed, and PG_WRITE
for the page to which all graphical operations go. These two values are bitmasks, so you can use them together. In other words, you can call the function like this: gsetpage(PG_DISPLAY|PG_WRITE, 0)
. Since we're assuming we only have two pages, the valid values for pg_no
are 0 and 1.
Function gsetcolor(color)
accepts only two values: CO_FORE
for foreground color, and CO_BACK
for background color. Drawing with the background color effectively erases the content. Finally, function gcls()
clears the write page.
Now let us use all of them to write the clear screen function.
void clear_screen()
{
gsetpage(PG_WRITE,0);
gcls();
gsetpage(PG_WRITE,1);
gcls();
gsetpage(PG_DISPLAY,0);
gsetcolor(CO_FORE);
}
The Game State
We need a structure to store our game state. For simplicity, let's assume that we are only going to draw one sprite. Our game state structure, therefore, needs at least three members: the current display page, the current sprite position, and the previous sprite position.
Why do we need the previous sprite position? Because we will be drawing to a different page each cycle. And when we draw the next page, we will need to erase the previous content. The previous coordinates will help us with this task.
Our game structure will look like this:
typedef struct game_s {
uint8_t page;
int x;
int y;
int prevx;
int prevy;
}
Following figure demonstrates our algorithm.
Implementation
We are now finally ready to implement our game loop. We will start by initializing the game structure. Then we will enter the loop and alternately display the current page and prepare the next page for displaying. We will use data from the previous page for content removal and data from the current page for calculating the next sprite position.
void game_loop() {
/* remember our clear screen? */
clear_screen();
/* initialize game at position 0,0 */
game_t g = { 0, 0, 0, 0, 0 };
/* first display page to 1, but write page to 0 */
gsetpage(PG_DISPLAY,1);
gsetpage(PG_WRITE,g.page);
/* is there previous game state? */
bool has_prev_state = false;
while(1) {
/* set color to foreground */
gsetcolor(CO_FORE);
/* you need to provide function draw_sprite to draw sprite
at position x,y */
draw_sprite(g.x, g.y);
/* show drawn page */
gsetpage(PG_DISPLAY,g.page);
/* set next page as drawing page */
g.page = g.page ? 0 : 1;
gsetpage(PG_WRITE,g.page);
/* delete previous lines? */
if (has_prev_state) {
/* setting color to CO_BACK will delete everything
drawn previously. */
gsetcolor(CO_BACK);
/* and erase sprite using previous coordinates */
draw_sprite(g.prevx, g.prevy);
} else
has_prev_state=true;
/* store prev. state */
g.prevx=g.x; g.prevy=g.y;
/* your function to calculate next sprite position
and set g.x and g.y */
calc_next_sprite_position(&g);
/* game over? */
if (end_of_game()) break;
}
}