Skip to content

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

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: building the user interface 1.

In the previous post, I started building the functions to manage the game windows within Hexapawn. In this post I’ll continue with that. I also left myself with a challenge  – to build my first window – the game window where the user will play games against the computer.

My main function in hexapawn.c now looks like this:

//
//  main.c
//  Hexapawn
//

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

int main(void) {
  // Start ncurses
  initialise_curses();
  
  // Create the game window
  WINDOW * game_window;
  PANEL * game_panel;
  create_basic_window(&game_window, &game_panel, 24, 80, 0, 0);
  mvwaddstr(game_window, 1, 3, "Hexapawn: Play a game");
  wrefresh(game_window);
  
  while (getch() != 'x');
  
  // End ncurses
  endwin();
  
  return 0;
}

When I build this with make and run it, I see this:

How the Hexapawn Play a game window appears.
How the Hexapawn Play a game window appears

One thing to note about interfaces in ncurses is that everything is built with text. Don’t be fooled into thinking that the line around the edge of the window is drawn with some kind of graphics library, it’s just a series of extended characters that have been created for this sort of purpose. Having said that, ncurses does make a number of functions available for “drawing” with these characters. I’ll use some of these now to draw a line underneath the title. I added these lines just before the call to wrefresh.

  mvwaddch(game_window, 2, 0, ACS_LTEE);
  mvwhline(game_window, 2, 1, ACS_HLINE, 78);
  mvwaddch(game_window, 2, 79, ACS_RTEE);

You should be starting to see a pattern to ncurses function names and arguments by now. All three of these functions direct their output to a particular window, so they want a pointer to that window as the first argument. They also start by moving the cursor to the specified y and x coordinates in the second and third arguments. The function mvwaddch adds a single character at that point. The function mvwhline draws a horizontal line of characters, made using the character specified as the fourth argument to the length set in the fifth argument.

You may be wondering what ACS_LTEE, ACS_HLINE and ACS_RTEE are. These are those special drawing characters I mentioned earlier and they are all defined in curses.h (although I don’t explicitly link to this file, ncurses.h gets it for me). When I save the file, build and run the code I now see a neat line underneath your title, like this:

The Hexpawn Play a game window showing line drawing with special characters.
The Hexpawn Play a game window showing line drawing with special characters

Even though I’ve only got a few lines of code so far, I’ve already got lots of “magic numbers” littered throughout my code. Magic numbers are number literals that I use in an ad hoc fashion in my code, such as the numbers I use to set window sizes and line lengths etc. Having a lot of these throughout my code is bad practice. Let’s say I decide to change the width of the windows used in the game to 60 instead of 80. I’d have to search through my code for every instance where I’d initialised a window and make the same change. Let me fix that now.

I’ll collect all these numbers into one place where I will be able to edit them easily. To make life easy, I’ll create a new header file that will hold all the constant values used in the game.

In the directory with my other C files, I created a new file called hexapawn.h with the following content:

//
//  hexapawn.h
//  Hexapawn
//
//  Common definitions and values for Hexapawn
//

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

#ifndef __Hexapawn__hexapawn_h
#define __Hexapawn__hexapawn_h

#define BORDER_WIDTH 1
#define DIVIDER_X 0
#define DIVIDER_Y 2
#define TITLE_X 3
#define TITLE_Y 1
#define WIN_MAIN_HEIGHT 24
#define WIN_MAIN_WIDTH 80
#define WIN_MAIN_X 0
#define WIN_MAIN_Y 0

#endif // defined(__Hexapawn__hexapawn_h)

#include "hexwindows.h"

You’ll notice that, since I’ll include this file in every C file I create for Hexapawn, I’ve also used it to include the other header files I’ll always need.

Previously I’ve used the #define preprocessor directive to define a symbol. There’s an example on line 12. On lines 14 to 22, I am using #define in a different way to create something known as a macro. To create a macro, I follow the symbol name with a space and then a value. The way macros work is that, whenever the compiler encounters the symbol name in my source code, it will substitute the value for this symbol. So, instead of having my code littered with the number 24 whenever I want to specify a window height, I can instead just insert the symbol name, WIN_MAIN_HEIGHT, and the compiler will replace it with the correct value at compilation time.

While I’m getting constant values out of my C files and into a header file where I can find and amend them easily, I should also do the same thing with string literals. I created another header file and called this one hexstrings.c. It has the following content to begin with:

//
//  hexstrings.h
//  Hexapawn
//
//  Contains all the string literals for Hexapawn
//

#ifndef __Hexapawn__hexstrings_h
#define __Hexapawn__hexstrings_h

#define STR_GAME_SUBTITLE "Play a game"
#define STR_MAIN_TITLE "Hexapawn: "

#endif // defined(__Hexapawn__hexstrings_h)

Note that on line 12 there is a space at the end of the string, after the colon. Also note that string literals must be enclosed in quotes, or I will get a compiler error.

You might be wondering why I have created a separate header file for strings. For projects like this that I am just creating as a learning experience or for my own amusement, it doesn’t matter that much. But if I ever go on to create something that I might want to translate to create a French or German or Spanish version etc., I’ll be mighty glad I have just a single file containing all the strings that need translating. So it’s a good habit to get into.

By the way, both of these files are tiny at the moment, but they will both grow quite large, so I want to take the trouble to keep them organised alphabetically – or by some other scheme where I can find individual items easily.

To illustrate how these files will be used, I’ll make some modifications to hexwindows.c. All of mymain windows are going to have the same format, so it makes sense to have a function to create them all in the same way.

First, I replaced the current line to #include “hexwindows.h” with the following two lines:

#include "hexapawn.h"
#include "hexstrings.h"

Now, at the bottom of the file, I added the following new function:

// Create a main window
void create_main_window(WINDOW ** window, PANEL ** panel, const char * subtitle) {
  // Create the window and panel
  create_basic_window(window, panel, WIN_MAIN_HEIGHT, WIN_MAIN_WIDTH, WIN_MAIN_Y, WIN_MAIN_X);
  
  // Add a title and a divider between title and content area
  mvwprintw(*window, TITLE_Y, TITLE_X, "%s%s", STR_MAIN_TITLE, subtitle);
  mvwaddch(*window, DIVIDER_Y, DIVIDER_X, ACS_LTEE);
  mvwhline(*window, DIVIDER_Y, DIVIDER_X + 1, ACS_HLINE, WIN_MAIN_WIDTH - BORDER_WIDTH * 2);
  mvwaddch(*window, DIVIDER_Y, DIVIDER_X + WIN_MAIN_WIDTH - 1, ACS_RTEE);
}

You can see that this is largely the same as the code I used originally in hexapawn.c. The main difference is that I am now using a formatted string to assemble the window’s title from two different strings. One other difference you might have noticed is that I am now passing window and panel directly to create_basic_window without prefixing them with &. This is because the values passed to create_main_window are already pointers to pointers – I don’t need to convert them to an address again.

I’ll also need to add the function declaration to hexwindows.h just beneath the existing function declaration for create_basic_window:

void create_main_window(WINDOW ** window, PANEL ** panel, const char * subtitle);

Finally I can now modify my hexapawn.c file to use both the new header files and my now function:

//
//  main.c
//  Hexapawn
//

#include "hexapawn.h"
#include "hexstrings.h"

int main(void) {
  // Start ncurses
  initialise_curses();
  
  // Create the game window
  WINDOW * game_window;
  PANEL * game_panel;
  create_main_window(&game_window, &game_panel, STR_GAME_SUBTITLE);
  wrefresh(game_window);
  
  while (getch() != 'x');
  
  // End ncurses
  endwin();
  
  return 0;
}

You can see that my code is now a lot cleaner and easier to understand without all those magic numbers. It’s also less error-prone because whenever I need to change a value, I only need to change it one place.

At this point I built and ran the code again to make sure everything was still working.

Now I could repeat the code above for each of the other main windows, but actually that still seems like a lot of effort. Ideally what I’d like to do is somehow tell hexwindows.c to go ahead and build all the windows for the game with a single function call. To be able to do that, hexwindows.c needs to keep track of a few things about each window, including the pointers for the window and panel. Although I’ve only created a main window so far, I know from my design that there will be at least one other type of window – a dialogue window – so I probably also need to keep track of the window category and I’ll need some unique way of referencing each window.

From what you’ve seen of C so far in this series, you’re probably thinking that arrays are a good way to go. For example, if NUM_WINDOWS held the number of windows I could have something like this:

WINDOW * windows[NUM_WINDOWS];
PANEL * panels[NUM_WINDOWS];
int window_cats[NUM_WINDOWS];

Each window would then have a unique id, which would be the integer values starting from 0. These would effectively be indexes into the arrays. For example, let’s say the ID for the game window was 1 and I wanted to display some text on that window. I could do something like this:

waddstr(windows[1], "Some text");

While this scheme would work, there are some fundamental problems with it. Foremost among these is that I am effectively creating three independent data groups. I have no way of enforcing the relationship between those groups, which means it’s easy for them to get out of sync, causing us all sorts of debugging headaches. Wouldn’t it be nice if there was some way I could keep all the data for each window together? Fortunately there is, and it’s called a struct.

A struct is a complex data type that groups together several items of data as a single, addressable unit. A struct needs to be defined before it can be used. The definition of a struct to represent one of my window objects might look like this:

struct hexwindow {
  int uid;
  int w_cat;
  WINDOW * w_ptr;
  PANEL * p_ptr;
  const char * subtitle;
};

This struct contains five different pieces of data. Note that structs are not fussy about how I mix and match data types – as long as it’s a valid data type, I can put it in a struct. Note that each item ends with a semicolon, and the entire definition also ends with a semicolon.

To create a new variable using this definition (assuming that the ID is 1, the category is 0, I’ve already created window and panel pointers in game_win_ptr and game_panel_ptr respectively, and my subtitle is a string literal defined as STR_GAME_SUBTITLE), I would do this:

struct hexwindow gamewin = {1, 0, game_win_ptr, game_panel_ptr, STR_GAME_SUBTITLE};

Now that I have my struct declared and initialised, I can access individual elements within it using the dot operator (.), e.g.:

gamewin.w_ptr = newwin(24, 80, 0, 0);
int current_win = gamewin.uid;

I can also pass around the entire structure as a unit. You might remember earlier I promised to tell you how a function could return multiple values – well here’s how – return them as a struct, e.g.:

struct hexwindow create_window(void) {
  struct hexwindow new_window;

  .
  .
  .

  return new_window;
}

One other thing I need to consider is how to generate unique ids for my windows. I could just do this manually, but again this leaves me open to same nasty bugs. Fortunately it’s C to the rescue again with yet another data type, the enumerator, or enum. An enum type will automatically generate a sequence of unique values. This is exactly what I need for my window uids.

A definition for an enum that generates uids for my main windows looks like this:

enum window_id {
  WIN_MAIN_MENU,
  WIN_INSTRUCTIONS,
  WIN_GAME,
  WIN_AI,
  WIN_HISTORY,
  WIN_SAVE,
  WIN_LOAD
};

I can then obtain the individual values in the enum simply by using the relevant label, e.g.:

struct hexwindow gamewin = {WIN_GAME, 0, game_win_ptr, game_panel_ptr, STR_GAME_SUBTITLE};

Ok let me implement this plan. I added the following code to the bottom of hexapawn.h, just before the #endif directive:

enum window_id {
  WIN_MAIN_MENU,
  WIN_INSTRUCTIONS,
  WIN_GAME,
  WIN_AI,
  WIN_HISTORY,
  WIN_SAVE,
  WIN_LOAD
};

enum win_cat {
  WT_MAIN
};

struct hexwindow {
  enum window_id uid;
  enum win_cat w_cat;
  WINDOW * w_ptr;
  PANEL * p_ptr;
  const char * subtitle;
};

This is almost the same as my earlier version except that I have now substituted enum window_id for int as the data type for uid and enum win_cat instead of int for the category. Note that my win_cat enum only has a single value right now, but I’ll add others as I implement them.

I can now add the data for my windows. First I add some subtitles to hexstrings.h by updating the existing string definitions to this:

#define STR_AI_SUBTITLE "Browse AI"
#define STR_GAME_SUBTITLE "Play a game"
#define STR_HIST_SUBTITLE "Browse game history"
#define STR_INSTRUCTIONS_SUBTITLE "Instructions"
#define STR_LOAD_SUBTITLE "Load game history and AI"
#define STR_MAIN_TITLE "Hexapawn: "
#define STR_MENU_SUBTITLE "Menu"
#define STR_SAVE_SUBTITLE "Save game history and AI"

I added the following to the top of hexwindows.c underneath the #include directives:

static struct hexwindow hexwindows[] = {
    // Main menu
    {WIN_MAIN_MENU, WT_MAIN, NULL, NULL, STR_MENU_SUBTITLE},
    // Instructions
    {WIN_INSTRUCTIONS, WT_MAIN, NULL, NULL, STR_INSTRUCTIONS_SUBTITLE},
    // Game
    {WIN_GAME, WT_MAIN, NULL, NULL, STR_GAME_SUBTITLE},
    // AI
    {WIN_AI, WT_MAIN, NULL, NULL, STR_AI_SUBTITLE},
    // History
    {WIN_HISTORY, WT_MAIN, NULL, NULL, STR_HIST_SUBTITLE},
    // Save
    {WIN_SAVE, WT_MAIN, NULL, NULL, STR_SAVE_SUBTITLE},
    // Load
    {WIN_LOAD, WT_MAIN, NULL, NULL, STR_LOAD_SUBTITLE}
};

Note that the structs for the windows are stored in an array. Given that this is the case, you might wonder why I don’t just use the array index as the uid. Using the index means I have to store the window data in a particular order, and this is a scheme that can be easily broken. Having a unique id that’s stored as an integral part of the data means that individual structs can change positions within the array, but I am still able to uniquely identify them.

Also note that I’ve initialised the window and panel pointers to NULL. That’s because I won’t know what these addresses are until I create the windows and panels – at the moment all I need is somewhere for these pointers to be stored.

Now I need a new function that will initialise all the windows. This function should loop through the array, creating each window in turn. One thing that complicates this task a little is that C has no built in function to return the number of elements in an array. Now I could get around this by manually keeping track of how many windows I have and using #define to create a macro with that amount, but that leaves me open to bugs if I forget to update the macro when I add new windows. A better approach would be to build my own equivalent of an array size function.

How will this work? I can make use of a very useful C operator sizeof. When passed an object or a datatype as its operand, this operator will return the size of that object in bytes. I can use this to find the total size in bytes of the array. If I then use sizeof to find the size of a single object in the array, I simply divide the first by the second to determine how many elements are in the array.

Because this is a very simple calculation it’s hardly worth the overhead of calling a function. Instead I will use a new type of macro – one that accepts parameters. I added the following to the bottom of hexapawn.h, just above the #ENDIF directive:

// Macro to find size of array
#define ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))

Note that, unlike a simple macro, the symbol name here is immediately followed by parentheses containing an argument name. Just like argument lists for functions, I can separate multiple arguments with commas. So, for example, if, for some reason, I wanted to create a less efficient version of my macro above, so that I had to explicitly provide the element to be divided by, it would look like this:

#define ARRAY_SIZE(x, y) (sizeof(x) / sizeof(y))

Unlike C functions, I don’t specify a type for the arguments in a macro. The C pre-processor treats all arguments simply as text to be substituted.

Now I can use this macro in my function to initialise the windows. I added this function to hexwindows.c. From now on I won’t always mention that I’m adding the function declaration to the header file, so assume that I’ll do this for each new function I add. If I see a compiler error complaining about an implicitly declared function, it’s probably because I’ve forgotten to add the declaration to the relevant header file.

// Create all the permanent windows
void initialise_windows(void) {
  for (int i = 0; i < ARRAY_SIZE(hexwindows); i++) {
    switch (hexwindows[i].w_cat) {
      case WT_MAIN:
        create_main_window(&hexwindows[i].w_ptr, &hexwindows[i].p_ptr, hexwindows[i].subtitle);
        break;
    }
  }
}

When the GCC compiler encounters ARRAY_SIZE(hexwindows) it expands the macro, substituting hexwindows wherever x appears in the macro. In effect the first line of the for loop will be interpreted like this:

for (int i = 0; i < (sizeof(hexwindows) / sizeof(hexwindows[0])); i++)

One thing to note about the macro is that the entire expression is enclosed in parentheses. C doesn’t require that I do this, but I always should, because it will ensure that, when the macro is used in an expression, the entire macro is evaluated in isolation before the result is used in the expression.

Note that I have used a switch expression that has only a single case at the moment, but I will add further cases as I introduce other window categories.

You may have noticed that I defined my array using the keyword static. If I had defined it without using static the array would have had program scope. This means that the variable is visible across the entire program.  By using the keyword static, I restrict the variable to file scope. This means the variable is visible from the point it is defined to the end of the file that contains it.

By doing this, the only way to access the array outside of hexwindows.c is via the functions I create in hexwindows.c. It’s always a good idea to constrain data to the minimum necessary scope as it reduces the possibility of errors.

Next I’ll need a function to show a particular window. External functions will request a particular window by passing the window’s uid, which means I will also need a helper function that returns a pointer to the relevant hexwindow struct for a given uid. I added this function to hexwindows.c:

// Return a pointer to struct that matches the uid or NULL if no match is found
struct hexwindow * get_hexwindow(enum window_id win_id) {
  for (int i = 0; i < ARRAY_SIZE(hexwindows); i++) {
    if (hexwindows[i].uid == win_id) {
      return &hexwindows[i];
    }
  }
  return NULL;
}

One thing you should note about this function and the way I have uniquely identified our windows, is that this is not a particularly efficient search mechanism. In the worst case it will iterate the entire array before finding a valid uid. However, because my array is never going to grow particularly large, iterating the array will never be too expensive time-wise and the simplicity of this function is a good trade-off.

Having created all those windows is not much use unless I can show them. Let me add another function to do that:

// Show the specified window
void show_window(enum window_id win_id) {
  struct hexwindow * hex_win = get_hexwindow(win_id);
  
  // Update the window
  wrefresh(hex_win->w_ptr);
  
  // Show the panel
  top_panel(hex_win->p_ptr);
  update_panels();
  doupdate();
}

First this function gets a pointer to the relevant hex window struct using the lookup function I just created, then I refresh the window to ensure it’s up to date. The top_panel function brings the specified panel to the front. The update_panels function redraws all the panels in the correct order. Finally the doupdate function compares the physical screen to the virtual screen and draws the virtual screen to the physical screen if there are differences.

You will have seen that, instead of using the dot operator (.) to access members of the struct, I’ve started to use a new operator (->). This is because I am no longer referencing the struct directly, but rather a pointer to the struct, so the dot operator has no meaning here, as a pointer has no members – only the struct that it points to does. The arrow (or deference operator) is a shorthand way of telling the compiler, “hey, I want to dereference this pointer and then get this member from the dereferenced struct”. In other words, it’s shorthand for doing this:

wrefresh((*hex_win).w_ptr);

Again, don’t worry if you are struggling with this concept at the moment. The important thing to remember is that I use when I am using the struct directly and -> when I am using a pointer to the struct.

OK, it’s finally time to put these functions to good use in Hexapawn and create all my main windows. I edited the main function in hexapawn.c to look like this:

int main(void) {
  // Start ncurses
  initialise_curses();
  
  // Build the game windows
  initialise_windows();
  
  show_window(WIN_MAIN_MENU);

  int c;
  
  while ((c = getch()) != 'x') {
    switch (c) {
      case 'm':
        show_window(WIN_MAIN_MENU);
        break;
      case 'i':
        show_window(WIN_INSTRUCTIONS);
        break;
      case 'g':
        show_window(WIN_GAME);
        break;
      case 'a':
        show_window(WIN_AI);
        break;
      case 'h':
        show_window(WIN_HISTORY);
        break;
      case 's':
        show_window(WIN_SAVE);
        break;
      case 'l':
        show_window(WIN_LOAD);
        break;
    }
  }
  
  // End ncurses
  endwin();
  
  return 0;
}

Note that in the condition for the while loop, I am doing two things. First I am waiting for a keypress and getting the value into the c variable with (c = getch()). You’ll recall from previous posts that if I use an assignment statement within a conditional expression, it evaluates to the value of the assignment. So the second thing I am doing here is checking that the key pressed was not equal to ‘x’. This part of the expression is equivalent to (getch() != ‘x’).

I saved my file and rebuilt everything with make. I always look out for implicit declaration errors, as it’s probably because I’ve forgotten to add a function declaration for one of the new functions in hexwindows.c, so I check my hexwindows.h file to make sure they are all there.

When I ran the program, I saw the window for the Main menu. I could now press a key to switch between any of the main windows:

  • m: main menu
  • i: instructions
  • g: play a game
  • a: browse AI
  • h: browse history
  • s: save AI and game history
  • l: load AI and game history

When I’d satisfied myself that all the windows were there and with the correct titles, I ended the program by pressing x.

Now that I have my core windows management code in place, in the next post I will build the content for my first window: the main menu.

Published inGamesProgramming

Be First to Comment

Leave a Reply

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