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.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.Stunning modern visuals! |
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.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 Steps
- Try making a function that draws circles and ellipses. Can you add a moon and clouds?
- 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?
- Make your existing draw functions take color input.
- 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: heredraw.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:
Post a Comment