OS X Games from the Ground Up: Hexapawn – Introducing the NCurses Library

In the previous post we analysed the original BASIC programme, identified a few things we need to fix and noted several enhancements we could make.

Both of the games we’ve developed so far in this series have been text-based games that have run in a simple terminal window. This game will also be a text-based game but, unlike the previous two games, it will have a user interface.

Before we started coding our previous two games we made a flowchart to show how the game mechanics worked. When you are creating a game with a user interface, it’s also important to map out the interface before you start. My tool of choice for this sort of work is Omnigraffle, but pencil and paper work just fine. Here’s an example of a UI map for the version of Hexapawn we will be building:

As you can see, your interface map doesn’t have to be a work of art, neither does it need to show every detail of every screen. What it does need to show are what elements will go onto each screen and approximately how they will be laid out.

Once you have your interface map, you can use it to walk through typical user journeys to make sure they make sense. For example, here the player starts at the main menu in the centre. From there the menu items will take them to different screens for playing a game (top left), browsing the current state of the AI (top middle), browsing the game history (top right), saving/loading AI and game history (bottom left) or viewing instructions (bottom middle). I’ve also shown how a dialogue window will look (bottom right).

OK, now that we know what we’re building, let’s get started. Open Xcode and create a new OS X Command Line Tool Application, as you have done for the previous games in this series. Make sure you select C for the language.

Before we can start coding, there’s a small change we need to make to the project. For the previous two games we were able to run and test the games using the output panel in Xcode. Now that we will have a user interface, that isn’t going to work. Instead of running the application directly we will need to run it in an external terminal window.

To set this up, go to the Product menu and select Scheme > Edit Scheme. Make sure that Run Debug is selected.

The Executable is currently set to Hexapawn. Open this list and select Other…. This will open a dialogue window for you to choose a different application. Find and select your Terminal application. On my machine, this was found at Applications>Utilities>Terminal.app.

Now we need to tell the Terminal app which executable to launch. Select the Arguments tab. In the Arguments Passed on Launch section, click the + symbol. In the text box that appears, enter the following:

${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}

These are placeholders that will cause Xcode to pass the build directory and filename to the Terminal app each time we run the Debug version.

To check that everything is working, click Close, then click the Play button on the toolbar. All being well, the application should build successfully, and you should see a terminal window appear that looks something like the one below:

If you see something else, go back and check you have followed all the steps correctly. Especially check that the Arguments Passed on Launch line is correct. If you don’t see a window at all, it might have opened behind the Xcode window – use cmd + tab to check. If you have configured your Terminal to be something other than the default size of 80×24 or the default colour scheme of black on white, you might find it easier to temporarily re-enable these defaults while you are working through these tutorials.

Note that to close an application launched in this way, you can’t just click the close button on the application window. Instead, come back to Xcode and click the Stop button. This will close the process, including any windows it has open.

OK, so how do we begin to build a text-based user interface that supports multiple windows and screens and positioning of text? Well we could try and build a system to do that from the ground up, but that would be an enormous undertaking. One of the key principles you should keep in mind as your programming projects become more complex is to avoid reinventing the wheel. Fortunately there is already a widely used C library for building text interfaces. It’s called Curses. In fact the latest version is called NCurses (for New Curses).

Luckily for us, the NCurses library is a standard part of OS X, so we don’t even have to download and compile it. We do have to let Xcode know that we want to link to it though. To do that, make sure you have Hexapawn selected at the top of the Navigator Panel. You should see the Build Settings in the main window. Select the Build Phases tab. Click Link Binary with Libraries to open that section and click the + symbol.

In the Search box for the dialogue window that appears, enter cur. You should now see a much smaller list. One of the items on that list will be libncurses.dylib. Select it and then click the Add button. Your screen should now look like this:

To see what NCurses does for us, let’s convert the default Hello World programme to work with the NCurses library. In the Project Navigator, select the main.c file, and change it to match the listing below.

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

//

// main.c

// Hexapawn

//

#include <ncurses.h>

intmain(void)

{

// Start ncurses

initscr();

// Display message - NOTE printf has now changed to printw

printw("Hello, World!\n");

// Wait until 'x' is pressed

while(getch()!='x');

// End ncurses

endwin();

return0;

}

Press Play to build and run the code. You should see a terminal window that looks like this:

The programme his displayed the text “Hello, World!” and then put a cursor on the following line. If you are just seeing a blank screen with a cursor, check that you changed the printf function to printw.

At the moment the programme is hanging around, waiting for you to press the x key. Do that now (if it doesn’t work, make sure caps lock is off), and the screen should clear to be replaced with a few lines of text ending with [Process Completed]. Remember that, even though our programme has ended, you still need to go back to Xcode and click the Stop symbol to close the process.

Let’s have a look at what’s going on in this new version of Hello World. In line 6, we are now including the header for the NCurses library rather than the usual standard I/O library. In our first two games, all our output was written to the stdout stream, which, as we saw, can be easily redirected to various places. NCurses takes over the terminal window and maintains its own structures for input and output. By default, output goes to a stream called stdscr. To initialise these, we use the initscr() function on line 11. This sets the terminal to curses mode and clears the screen.

On line 14, we have replaced the normal formatted output command, printf, with NCurses own version, printw. These two functions work identically, except that printf directs output to stdout, while printw directs output to stdscr.

Line 17 does nothing except hang around waiting for the user to type a lower case x. Why do we need this? Try commenting out this line and running the programme again. You won’t see Hello, World! at all, just a few lines of text ending with [Process Completed]. This is because, when we end NCurses with the endwin() function on line 20, the stdscr output is lost and no longer visible. To see it, we need to keep the programme running until we are ready to end it. For now we are doing this by waiting for an arbitrary key press, but later we’ll provide a cleaner way for the user to exit the programme.

Uncomment line 17 and run the programme again to look at a couple of other features of the default NCurses output. You’ll notice that NCurses has placed a cursor on the screen. Try typing something, e.g. Hello yourself! (make sure whatever you type does not include the letter x). You should see something like this:

NCurses conveniently echoes your typing to the screen. You might not want either of these features to be enabled for a game, Fortunately, it’s easy enough to switch them off, with these lines:

C

1

2

noecho();

curs_set(0);

So far I’m sure you’re completely underwhelmed. It seems like we’ve used quite a few lines of code to do something we previously achieved with one line. Let’s redeem the situation by starting to use the power of NCurses. Suppose we want our Hello, World! greeting to appear in the centre of the screen? Change the programme as shown below:

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

//

// main.c

// Hexapawn

//

#include <ncurses.h>

intmain(void)

{

// Start ncurses

initscr();

noecho();

curs_set(0);

// Display message - NOTE printf has now changed to printw

mvprintw(10,32,"Hello, World!\n");

// Wait until 'x' is pressed

while(getch()!='x');

// End ncurses

endwin();

return0;

}

When you build and run again, you should see this:

(Note that all the examples in this sequence of tutorials assume you have your Terminal app set to the default colour scheme (black on white) and size (80×24).)

What’s changed? Firstly, in lines 12 and 13, we’ve disabled the echoing of input and the visible cursor. In line 16 we’ve used a slightly different form of the formatted print function. This version, mvprintw wants two additional arguments – y and x coordinates. It first moves the cursor to that location and then outputs the formatted string.

Two things to watch out for with all the NCurses functions are that they want the y coordinate first, then the x coordinate and these are relative to the top-left corner, not the bottom-left. If text does not appear where you expect it to on the screen, check you haven’t inadvertently swapped the y and x arguments and you are counting rows from the top down not the bottom up.

If positioning text on the screen was all the NCurses library did, it would hardly be worth the effort of using it. Fortunately it does a lot more, and one of its advanced features we’ll be taking advantage of is windows. In addition to stdscr, we can also create and send output to different windows. Let’s see how these work. Change your code to:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

//

// main.c

// Hexapawn

//

#include <ncurses.h>

intmain(void)

{

// Start ncurses

initscr();

noecho();

curs_set(0);

refresh();

// Create first window

WINDOW *window1=newwin(10,15,5,12);

box(window1,0,0);

mvwaddstr(window1,4,3,"window 1");

wrefresh(window1);

// Wait until 'x' is pressed

while(getch()!='x');

// End ncurses

endwin();

return0;

}

The function refresh() in line 14 redraws stdscr. Even though we don’t have anything on stdscr at this point we need it here because of an (ahem) “undocumented feature” of NCurses which prevents windows being properly displayed until stdscr has been updated at least once.

in line 17 newwin creates a new window. The arguments passed to this function are the window’s height, width, and the y and x coordinates. It returns a pointer to an area in memory that keeps track of the properties of the window and its contents. In a later part of this series we’ll explore how this works in more detail, but for now just accept that this WINDOW pointer, which we’re storing in the variable window1, is how you will access the window.

The box function in line 18 draws a box around the edge of the specified window. You can specify which characters to use to draw the box using the second and third parameters. Setting these to 0, as we’ve done, uses the default characters.

Line 19 introduces another new function, mvwaddstr. This expects to be passed a pointer to a window, the y and x coordinates and an unformatted string. It then writes the string to the specified window at those coordinates. Note that all the other coordinates we’ve used so far have been relative to the top left corner of stdscr. When we use functions that output to a window, coordinates are relative to the top left corner of the window, not the screen.

Any output you send to a window is written to that special area of memory we talked about, but you won’t see that output until you add the window’s content to the screen. This is done using the wrefresh function on line 20.

If you run this code you’ll see this:

What happens when we add a second window to the mix? Add the following lines of code after the line that calls the wrefresh(window1); function.

C

1

2

3

4

WINDOW*window2=newwin(10,15,8,20);

box(window2,0,0);

mvwaddstr(window2,4,3,"window 2");

wrefresh(window2);

When you run the code again, you’ll see that part of window 1 is now covered by window 2:

When two or more windows overlap like this, what you see in the overlapping area depends entirely on the order in which the windows are refreshed. You can verify this for yourself, by moving line 20 (wrefresh(window1);) so that it comes after line 25 (wrefresh(window2);). If you do this and run again, you’ll see that window 1 now appears in front of window 2.

Now add another wrefresh(window2); function call after the first two so you have:

C

1

2

3

wrefresh(window2);

wrefresh(window1);

wrefresh(window2);

So this should show window2, then show window1 on top of window2, and finally show window2 on top of window1, so the net effect is that window2 will appear on top right? If you run it you might be surprised to see that window1 is still on top:

What’s going on? Well, whenever you write something to a window, NCurses marks that window as having been changed. Each time you call wrefresh for that window, it marks the window as updated and unchanged. So when it receives the second wrefresh call for window2, it looks at the window and sees that nothing has changed since the last wrefresh and decides there’s no need to redraw the window!

So what do you do if you want to draw window2 on top again, even if nothing new has been written to the window? Well there is a workaround. Add the following line of code just before the second call to wrefresh(window2);:

C

1

touchwin(window2);

If you now run the code again you’ll see that window2 is back on top. The touchwin function marks the window as if it had changed. Now, when the second call to refresh happens, NCurses sees that the window needs to be updated and writes it back to the screen.

If you had to manage several different stacked windows, you can see that trying to keep track of which order they need to be refreshed in and which ones had been changed or not changed would soon become messy.

Fortunately there is a companion library to NCurses, the panel library, that is intended to take the headache out of exactly this sort of window management. Panels add a capability to give windows a precise depth order (also known as a z-order) and to draw them correctly based on this order.

You add this library in exactly the same way you did NCurses, but this time the library you need to add is called libpanel.dylib. With the panel library linked to the project, update your code to look like this:

C

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

//

// main.c

// Hexapawn

//

#include <ncurses.h>

#include <panel.h>

intmain(void)

{

// Start ncurses

initscr();

noecho();

curs_set(0);

refresh();

// Create first window and link it to a panel

WINDOW*window1=newwin(10,15,5,12);

box(window1,0,0);

mvwaddstr(window1,4,3,"window 1");

wrefresh(window1);

PANEL*win1panel=new_panel(window1);

// Create second window and link it to a panel

WINDOW*window2=newwin(10,15,8,20);

box(window2,0,0);

mvwaddstr(window2,4,3,"window 2");

wrefresh(window2);

PANEL*win2panel=new_panel(window2);

intc,order=1;

do

{

c=getch();

if(c=='s')

{

top_panel(order?win1panel:win2panel);

update_panels();

doupdate();

order=!order;

}

}while(c!='x');

// End ncurses

endwin();

return0;

}

In line 7, we are now also including the header for the panel library. In lines 17 to 21 we create window1, draw it and refresh it as before. In line 22 we create a panel for that window. In lines 24 to 29 we do the same for window2.

The routine to read the keyboard, in lines 31 to 44, still checks for the user pressing x to exit, but it now also checks for the s key being pressed to switch the order of the windows. The order variable, keeps track of which window is on top. If it’s 1, then window1 is on top, and if it’s 0 then window2 is on top. Line 41 swaps the order each time s is pressed.

The top_panel function in line 38 brings the specified panel to the top of the stack. The update_panels function in line 39, redraws all the windows according to the current order of the panels and do_update writes all these changes to the actual screen.

If you run this code, you can swap the order of the windows simply by pressing s. Note that the windows are redrawn correctly, even though they have not been changed since they were last drawn. When you get bored, press x to exit.

Now that you have a solid grounding in how NCurses works, in the next part we’ll start using it to build a user interface.

By laurence October 25, 2015 - 1:02 pm

NOTE: if you are trying to follow this after upgrading to OS X El Capitan, you might find that Xcode refuses to launch the Terminal app. If you experience this, please follow my instructions here: http://laurencescotford.co.uk/?p=1312

By laurence October 25, 2015 - 3:27 pm

NOTE: If you are trying to follow this using Xcode 7 or later you will find that the dylib files have now been replaced with tdb files. You should link libncurses.tbd and libpanel.tbd instead of libncurses.dylib and libpanel.dylib.