Skip to content

Games programming from the ground up with C: Building the user interface 1

You can see an index of all the posts in this series: go to index.

If you want to review where I’ve got up to, you can download the code from the end of the previous post here: introducing ncurses

In the previous post I linked the ncurses and panel libraries to the Hexapawn project and I showed how windows and panels work in ncurses. In this part I’ll start using these features to build a text-based user interface for the game.

I could just build the windows I need manually, but since I will have quite a few different windows to build on this project, it makes sense to create some generalised functions to manage them. As this project will grow to be a lot bigger than either of the two previous games I’ve worked on, another thing I’ll do is start organising functional areas of the game into different files. I’ll start by creating a new file to hold my windows-related functions.

In the c-games-hexapawn directory, I created two new empty files called hexwindows.c and hexwindows.h. The latter file is a header file, and I’ll talk more about these later.

I moved the ncurses initialisation code into its own function. This is the code I added to hexwindows.c.

//
//  hexwindows.c
//  Hexapawn
//
//  Functions for managing the windows in Hexapawn
//

#include <ncurses.h>
#include <panel.h>
#include "hexwindows.h"

// Initialise Curses
void initialise_curses(void)
{
  initscr();              // Start ncurses
  cbreak();               // Disable line buffer
  noecho();               // User input will not be echoed to screen
  keypad(stdscr, TRUE);   // Enable special keyboard characters
  curs_set(0);            // Switch off the cursor
  refresh();              // Draw stdscr (workaround for windows bug)
}

You’ll notice I’ve taken the opportunity to add a couple more function calls to my initialisation code. Normally the characters I type as input are placed in a buffer and are not made available to the program until the return key is pressed. Line 16, cbreak(), disables this behaviour so that characters are available to the program immediately that they are typed. Line 18, keypad(stdscr, TRUE), causes special keyboard keys, such as function keys and arrow keys to be returned as a single value in the input stream. This will make it easier for me to implement my user interface because I won’t have to interpret the escape sequences that are normally generated by these keys.

OK, let me try my new function out. I opened hexapawn.c and replaced lines 11 – 14 (i.e. the lines starting with the call to initscr and ending with the call to refresh) with the following single line:

initialise_curses();

Now I saved the file and tried compiling it and immediately got an error:

A compiler error when trying to use a function in an external file.
Compiler error when trying to use a function from our hexwindows.c file

It’s complaining about an implicit declaration of the function initialise_ncurses. What’s going on? Well the problem is the function is in a different C file, so hexapawn.c can’t see it. You’ll recall from the earlier parts of this series that I got around issues of function visibility by adding a function declaration to the top of the file. But which file should I add it to? I can’t add it to hexwindows.c because I’ve already established that hexapawn.c can’t see the contents of that file. I could add it to the top of hexapawn.c. That would work, but what would happen if I wanted to use that function in another C file that I create later? I’d have to add the declaration there as well.

The solution is to use something called a header file. A header file is a file that contains all the information about a source file, including function declarations, that I want to share with other parts of my program. I created the header file for hexwindows.c by opening hexwindows.h and adding the following code:

//
//  hexwindows.h
//  Hexapawn
//
//
//  Functions for managing the windows in Hexapawn
//

#ifndef __Hexapawn__hexwindows__
#define __Hexapawn__hexwindows__

void initialise_curses(void);

#endif /* defined(__Hexapawn__hexwindows__) */

You should recognise line 11 as being the function declaration for intialise_curses, but what’s all that stuff either side of it? The lines beginning with # are called preprocessor directives. Their purpose is to modify a source file before it is compiled. I’ve already been using another pre-processing directive, #include, which I used to include the C libraries I am using.

The #ifndef directive checks to see if a named symbol has been defined. If the symbol has not been defined then the code between the #ifndef and matching #endif directives will be compiled. If the symbol has already been defined then that code won’t be compiled. The symbol name in this case is __Hexapawn__hexwindows__ (note that the characters either side and in the middle of the two words are pairs of underlines – this is a naming scheme I’ve unashamedly stolen from Apple because it works well for me). Note that if the symbol has not been defined, the first thing I do is define it with the #define directive.

If I continue building my program you’ll see why I need this. I saved the header file and opened hexapawn.c to add the following line at the top of the file, underneath the other #include lines:

#include "hexwindows.h"

Note that when I am including header files that I have created, I use quotes to surround the filename, rather than the angle brackets I’ve been using to include library header files. You can see that this effectively adds the contents of the header file (which at the moment is just the initialise_curses function declaration, to the top of the hexapawn.c file. This means that when the compiler reaches the call to initialise_curses, it won’t throw the implicit declaration error I saw earlier.

As my programs get more complex, I might have a situation where a header file also includes other header files. For example. I might have a file some_useful_functions.h, which also includes hexwindows.h, If I was to later include some_useful_functions.h in hexapawn.c I’d end up including hexwindows.h twice, which would cause an error, because I’d have two function declarations for initialise_curses.

Diagram showing how a header file can inadvertently be included twice in single C file.
A potential error when a header file is inadvertently included twice

The reason I added those preprocessor directives to my header file was to guard against just such a possibility. In this situation, the first time the header is included, the __hexapawn__hexwindows__ symbol does not yet exist, so the symbol is defined and the function declaration is compiled. The second time the header file is included, the symbol will already have been defined, so the code between the #ifndef and #endif directives will not be compiled. The net result is that, even though the header file is included twice, the function declaration will only be compiled once.

There’s one more step I must take to get my program working correctly, which is to compile hexwindows.c as well as hexapawn.c. To compile the program now I’d have to use the following command:

gcc hexapawn.c hexwindows.c -l ncurses -l panel -o ../hexapawn

Phew! My compilation command is starting to get a bit long-winded – and I’ll have more files to add in future! There’s got to be an easier way. And there is. It’s called a makefile. In my c-games-hexapawn directory, I created a new file called makefile with the following content:

# makefile for Hexapawn

.DEFAULT_GOAL := build

build:
    @echo "Building Hexapawn..."
    gcc hexapawn.c hexwindows.c -l ncurses -l panel -o ../hexapawn

This file will be used by the make program. When I run make it will look for a makefile in the current directory. The makefile contains a set of rules to control various build processes in my project. A rule has two parts, a target and a recipe. Lines 5-7 in my file show a typical rule.

The target is build (note that targets are always followed by a colon) and the recipe is the two lines that follow.

The first of these @echo “Building Hexapawn…” causes the string to be output to the terminal. The command to do this is just echo. The reason I have the @ in front is because normally, make will display each command in a recipe, but the @ suppresses this behaviour for the current line. Without it I’d see a somewhat redundant echoing of the echo command before I see the echoed string!

The output of make if I don't suppress the echoing of the echo command.
Output from make if I don’t use @to suppress echoing of the echo command!

The second line of the recipe is just my normal compilation command.

I can use make to execute a particular target in my makefile by using the name of the target, so make build would execute the rule that has build as a target. I can also specify a default goal, which I do on line 3. This specifies the rule that will execute if I use make without a target. So entering just make will also execute the rule that has build as a target.

One other thing to note about the makefile is that the # on line 1 indicates that this line is a comment.

I’ll try this out. I save my file, and then, from inside the c-games-hexapawn directory, I entered make to see this:

Output when using make with a makefile.
Using a makefile to compile our code.

That’s going to save me a lot of typing moving forward, and when I add new files I can simply update my makefile.

When I now run the program it executes as before.

What’s next? Well ideally I want to create another function that will create a game window for me. Let me think about what that function should do:

  • It should accept the dimensions for the new window as parameters
  • It should create a new window to those dimensions
  • It should set the window up to accept special keys as input (e.g. function keys, arrow keys)
  • It should draw a border round the edge of the window
  • It should create a new panel and associate it with the window
  • It should return a pointer to the panel and the window

That all looks absolutely fine except for the last bit. How can I return more than one value from a single function? I’ll let you into a little secret here – there is a way of doing it, and I’ll let you know what that is later on. But for now I’ll explore a different solution.

You might recall from an earlier part of this series that I introduced the idea of referring to an object in memory by using using its address rather than its name. This is called indirection. I used this when I wanted to modify an existing string in a function. I simply passed a pointer to the string to that function. The function was then able to access the string directly so it could modify it. So why don’t I take the same approach here? If I create a pointer to a window and a pointer to a panel outside of the function, I can simply pass the addresses of those pointers to the function so it can modify them directly.

In my hexwindows.h file, just beneath the existing function declaration, I added the following new declaration:

void create_basic_window(WINDOW ** window, PANEL ** panel, int height, int width, int y, int x);

And added a new function to hexwindows.c that looked like this:

// Create a basic window with an associated panel and a border
void create_basic_window(WINDOW ** window, PANEL ** panel, int height, int width, int y, int x) {
  // Create the window
  *window = newwin(height, width, y, x);
  
  keypad(*window, TRUE);
  
  // Add the window to a panel
  *panel = new_panel(*window);
  
  // Display a border around the window
  box(*window, 0, 0);
}

Woah! What’s the deal with those double asterisks? Wasn’t I just going to pass through the addresses of the window and panel pointers? Well yes, but let’s think about what’s going on here. When I create a new window, e.g.

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

what gets returned by the newwin function and stored in window is a pointer to the data for that window (in other words, the address of the data). If we were to pass that value directly to a function, e.g.:

void my_function(WINDOW * win_ptr) {
  .
  .
  .
}

then the function is getting is the address of the window data:

Diagram showing that passing a WINDOW pointer directly into a function leads to the wrong level of indirection.
Passing a pointer directly leads to the wrong level of indirection

This is not what I want. I want my function to modify the pointer variable itself, not the data it points to. So what I need to pass to my function is the address of the pointer variable, in other words a pointer to the pointer variable!

A diagram showing how to pass the WINDOW variable into a function.
The right level of indirection is to pass a pointer to the WINDOW variable itself

Now that I have the address of the pointer variable in the function, how do I read or write the value inside it, for example, when I want to set it the address of some new window data I’ve just created? You would be forgiven for thinking I could just do this:

win_ptr = newwin(height, width, y, x);

But this wouldn’t work, because this line is asking C to take a pointer to a pointer to some window data and turn it into an ordinary pointer to some window data:

A diagram showing why using a pointer to a pointer as if it's a pointer to data won't work.
Using the variable incorrectly by treating it like a direct pointer to data

But actually I want to modify the content of the pointer, and I can get to that content by using the asterisk operator in a slightly different way:

*win_ptr = newwin(height, width, y, x);

Which has this effect:

Diagram showing the correct way to use a pointer to a pointer.
The correct way to use a pointer to a pointer

Don’t worry if you are struggling to follow this. As I’ve said before, pointers are the most difficult aspect of C to get to grips with, especially when you start dealing with multiple levels of indirection. Stick with it, and one day it will all just click into place. I’ll be showing plenty more example of this during the rest of this series. What often works for me, if something doesn’t make sense, is to try drawing a diagram of what’s happening, like those above.

Let’s now go back to my new function and see what it’s doing. Line 26 is creating a new window and making my window pointer point to it. In line 28 I set the window up to accept special keys. In line 31 I create a new panel and associate it with the new window, I then make my panel pointer point to this panel. Finally in line 34 I draw a box around the window. Notice how I use the * operator everywhere I want to refer to the content of the pointer.

OK, so how do I use this new function? Back in hexapawn.c I changed the code that creates and displays the windows to this:

  // Create first window and place it in a panel
  WINDOW * window1;
  PANEL * win1panel;
  create_basic_window(&window1, &win1panel, 10, 15, 5, 12);
  mvwaddstr(window1, 4, 3, "window 1");
  wrefresh(window1);
  
  // Create second window and place it in a panel
  WINDOW * window2;
  PANEL * win2panel;
  create_basic_window(&window2, &win2panel, 10, 15, 8, 20);
  mvwaddstr(window2, 4, 3, "window 2");
  wrefresh(window2);

Note that I am using the address-of operator (&) to pass through the address of the window and panel pointers rather than their content. If I forget to include this operator, I’ll get an error because the compiler is expecting a pointer to a pointer here.

When I build the program with make i can see that it functions just as before.

In the next part of this series I’ll continue to build my interface by creating the first of my actual game windows. In the meantime, here’s what I’ll be challenging myself to do. Like all the other main windows in the game, the game window, in which you play a game against the computer, will be 24 rows high by 80 characters wide and will be positioned at the top-left corner of the terminal window. It will have a border, and a title – “Hexapawn: Play a game”. I should now be able to build this window using the functions I have created so far. I’ll look at my solution in the next post.

Published inGamesProgramming

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *