Game Programming in C with the Ncurses Library

If you've ever wanted to create a simple video game that oozes lo-fi 1980's home computer nostalgia, you should definitely check out the ncurses programming library. It's a modern implementation of the original curses library that shipped with early versions of BSD UNIX. You might not be familiar with the name "ncurses", but you use it every time you type the characters t-o-p into your terminal.

To show the most basic usage of how you would use the ncurses library in your program, let's create a simple simulation of a "ball" bouncing back and forth across the screen:

Make sure you have a C compiler installed on your machine and have the ncurses headers available. I'm writing this on OSX Mavericks (which requires installing XCode), but other flavors of Unix should have the headers available for installation if they don't ship with the OS (e.g. apt-get install libncurses5-dev on Ubuntu).

Once you have the necessary dependencies installed, let's start with the basic building block of any ncurses-based program: the window.

Windows

Since we have a simple simulation, we only need to create a single window and draw to it:

initscr(); // Initialize the window
noecho(); // Don't echo any keypresses
curs_set(FALSE); // Don't display a cursor

To test this right now, create a Makefile:

# Makefile
LDFLAGS=-lncurses

all: demo

And a source file (we'll continue to use this file as we enhance the functionality over the course of this post):

// demo.c
#include <ncurses.h>
#include <unistd.h>

int main(int argc, char *argv[]) {

 initscr();
 noecho();
 curs_set(FALSE);

 sleep(1);

 endwin(); // Restore normal terminal behavior
}

Then compile and run the program:

$ make && ./demo

You'll see the screen go blank for a second and then your previous terminal will be restored. Not particularly exciting, but it sets the stage for drawing to the screen.

Drawing

Printing characters to our newly-created window is pretty simple. You treat the screen as a series of XY coordinates when deciding where to display text. Since characters are buffered before they're displayed on the screen, you need to refresh the window to see them.

For this, we use the function mvprintw() in combination with refresh():

#include <ncurses.h>
#include <unistd.h>

int main(int argc, char *argv[]) {

 initscr();
 noecho();
 curs_set(FALSE);

 mvprintw(0, 0, "Hello, world!");
 refresh();

 sleep(1);

 endwin();
}

Now, the text 'Hello, world!' will appear in the upper left corner of the screen for a second before your terminal window is restored. Remember that it's important that you call refresh() when you want to update your display -- forgetting to do this will prevent the text from being printed on the screen.

Moving an Object

Now that we can print text to the screen, let's move a "ball" across the screen. To do that, we'll print the 'o' character at an initial position on the screen and advance it to the right (hit ^C to exit):

#include <ncurses.h>
#include <unistd.h>

#define DELAY 30000

int main(int argc, char *argv[]) {
 int x = 0, y = 0;

 initscr();
 noecho();
 curs_set(FALSE);

 while(1) {
 clear(); // Clear the screen of all
 // previously-printed characters
 mvprintw(y, x, "o"); // Print our "ball" at the current xy position
 refresh();

 usleep(DELAY); // Shorter delay between movements
 x++; // Advance the ball to the right
 }

 endwin();
}

This will advance the "ball" from the upper left portion of the screen all the way to the right in one-second intervals. After a few seconds, you'll notice some undesireable behavior -- when the ball reaches the right-most portion of the screen, it continues to move even though we can no longer see it.

Collision Detection

Now that we've got object movement down, let's make it bounce off the "walls". For this you'll use the getmaxyx() macro to get the dimensions of the screen and add some simple collision detection logic for great justice.

#include <ncurses.h>
#include <unistd.h>

#define DELAY 30000

int main(int argc, char *argv[]) {
 int x = 0, y = 0;
 int max_y = 0, max_x = 0;
 int next_x = 0;
 int direction = 1;

 initscr();
 noecho();
 curs_set(FALSE);

 // Global var `stdscr` is created by the call to `initscr()`
 getmaxyx(stdscr, max_y, max_x);

 while(1) {
 clear();
 mvprintw(y, x, "o");
 refresh();

 usleep(DELAY);

 next_x = x + direction;

 if (next_x >= max_x || next_x < 0) {
 direction*= -1;
 } else {
 x+= direction;
 }
 }

 endwin();
}

Now the ball bounces as expected -- from left to right and back again, but if you resize the right-hand side of the window you'll notice that the ball either goes off the screen or bounces back before it hits the edge.

Handling Window Resizing

In our simple example, handling the case where the user resizes the window while the simuluation is running is trivial. Moving the call to getmaxyx() into the main loop will reset the "wall" location:

#include <ncurses.h>
#include <unistd.h>

#define DELAY 30000

int main(int argc, char *argv[]) {

 // ...

 while(1) {
 getmaxyx(stdscr, max_y, max_x);

 clear();
 mvprintw(y, x, "o");
 refresh();

 // ...

 }

 // ...
}

Now resizing the window when the program is running produces the desired result -- the ball consistently bounces off the new "wall" location.

Next Steps

This should serve as a good starting point for making your own interactive console games. If you've read this far and want a more in-depth introduction to the features and useage of the library, check out the NCURSES Programming HOWTO and Writing Programs with NCURSES tutorials.

As your games become more complex, you'll want to read up on the advanced windowing capabilities of the library -- take a look at the functions newwin, subwin, wrefresh, and mvwprintw to get started. I'll talk more about these and other related topics in future posts.

For reference, a complete version of the code discussed in this post is available as a Gist.

Patrick is development director in Viget's Boulder, CO, office. He writes clean Ruby code and automates system infrastructure for clients such as Shure and Volunteers of America.

More posts by Patrick