4 Hugues Ross - Blog: Let's Make a Roguelike - 3 - Shapes and colors
Hugues Ross

1/12/16

Let's Make a Roguelike - 3 - Shapes and colors

In a move surprising nobody, I've released this a day late. I'm going to blame AGDQ, and hope that things work out better in the future.

Followup

Off-Center Text

The text is off-center for one simple reason: Even though we're placing the text in the center, the text is still drawn left-aligned. In order to make the text be properly centered, we need to adjust for its width. To do that, we subtract half the length of the text, which we can grab with a simple strlen call. I decided to go a bit further, by letting the user specify the text alignment in my function.

If I wanted to take this even further, I could also add wrapping/ellipsis options, but I decided to leave it here for now:
    7 enum alignment
    8 {
    9     ALIGN_LEFT,
   10     ALIGN_CENTER,
   11     ALIGN_RIGHT
   12 };
   13 
   14 /*
   15  * This function lets the user draw a string to a specified row, with
   16  * automatic alignment. If the chosen row is outside the bounds, or the text is
   17  * too wide, the function draws nothing and returns.
   18  *
   19  * align specifies the alignment of the text, and expects an alignment value as
   20  * declared above.
   21  */
   22 void draw_text_aligned(WINDOW* win, int row, const char* text, int align)
   23 {
   24     int rows, cols;
   25     getmaxyx(win, rows, cols);
   26 
   27     // If we're out of bounds, or we don't have enough space, return
   28     if(row > rows || strlen(text) > cols)
   29         return;
   30 
   31     switch(align) {
   32         case ALIGN_LEFT:
   33             mvwaddstr(win, row, 0, text);
   34             break;
   35         case ALIGN_CENTER:
   36             mvwaddstr(win, row, (cols / 2) - (strlen(text) / 2), text);
   37             break;
   38         case ALIGN_RIGHT:
   39             mvwaddstr(win, row, cols - strlen(text), text);
   40             break;
   41     }
   42 }

Drawing Shapes

Our current title screen is functional, but it's also very plain and boring. Let's draw some shapes to fix that. To keep our code clean, I'm going to add a new pair of source files to the project: draw.c and draw.h. We'll keep some of our basic drawing code in here.

A Single Character

To kick things off, we're going to just draw one character onscreen. We don't really need a proper function for this yet; instead we'll just use the curses function instead. The function in question is addch.

Note

You might notice that this function is using the same naming standards as last part's addstr. Just like addstr, you can add prefixes to move the cursor or change the target window.
Simple as it may be, addch is a very useful function because we can use it to draw whatever we want. At this point, you can draw pretty much any shape just by calling addch in the right places. This will become very important when we start on the game proper, but for now let's add a simple starfield to our title screen as an exercise in using it.

For the sake of convenience, I've put this code together into a function. Let's take a closer look:
   35 /*
   36  * This function draws a bunch of characters onto the screen to simulate stars.
   37  */
   38 void draw_stars(WINDOW* win)
   39 {
   40     int rows, cols;
   41     getmaxyx(win, rows, cols);
   42 
   43     char star_chars[] = { '.', '.', '.', '*', '+' };
   44 
   45     int star_count = rand() % 100 + 50;
   46     for(int i = 0; i < star_count; ++i) {
   47         mvwaddch(win, rand() % rows, rand() % cols, star_chars[rand() % 5]);
   48     }
   49 }
The first couple of rows should look familiar to you by now. We grab the dimensions of the window for later use, and assign them to the usual variables.

Next, for convenience, we make an array with the possible stars that we'll be drawing. To make smaller stars more likely, I've simply made extra spots in the array with periods. We also determine how many stars we want with a simple rand call.

Warning

