4 Hugues Ross - Blog: Let's Make a Roguelike - 2 - Beginning the Project
Hugues Ross

12/29/15

Let's Make a Roguelike - 2 - Beginning the Project

Now that we've discussed some of the prep work we need to actually set up and create our project. In this tutorial, we'll be setting up a title screen and starting menus. This project assumes that you've set up your work environment how you want to and made a fresh project, so if you haven't done that yet, then go do that now.

Drawing Text

Now, we're going to get into some actual code. We start by setting up NCurses:
    1 #include <curses.h>
    2 
    3 int main(int argc, char* argv[])
    4 {
    5     /* Start Curses
    6      *
    7      * clear()  clears the terminal, giving us a blank canvas to work with
    8      * noecho() prevents getch() calls from displaying the typed character
    9      * cbreak() ensures that typed input is received immediately by curses
   10     */
   11     initscr();
   12     clear();
   13     noecho();
   14     cbreak();
   15 
   16     /*
   17      * The program body will go here.
   18      */
   19 
   20     // End Curses
   21     endwin();
   22     return 0;
   23 }
If you try running this now, you won't see any output because all we're doing is setting up and shutting down. Let's fix that next.

A Centered Title

To start things off nice and easy, we're going to make a simple title screen. All it'll have for now is a line with the title and a line prompting you to press any key. Next week, we'll begin making things interactive, but for now let's stick to baby steps while we start out.

Terminal Size

Terminals come in all shapes and sizes nowadays. As a result, you'll want to be able to handle situations when you encounter a terminal with odd dimensions. While it's up to you how you handle different sizes, they can be broken into two main categories, resizable views and fixed-size views. Either way, it's recommended that you account for situations when the terminal is too small.
First, you need to determine the minimum size of your game, in characters. While there's no official standard, few if any terminals default to a size below 24 rows by 80 columns. This size is that of the VT100 terminal, which many emulators either default to or follow.

Note

Be aware that in this tutorial series, I will be referring to all coordinates/sizes in a rowxcolumn format. This means that the vertical/y-coordinate will be listed first. This notation is used by curses, and I'll be using it to try and stay consistent with the code.
It's up to you whether or not to support larger or smaller sizes, but I recommend keeping the minimum at or below 24x80.

Now, let's update the program to warn the user if their terminal is too small to use. The curses function getmaxyx() will let us do just that. This function takes in a handle to a curses window, as well as two integers for storage.
We haven't touched on windows yet, but you can use the stdscr variable as a replacement. stdscr represents the main window of the curses environment, which is created at the start and fills the entire terminal. getmaxyx(), as the name implies, gets the number of rows and columns in a curses window. Now, we can use these functions to test our terminal.
    3 #define MINIMUM_TERM_ROWS 24
    4 #define MINIMUM_TERM_COLS 80
  ... 
   23     // Test the resolution of of our terminal
   24     int rows, cols;
   25     getmaxyx(stdscr, rows, cols);
   26     if(rows < MINIMUM_TERM_ROWS || cols < MINIMUM_TERM_COLS) {
   27         endwin();
   28         fprintf(stderr, "Error: Your terminal is too small.\nThis application expects dimentions of at least %d rows by %d columns.\n", MINIMUM_TERM_ROWS, MINIMUM_TERM_COLS);
   29         return 1;
   30     }
With this, we can now rest easy knowing that the user won't be launching our game in too small of a terminal. We can handle changing our game based on the size later. For now, this will do.

Note

If you're planning to use a graphical curses implementation, like pdcurses with SDL, then changing resolutions is probably not a huge problem for you. Regardless, it may be good to implement the above in case you want a fallback version that runs in the terminal.

Printing Text

Let's finally get some text on the screen. I'm going to start by showing the code, then we'll go through it line-by-line to examine how it works.
   28     // Print the title, then wait for a button press before exiting
   29     mvaddstr(3, cols / 2, "Cool Game Title");
   30     mvaddstr(rows / 2, cols / 2, "Press any key to continue");
   31     getch();
On the first two lines, we call the function mvaddstr(). This function is actually a sort of shorthand. In curses, the move() function changes the cursor's position, and the addstr() function places a string at the cursor's current position.

As you work with curses, you'll find that a lot of functions have common prefixes that can be added to simplify your code. move() takes the row and column positions to move to as basic integers, and the mv prefix on a function adds these arguments to the start, allowing you to move the cursor then do something, all in one function call. Again, keep in mind that curses starts with the row number, or the y. Here, I use the rows/cols variables that we used to check our terminal above to center our text. It's always helpful to have the current (or expected, for static views) screen size in a variable for the purpose of placing elements, especially since it'll result in less breakage if you change your mind later.

So, what does getch() do here? getch() is one of the most basic tools at your disposal when working with curses. With getch(), you can check for a single keypress at any time, getting back an integer with the character code (if it was a character), or a const value representing some other key. By default, the function waits indefinitely for a keypress, effectively pausing until the user decides to enter a command, but there's a way to prevent this that I'll cover in a later tutorial. For now, we're using the getch() call to pause the application before we exit (This is also why I added a "press any key to continue" message).

At this point, if you've been following along, you'll have some text showing up on screen. Next time, we'll start looking at drawing more interesting things in our terminal, and play with color.

Next Steps

  1. Our centered text is slightly off-center. Can you figure out why? I'll provide an explanation (and solution) at the beginning of the next part.
  2. Take the text that you fixed in part 1, and create a function that draws any text that you pass in centered on your screen. Bonus points for proper error checking (What happens when your text is too long?) and the ability to specify the curses window.
  3. Delete the clear() call at the start. Do you notice something odd? Take a look at the documentation and try to figure out what happened.

Important Terms and Functions

Note

These sections will contain an overview of the new things that we covered in any given part. My hope is that it'll be a useful reference for following the next steps, although you may still need to refer to the API for more details.

Windows

  • initscr() Initializes curses.
  • endwin() Resets the terminal. It should be noted that endwin() is not necessarily final--you can continue using curses as long as you refresh the screen when you're done, making endwin() useful for temporary trips to the command line.
  • getmaxyx(WINDOW *win, int y, int x) Gets the size of a given window. Note that y and x shouldn't be passed by reference, because getmaxyx() is a macro.
  • stdscr The default curses window

Input

  • getch() Returns the next character of keyboard input. If echo is enabled, characters are printed. By default, getch() waits until input is given.
  • noecho() Disables echo, which is enabled by default. A corresponding call to echo() will re-enable echo.
  • cbreak() Enables cbreak, which prevents the terminal from buffering keyboard input. If cbreak is off, input won't be received by curses until a newline(\n) is added, probably from the enter key. A corresponding call to nocbreak() will disable cbreak.

Output

  • clear() Clears the current window.
  • addstr(const char *str) Draws a string at the cursor. The cursor is advanced to the end of the string.

Other

  • move(int y, int x) Moves the cursor to [y, x] in the current window.
  • mv- Prefix for some functions, mostly output functions. Moves the cursor to [y, x] before the function is called.

Final Code

Note

I probably won't always include this part, but I think it's helpful to include the full code that I wrote in a tutorial when I can. Some tutorials might not lend themselves as well to this, but I'll be providing Github links to the relevant commits otherwise, like this one.
    1 #include <curses.h>
    2 
    3 #define MINIMUM_TERM_ROWS 24
    4 #define MINIMUM_TERM_COLS 80
    5 
    6 int main(int argc, char* argv[])
    7 {
    8     /* Start Curses
    9      *
   10      * clear()  clears the terminal, giving us a blank canvas to work with
   11      * noecho() prevents getch() calls from displaying the typed character
   12      * cbreak() ensures that typed input is received immediately by curses
   13     */
   14     initscr();
   15     clear();
   16     noecho();
   17     cbreak();
   18 
   19     // Test the resolution of of our terminal
   20     int rows, cols;
   21     getmaxyx(stdscr, rows, cols);
   22     if(rows < MINIMUM_TERM_ROWS || cols < MINIMUM_TERM_COLS) {
   23         endwin();
   24         fprintf(stderr, "Error: Your terminal is too small.\nThis application expects dimentions of at least %d rows by %d columns.\n", MINIMUM_TERM_ROWS, MINIMUM_TERM_COLS);
   25         return 1;
   26     }
   27 
   28     // Print the title, then wait for a button press before exiting
   29     mvaddstr(3, cols / 2, "Cool Game Title");
   30     mvaddstr(rows / 2, cols / 2, "Press any key to continue");
   31     getch();
   32 
   33     // End Curses
   34     endwin();
   35     return 0;
   36 }

No comments: