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:
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.
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 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:
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:
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!
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:
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:
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.
Be First to Comment