Don't for get to call srand() at the start of your program! srand() is used to seed the RNG, so if you fail to call it then you'll probably get the exact same random numbers every time you launch the program. You should have to bother calling it constantly either. A single srand(time(NULL)); at the start of your program should do fine.
After that, it's just a matter of choosing a random location and character with rand, and repeating the process until you hit the desired number. Stick the function call before you draw your text, and you'll have get a result like this:
Stunning modern visuals!
We could go particularly crazy with this solution by adding custom noise functions and whatnot, but this will do for a simple title effect.

A Box

Now, we have something apart from our title. However, we can do better. Next, we'll draw a rectangle to represent land.

Note

Curses does have box and border functions, but they're used to draw borders around windows. Since we're still only working with a single window, they're not useful to us just yet.
To do this, we'll add a third drawing function, draw_rect:
   52 /*
   53  * This function draws a filled rectangle with the specified dimensions at the
   54  * specified position. The shape argument determines what character will be the
   55  * fill for the rectangle.
   56  *
   57  * If the rectangle would go out of bounds, it is cut off.
   58  */
   59 void draw_rect(WINDOW* win, int row, int col, int height, int width, char shape)
   60 {
   61     int rows, cols;
   62     getmaxyx(win, rows, cols);
   63 
   64     for(int i = max(row, 0); i < min(row + height, rows); ++i) {
   65         for(int j = max(col, 0); j < min(col + width, cols); ++j) {
   66             mvwaddch(win, i, j, shape);
   67         }
   68     }
   69 }
As always, we grab the window dimensions. Then, we do something a little different. As far as I am aware, the C standard doesn't have any sort of clamping function. Since clamp functions are generally quite useful in a lot of situations, I threw together a handful of simple macros:
    1 #define min(a, b) (a < b ? a : b)
    2 #define max(a, b) (a > b ? a : b)
    3 #define clamp(a, b, c) (min(max(a, b), c))
These are simple to make and provide a lot of convenience, so I recommend keeping them somewhere easily accessible. I've added them to a new header, math_util.h, so that I can reuse them later. Here, I'm just using them to keep the rectangle within the bounds of the window.

One nice thing about addch-based drawing functions is that most of them end up being pretty similar: A constrained, finite loop with a single mvaddch or mvwaddch inside it.

With all the drawing functions done, my new main function looks like this (Some parts have been skipped)
    1 #include <curses.h>
    2 #include <stdlib.h>
    3 #include <time.h>
    4 #include "draw.h"
  ... 
    9 int main(int argc, char* argv[])
   10 {
   11     // Seed the RNG
   12     srand(time(NULL));
  ... 
   34     draw_stars(stdscr);
   35     draw_rect(stdscr, rows / 2 + 3, 0, rows / 2 - 3, cols, '#');
   36 
   37     // Print the title, then wait for a button press before exiting
   38     const char* title_str = "Cool Game Title";
   39     draw_text_aligned(stdscr, 3, "Cool Game Title", ALIGN_CENTER);
   40     draw_text_aligned(stdscr, rows / 2, "Press any key to continue", ALIGN_CENTER);
   41     getch();
   42 
   43     // End Curses
   44     endwin();
   45     return 0;
   46 }
This puts our rectangle just below our "Press any key" prompt.

Adding Color

We've already made some progress towards a nice title screen, but our text is starting to get harder to read. If we add much more detail, the problem will only get worse from here. It's time to add some color.

Color Pairs

As I mentioned in the first part of this series, curses handles colors in foreground/background pairs. curses keeps track of both colors and pairs with ids, rather than storing RGB values directly. It's up to the developer to decide what each pair contains, and what each color actually is (assuming the terminal supports color changes).

When doing more complex things with color, it's important to know how to manage your color pairs. I'm not going to cover that just yet, however. For now, we'll cover the basics of actually setting up and using color in curses.

Checking for Color

The first step, obviously, is to ensure that color actually works. Just like with terminal dimensions, we need to check what our target supports in terms of color.

Unlike terminal dimensions, we can still make things work if we don't have color support. It's up to you if you want to actually support monochrome or low-color displays, but it's probably easier than low resolutions.

Testing a terminal's color capabilities is pretty simple. There are four main things to check: color, color changing, color count, and color pair count. Take a look below.
    8 #define MINIMUM_COLORS    16
    9 #define MINIMUM_PAIRS     16
  ... 
   36     // Test the color capabilities of our terminal
   37     if(!has_colors()) {
   38         endwin();
   39         fprintf(stderr, "Error: Your terminal does not support color output.\nThis application must have color output to run.\n");
   40         return 1;
   41     }
   42 
   43     start_color();
   44 
   45     // If they support color but lack full support, warn them and continue
   46     if(!can_change_color()) {
   47         endwin();
   48         fprintf(stderr, "Warning: Your terminal cannot change color definitions.\nThis may impact the game's aesthetic.\nPress any key to continue.\n");
   49         getch();
   50         refresh();
   51     }
   52     if(COLORS < MINIMUM_COLORS || COLOR_PAIRS < MINIMUM_PAIRS) {
   53         endwin();
   54         fprintf(stderr, "Warning: Your terminal lacks sufficient color support to use this game's full range of color.\nThis may impact the game's aesthetic.\nPress any key to continue.\n");
   55         getch();
   56         refresh();
   57     }
The first block is where I test if colors are supported, using the has_colors function. I've decided that (for now) I'll be requiring some kind of color support, but that's all. After that, the start_color function initializes color support, which we'll need for the latter checks.

can_change_color, as the name implies, simply checks if we can change the values attached to the color ids. This is mostly necessary if you plan on assigning specific values to each color, which we won't be doing this time. Nevertheless, we should check it for future reference.

The final check is for the number of colors and pairs available. I consider this to be the most important, after checking color support. This number will ultimately dictate the size of your palette, and thus just how many colors you can push to the screen. This check is last, however, because COLORS and COLOR_PAIRS is unset until you call start_color.

Note

If you're developing on linux (and possibly Mac, but I haven't tried), then you can test these checks easily by setting the TERM environment variable before starting your application. This variable informs curses of what kind of terminal it's running in, and different terminal profiles have support for different color settings.

Using Color

Now that we've checked our color capabilities, let's finish off with a little demonstration.
The two main functions that we'll be using to handle colors are init_pair and color_set. The names of these two functions is pretty self-explanatory, so I'm going to jump back into the code to explain their use.
   59     init_pair(1, COLOR_YELLOW, COLOR_BLACK);
   60     init_pair(2, COLOR_GREEN, COLOR_BLACK);
   61 
   62     color_set(1, 0);
   63     draw_stars(stdscr);
   64     color_set(2, 0);
   65     draw_rect(stdscr, rows / 2 + 3, 0, rows / 2 - 3, cols, '#');
   66 
   67     // Reset our color
   68     color_set(0, 0);
   69 
   70     // Print the title, then wait for a button press before exiting
   71     const char* title_str = "Cool Game Title";
   72     draw_text_aligned(stdscr, 3, "Cool Game Title", ALIGN_CENTER);
   73     draw_text_aligned(stdscr, rows / 2, "Press any key to continue", ALIGN_CENTER);
The init_pair takes the id of the pair being set, followed by the foreground color and then the background color. For our convenience, curses provides macros representing several basic colors: COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, COLOR_MAGENTA, and COLOR_CYAN.
These come out to be the 8 basic colors available in most terminals. Other colors are often present, but since they're not guaranteed the user is expected to use their actual id number.

color_set isn't the only way to set the color being used, but it's easily the most convenient. The first number passed is the color pair's id, and the second is null. The second argument is actually a void pointer, but it's currently unused by the spec so we set it to 0.


Now, we've discussed the general drawing process when working with curses. You start off the program by building your palette with init_pair. Then you use color_set to select a color pair for use. Finally, you use functions like addch and addstr to place characters onscreen. If you've been following along, your title screen will probably look something like this:
The finished screen, complete with color
Next time, we'll be finally adding something more beyond this title screen.

Next Steps

  1.  Try making a function that draws circles and ellipses. Can you add a moon and clouds?
  2. Add support for empty rectangles to draw_rect. Then, try making a box surrounding all of your text. How do you ensure that the box has no stars in it?
  3. Make your existing draw functions take color input.
  4. Use your existing code to play around with colors and characters. Try to decide how you want things to look.

Important Terms and Functions

Output

  • addch(const chtype ch) Adds a character to at the cursor position and advances it. ch can be treated as a normal character, but can have some other attributes OR'ed to it as well.

Colors

  • start_color() Initializes color in curses, allowing it to be used.
  • has_color() Checks if the terminal supports color output.
  • can_change_color() Checks if the terminal supports changing the RGB values of colors.
  • init_pair(short pair, short f, short b) Sets the pair with id pair to use color f as the foreground and b as the background. If the pair is in use, all occurrences of it onscreen change as well.
  • color_set(short color_pair_number, void* opts) Sets the color pair to be used. opts must equal NULL.
  • COLORS The number of unique colors supported by the terminal. Set by init_color().
  • COLOR_PAIRS The number of unique color pairs supported by the terminal. Set by init_color().

Other

  • srand(unsigned int seed) Seeds the RNG for future rand calls.
  • rand() Returns the next pseudo-random number in the sequence seeded by srand.

Final Code

 Full code: here

draw.c:
    1 #include <string.h>
    2 #include <stdlib.h>
    3 #include "draw.h"
    4 #include "math_util.h"
    5 
    6 /*
    7  * This function lets the user draw a string to a specified row, with
    8  * automatic alignment. If the chosen row is outside the bounds, or the text is
    9  * too wide, the function draws nothing and returns.
   10  *
   11  * align specifies the alignment of the text, and expects an alignment value as
   12  * declared above.
   13  */
   14 void draw_text_aligned(WINDOW* win, int row, const char* text, int align)
   15 {
   16     int rows, cols;
   17     getmaxyx(win, rows, cols);
   18 
   19     // If we're out of bounds, or we don't have enough space, return
   20     if(row > rows || strlen(text) > cols)
   21         return;
   22 
   23     switch(align) {
   24         case ALIGN_LEFT:
   25             mvwaddstr(win, row, 0, text);
   26             break;
   27         case ALIGN_CENTER:
   28             mvwaddstr(win, row, (cols / 2) - (strlen(text) / 2), text);
   29             break;
   30         case ALIGN_RIGHT:
   31             mvwaddstr(win, row, cols - strlen(text), text);
   32             break;
   33     }
   34 }
   35 
   36 /*
   37  * This function draws a bunch of characters onto the screen to simulate stars.
   38  */
   39 void draw_stars(WINDOW* win)
   40 {
   41     int rows, cols;
   42     getmaxyx(win, rows, cols);
   43 
   44     char star_chars[] = { '.', '.', '.', '*', '+' };
   45 
   46     int star_count = rand() % 100 + 50;
   47     for(int i = 0; i < star_count; ++i) {
   48         mvwaddch(win, rand() % rows, rand() % cols, star_chars[rand() % 5]);
   49     }
   50 }
   51 
   52 /*
   53  * This function draws a filled rectangle with the specified dimensions at the
   54  * specified position. The shape argument determines what character will be the
   55  * fill for the rectangle.
   56  *
   57  * If the rectangle would go out of bounds, it is cut off.
   58  */
   59 void draw_rect(WINDOW* win, int row, int col, int height, int width, char shape)
   60 {
   61     int rows, cols;
   62     getmaxyx(win, rows, cols);
   63 
   64     for(int i = clamp(row, 0, rows); i < clamp(row + height, 0, rows); ++i) {
   65         for(int j = clamp(col, 0, cols); j < clamp(col + width, 0, cols); ++j) {
   66             mvwaddch(win, i, j, shape);
   67         }
   68     }
   69 }

No comments: