4 Hugues Ross - Blog: Tutorial
Hugues Ross
Showing posts with label Tutorial. Show all posts
Showing posts with label Tutorial. Show all posts

9/10/17

Let's Make a Roguelike - Stats, Items and, Messages

Well, here's a new tutorial segment!

This one was pretty awful to write, mostly because I chose too broad of a topic. As a result, it's also quite a bit longer than most of the others (~14 pages, with a previous record of ~11). On the upside, I'm very nearly done here. Currently, I'm planning the remaining 3 segments to cover:
  1. AI and combat
  2. Generating artificial spaces (e.g. mazes, dungeons, buildings, etc.)
  3. Various remaining loose ends, like visibility and overall game structure
I didn't quite do item systems justice here, unfortunately. I seem to have missed dropping/equipping items in particular, although these actions should be fairly trivial to implement with the knowledge from the tutorial.
Since it looks like things are mostly on schedule despite last month's delays, I'm currently pegging the completion date as December 17th, 2017. This will be one full year after I restarted the series, minus a couple days to make it land on a Sunday.

Speaking of dates, this series will be 3 years old in a couple weeks. All I can say at this point is that I'm glad it's coming to a close. It's going to be a while before my next series, I have a few ideas but I want to put a lot of consideration into any future endeavors. However, I do have a one-off informative...thing that I'll be putting up on the tutorials page not too long after this series.

7/23/17

Let's Make a Roguelike - Example: The Swamp Cave

EDIT - Tutorial link was dead yesterday, sorry about that! The page is up now.

Sometimes, when I'm working on my website late at night with the lights off, I hear a voice coming from the Tutorials page:
"Huey, you cantankerous crayfish! You claimed that the roguelike tutorial wasn't on hiatus, but it's been five months since your last post! Fess up!"
...Nope. Definitely not on hiatus. I had to, err....pickle the ASCII. To make it green. That's my story, and I'm sticking to it.

Anyway, there's a new segment of the roguelike tutorial. You can read it here!

Being ever-so-slightly serious for a moment, I didn't (and still don't) really want to work on the tutorial. But, I also want to get it done. That means that I have to write up tutorial segments eventually, even if I'm not really inclined to do so. I also have some other tutorials that I'd like to start, but I'm unwilling to do so before this series is over.

This part serves as a sort of culmination to the last few. It shows how to take the techniques shown so far, and combine them to make a greater whole. My goal now is to introduce the rest of the basic techniques and mechanics commonly found in roguelikes, tie it all back together into a simple package, and mark the series as complete. I've mapped out the remainder of the tutorial at this point, and it looks like it'll be another 5 parts, including the conclusion. My goal for this series is now to be done with it before this series becomes a year old. If I can manage that, then I think I can happily call this 3rd attempt a success. You may notice that the code is quite a bit messier and uncommented. That's also a result of my rush to finish this series up. Once the series is over, I may go back to clean it up and make it more readable, but for now I'm more concerned with speed than quality.

3/20/17

Let's Make a Roguelike: Schedule Change

Today, I have a not-so-good announcement. After much consideration, I've decided to drop the schedule for my tutorials, and post segments on a "when it's done" basis.

If you look at my track record, you'll find that I haven't hit my schedule on a very consistent basis. However, I probably wouldn't drop the schedule if this was the only issue. The real problem is how this schedule has been affecting my ability to work on other things.
My typical schedule for the past few months was something like this:

Pre-Tutorial Weeks
On weeks before a tutorial is due, I usually commit most of the week to working on the tutorial. However, I usually end up delaying things and as a result, I don't get a whole ton finished. This also means that I don't get anything done on my other projects. After that, I need to spend the weekend working on the tutorial so that it can be ready on Monday.

Post-Tutorial Weeks
Following the Monday tutorial post, I have about four days to work on other things before the weekend rolls around. I don't necessarily have to work on the tutorial during these weekends, but if I want to ensure that the tutorial will be done in time then it's a good idea.

Because of the circumstances, the tutorial is currently eating about 2/3rds of my development time. This makes it really tough to work on more than one other project at any moment. I'm not going to stop working on tutorials or anything, but after this one's done I'm going to move to a "prepare everything before posting" strategy.

Wrapping up
Despite these issues I'm still working on this series. After scrapping it twice, I want to see this thing done no matter what. However, I won't sacrifice my other projects to see that happen. To avoid that, I'm dropping the schedule.

3/13/17

No tutorial this week

This week's tutorial has been delayed to next week. This is mostly because I fell ill recently, and wasn't really in the best shape to do anything serious like preparing tutorial content until a few days ago. I did get a few mindless tasks done on my website, but besides that I didn't accomplish much.

Not to worry, though! I'll have a new post up later this week.

2/27/17

Let's Make a Roguelike - Generation

In a shocking turn of events, I actually finished this week's tutorial segment last night! This means that I was done a full day early, and I've opted to post the new segment this morning before I head to work.

The trick this time was starting early. I finished the followup last weekend, then did half of the work on Saturday and the rest on Sunday. I'm pretty sure that if I keep to this formula, then I should be able to keep up with the schedule without causing myself too much stress.

As for the contents, I'm continuing my policy of "give the reader some fun stuff early on" by sharing a couple simple random generation algorithms. In my old plan for the tutorial, this probably wouldn't have happened until part 6 or 7 (we're on part 4), so I think this was a good move. I'm also trying to get through things quickly because (as I said before) I'm still not 100% happy with this tutorial and would like to get it out of the way so that I can focus on making better-planned tutorials in the future.

Lastly, I wanted to say that I'm going to try and revive the old one post a week policy from my college days. I've actually been working pretty hard on a couple of interesting projects lately, so this will give me an opportunity to share them on non-tutorial weeks. I'm painfully aware of the fact that the tutorial is the only thing I've written about this year and it's nearly March.

Anyways, you can click here to read the latest part.

2/14/17

Let's Make a Roguelike: Fashionably Late

Had you worried, didn't I?

I almost had this weeks segment done last night, but I was quite tired and decided to put it off. That was apparently a good decision, since I put several hours into it today before wrapping up! This was mostly poor time management on my part, but I'm happy that the delay was small this time.

As for the content, I'm trying to accelerate the early bits a bit more than in my previous iterations. Even for a long-running series, it's important to keep the readers engaged, and the shape drawing from last time didn't seem up to snuff. I'm taking a bit of a write once, refactor later approach this time: I present the technique in a fairly simple and "bad" way first, then show how the implementation can be refined and improved later. This allows me to frontload much of the exciting stuff, while still providing some basic design lessons down the line. We'll see how that goes.

This series has really been a bit of an interesting experiment for me. There are so many things wrong with my approach to writing these that I almost want to start fresh again, but for the sake of actually getting things done I'm resisting that urge. I think this series is helping me learn a lot about tutorials though, and future seasons will benefit from those lessons.

Here's the link to the new part.

1/30/17

Let's Make a Roguelike: Back Again!

It's done!

If you've been watching the blog, you'll know that I've had some fun these past few weeks. That's been dealt with, and I can finally release the next part of my tutorial series. You can read it here.

For those of you who don't care for tutorials, but are curious about the demo that I was making, here are some links:

Windows build
Source code

It may look a tad underwhelming, but being able to distribute builds to platforms other than my own is an important first step. Now that that's been dealt with, I think things will be going much more smoothly from here on out. Keep watching this space for more updates as they come!

1/24/17

The Delay Continues, and I Explain it

As the title implies, I'm pushing last week's tutorial back another week. I realize that all of these unexplained delays make me look a bit lazy, so I'm going to give you a proper explanation of why this one "almost finished" tutorial segment is taking so long to be released, and why I've spent the last few days pulling my hair out.

Last Monday, I was mostly done with the tutorial. To demonstrate various features of curses, I made a nice little demo application. The code was still a little bit messy, but I figured that as long as I could compile a Windows version of the demo, I wouldn't have any problems.

Unfortunately, there was a small problem. I'm not sure if I've mentioned it here, but I started using Meson for builds instead of writing makefiles. This is good, because Meson works very well. However, I'd never actually tried cross-compiling code with it before, and I was having issues setting it up. In light of this, I pushed the tutorial back so that I would have some time to fix the issue and clean up the code.

Fast-forward to this weekend, when I finally have the time to sit down and figure out my problem. I got the Windows version to compile through Meson, but it would just crash immediately. I spent almost the entirety of my weekend trying various versions of PDCurses and messing with the code, until I finally tried making a stripped-down "hello world" and moving it over to my Windows machine for testing. I should also mention that due to certain circumstances, moving things to my Windows machine takes 5-10 minutes. It's a pain, but I'm working to resolve this over the next week or so.

Anyway, it's at this point that I realize that the PDCurses library really hates cross-compiling. The code is fine, and I manage to get it working by compiling it on my Windows machine. (This seems to be an issue specific to PDCurses. I've had tons of issues like this, but only when using that specific library)

On Monday (yesterday), I've figured out the problem but I still don't actually have a Windows build of the demo. Monday afternoon (after work), I realize that at some point, my fiddling has broken the demo completely on my PC, and it won't work on Linux anymore! Fixing this bug took 3 hours, because it was caused by an innocuous-looking include statement in a random header file.

After dinner I finally transfer my code over to make a Windows build. I set everything up in Code::Blocks for building, get a compiler error, open the relevant file, and Code::Blocks crashes. When I reopen the project, everything is gone. I repeat that song and dance a couple times, and it finally works. Still, the editor is stuttering a ton and making my life a pain.

I finally get the code to build, run it, and...it doesn't work. Fast-forward another 15-30 minutes, and I figure out that the window isn't resizing to fit the content, which screws everything up. When I shrink the content as a test, I come to the horrible realization that my program isn't drawing to an SDL window, it's drawing to the Windows command prompt! I HAD THE WRONG VERSION OF PDCURSES INSTALLED THE WHOLE TIME!

It's 11:00 PM at this point. I now have a Windows build, but it looks like this:

For reference, the build is supposed to look something like this, more or less:
It's at this point that I finally decide to cut my losses and delay things again. It's super late, I'm exhausted, I still don't have an acceptable Windows port, and the code is still not cleaned up, thanks to all of the delays.

So, that's the end of my tale. I'm pretty sure a less stubborn programmer would've simply given up at this point, but that's not my style. I'm going to try and get things sorted out for next week, and I think it'll be fine now that I'm aware of the issues. However, if I can't finish next week, delay again, and still can't finish it by February 6th, I'm putting the project back on hiatus and making a different tutorial series. I've lost a ton of time to some really silly and unexpected issues, and I can't afford to keep slamming my face into a wall forever.

In unrelated news, my personal projects have all been approved. You can expect some more posts about those once this stupid thing is dealt with.

1/16/17

A Small Delay

I've just about finished writing this week's tutorial, but I'm delaying its release until next Monday. While the text and most of the code is done, I'm having some issues getting the build to work nicely in a cross-platform manner. This is mostly due to my inexperience with Meson and some recent factors that temporarily reduced my free time.

Personally, I think it's more important to have this tutorial feel polished and complete than to hit every single deadline perfectly. It's still unfortunate that I have to push things back (especially this early on), but I think this is better than just throwing out something half-baked.

On the upside, once these issues are dealt with I'm unlikely to have to deal with them again, even in future projects and tutorials. I'm still in the process of feeling things out, as far as tutorials are concerned, so figuring things like this out early on is good. With that said, I'm probably going to try to complete future tutorial series before I start posting them, just to ensure that they don't get delayed.

1/2/17

Let's Make a Roguelike - Prep Work

Two weeks after uploading the first part, I've managed to keep to my schedule with a new one. With any luck, I'll keep this up!

This part started out as a lightly-edited version of the old part 1. However, I wasn't happy with it. Today, I sat down and rewrote the whole thing, and I'm very happy with the result. It's likely that I'll be deviating from the old tutorial quite a bit going forward.

So, what can I say about this new part? I mostly shifted the subject away from the limitations of curses, and more towards an explanation of the options that are available and what I plan to cover. I think the result is well-structured, and has a much better flow than the old part 1. It still has no images, since there isn't much to show, but I've got plans for part 2 that I'm hoping will help out a bit.

The other major change affects the tutorial series overall. I'm moving the tutorials away from ncurses, and focusing on the SDL version of PDCurses. I'm hoping that the narrow scope combined with better cross-platform support will make this series much more helpful. Using Meson for builds will likely help as well.

12/19/16

Let's Make a Roguelike, for the 3rd Time

Hello again! Last week, I promised the return of my roguelike tutorial. Today, I'm making good on that promise. Sort of.

I've pushed out a new update to the site and blog, which the observant among you may have noticed already. In addition to making the navigation bar a little bit nicer looking, I've also reorganized the pages a bit. The portfolio section is completely gone, because it never had any content and I don't really need one right now. To fill the space, I've split the games page off of the software page, and added a section for tutorials. The overall theming of the site has gotten some tweaks, and if you click the logo in the upper left-hand corner, then you'll see a very rough work-in-progress landing page. I could have withheld it, but I think it looks better than the word 'Placeholder'.

The tutorial section is also a bit of a work-in-progress. I'm planning on remaking the ring segment collision tutorial that I did a while back, but that could still be a while from now. i also need to play with the theme some more to make it look more polished.

The Tutorial

I've put up the first entry of the new-and-improved roguelike tutorial, a nice little introductory page. You can read it here. It borrows a couple sections from the old part 1, but most of the content is new. It's probably not the most exciting start for a new series but I expect the new tutorial to have a slightly faster pace overall, so hopefully that will offset the slow start.

That's all for the moment! You can expect the first numbered chapter of the tutorial to be up on January 2nd, and more will come every other Monday after that. This means that I might have the beginnings of a schedule! I'll be posting here whenever I put up a new part, and you can expect the other regular posts to appear once in a while, as they typically do. I'm still waiting for my projects to get an official OK from on high, so in the meantime please enjoy these updated tutorial chapters.

1/25/16

Let's Make a Roguelike - 4 - A basic map

Followup

Last week's shape drawing challenges were mostly an exercise, as it's unlikely you'll need them too much going forward. With that said, I still have the solutions.

Circles

Circles and ellipses can be difficult, but you they become much simpler when you apply just a little bit of trigonometry to them. By taking the sin from 0-180 degrees, we get a smooth, semicircle curve of values. From here, we can use some basic arithmetic to get the right length and position of each horizontal line, drawing similarly to how we handled the rectangle.
   79 void draw_ellipse(WINDOW* win, int row, int col, int height, int width, char shape)
   80 {
   81     int rows, cols;
   82     getmaxyx(win, rows, cols);
   83 
   84     for(int i = clamp(row, 0, rows); i < clamp(row + height, 0, rows); ++i) {
   85         float ratio = (float)i / height;
   86         float w = sin(degtorad(ratio * 180)) * width;
   87         for(int j = clamp(col + ((width - ceil(w))), 0, cols); j < clamp(col + ceil(w), 0, cols); ++j) {
   88             mvwaddch(win, i, j, shape);
   89         }
   90     }
   91 }
I've added two new utility macros, degtorad and radtodeg. This isn't really the place to explain them, as they're exactly the same as the normal mathematical conversions.
The result is decent, although it doesn't always perfectly fit the dimensions given.

Better Rectangles

An empty rectangle is quite simple. By keeping track of the starting and ending bounds (the ones used in the nested loops),you can easily get the border positions. If either iterator is at a border, draw a normal character. If not, don't. To keep the inside empty, you can simply draw a space character. This, in particular, is a really useful trick. If you absolutely need to blank something, you can always just draw a space over it. Here's the code:
   60 void draw_rect(WINDOW* win, int row, int col, int height, int width, char shape, int filled)
   61 {
   62     int rows, cols;
   63     getmaxyx(win, rows, cols);
   64 
   65     int imin = clamp(row, 0, rows);
   66     int imax = clamp(row + height, 0, rows);
   67     int jmin = clamp(col, 0, cols);
   68     int jmax = clamp(col + width, 0, cols);
   69     for(int i = imin; i < imax; ++i) {
   70         for(int j = jmin; j < jmax; ++j) {
   71             char s = shape;
   72             if(!filled && j != jmin && i != imin && j != jmax - 1 && i != imax - 1)
   73                 s = ' ';
   74             mvwaddch(win, i, j, s);
   75         }
   76     }
   77 }
And here's my final screenshot:

A Map

Today, we're going to draw an actual map. We won't be getting to gameplay or proper generation just yet, but I thought this would be a good way to cap off these posts about drawing.

Fundamentals

Let's start with some theory.
There are a few things to keep in mind when dealing with a text-based map, and be trying to address as many of these as I can here.

Tiles and Terrain

First off, you need a way to draw your map. I highly recommend using an array of 'tiles', each with various properties like their color pair and symbol.

Note: Why not shapes?

We've spent a couple weeks playing with various ways of drawing text and shapes, so you might be wondering why we don't just use various shapes for our maps. Sadly, this method results in a couple of problems:
  1. How do you use them? To generate your map, you'll need a pretty fancy algorithm just to handle placing and arranging shapes. After that, it'll take much more tweaking to get something even remotely decent looking. The best solution is simply to arrange rectangles and the like in a grid fashion, which just results in a set of large tiles. This could be done as an aesthetic, but you'll still be using tiles anyway.
  2. How do you know what you're standing on? The player and enemies need to know where walls are, at the very least. Many roguelikes go far beyond this, with multiple types of terrain. With shapes, you'll need to use a bunch of tradition collision detection techniques, but with tiles you can just use a simple array lookup.
  3. It'll be close to impossible to easily allow for terrain modifications, at least not at the level of granularity you'll want. With tiles, changing parts of the terrain is quite simple.
Tiles can be represented as structs, which allows us to add a few extra properties. These can vary from game to game, but could easily include things like terrain cost, contact damage, height, and even handles to scripts that execute when a creature enters! The possibilities are endless, so long as you take care not to go too crazy with the amount of data you use. Remember, you'll probably be storing thousands of them in memory.

Other Objects

Of course, your game would probably be boring without any non-tile objects. Most roguelikes generally have 2 separate kinds of non-tile objects: Items and Entities.
Items are pretty straightforward. Generally, tiles are allowed to have some kind of 'inventory', which is generally just a list of items. When the player picks one up, the item is moved to the player's inventory and removed from the tile's inventory.
Entities, as I call them, are a more general group. They can be anything from static objects like traps, push-able rocks, and statues, to more complex things such as monsters (and in some cases, the player). For static entities, you may simply want to make them into special types of tiles. Nevertheless, other kinds of entities benefit from some more complicated logic.
In many roguelikes, you can generally be assured that only one entity will be on a given tile. If this is the case, you may want to make tiles hold a flag of some kind indicating that they're occupied. This flag can simply force a tile to pretend that it's solid, and any AI you write can handle the rest. However, you should probably store your entities separately from your tiles anyway. Because they usually move around and appear/disappear regularly, they need to be managed more carefully than tiles or items.

Generation

In order to actually have a map, first you need to make one. Since this a roguelike, we'll be generating our map on the fly. We won't be covering any serious procedural generation algorithms just yet, but you need to start thinking early about how you want your maps to look. Are you making dungeons with interconnected rooms? Organic-looking natural caves? Grid-like mine shafts? Continents and outdoor terrain? Modern buildings? Several of the above, or something completely different?
Keep in mind some of the underlying elements of each. When the time comes to write a map generator, you'll probably need to put multiple techniques together to form the layouts you want.

Storage

Storing map data externally is optional. After all, you don't need to implement saving if your game is short. You also might regenerate areas on the fly, and make the constant changes part of the narrative. However, many roguelikes do it., because most games benefit greatly from a basic save system.
In some cases, you can get away with nothing more than literally writing the map's data to a file directly. In others, you'll want some special changes to account for entities and items. This is also up to you, and we'll address this issue much later. For now, it's good to mention for the sake of completion.

Implementation 

As a final treat to this otherwise dry and text-heavy tutorial, let's generate and draw a super-simple map. We won't bother making it look too nice and orderly yet, and instead focus on getting a set of tiles created and drawn for the moment. Here's our generation code:
    1 #include <curses.h>
    2 #include <stdlib.h>
    3 #include "map.h"
    4 
    5 typedef struct tile
    6 {
    7     int tile_char;
    8     int tile_pair;
    9 } tile;
   10 
   11 int map_rows, map_cols;
   12 tile* map_tiles = 0;
   13 
   14 void generate_map()
   15 {
   16     // Set map dimensions to match the screen
   17     getmaxyx(stdscr, map_rows, map_cols);
   18 
   19     // Create a new map, deleting an old one if it exists already
   20     if(map_tiles)
   21         free(map_tiles);
   22     map_tiles = calloc(map_rows * map_cols, sizeof(tile));
   23     for(int i = 0; i < map_rows; ++i) {
   24         for(int j = 0; j < map_cols; ++j) {
   25             map_tiles[i * map_rows + j].tile_pair = rand() % 3;
   26             map_tiles[i * map_rows + j].tile_char = (rand() % 93) + 32;
   27         }
   28     }
   29 }
This is pretty standard with our existing code. One thing you'll find is that many of the basic techniques used earlier come back every once in a while.

To keep track of the map,we use a simple global pointer. By keeping it in our C file, we can generally prevent it from being used elsewhere later. In general, we want to keep our map and tile access to functions whenever possible, and hide the actual structure and implementations in our C file.
Our generation 'algorithm', if you can call it that, takes a random color from the ones we picked and a random visible character, then assigns them to each tile. The result, as you'll see in a moment, is some interesting gibberish.

Next, we draw the map. You'll notice that this is awfully similar to our original rectangle drawing code:
   31 void draw_map()
   32 {
   33     for(int i = 0; i < map_rows; ++i) {
   34         move(i, 0);
   35         for(int j = 0; j < map_cols; ++j) {
   36             attron(COLOR_PAIR(map_tiles[i * map_rows + j].tile_pair));
   37             addch(map_tiles[i * map_rows + j].tile_char);
   38         }
   39     }
   40 }
In time, we'll be making this a bit more complicated. In particular, you might notice that very large maps are slow to draw. This is because curses absolutely must redraw every character, since they're all new. Some curses implementations will avoid redraws when they can, but to avoid dependence, we'll eventually want to make the drawing a bit more efficient. We'll also need cropping/scrolling code at some point, but we don't need either of those for this demo either. Also, I used a new method for selecting colors. attron(COLOR_PAIR(n)) allows you to easily set the current color pair, and attron() on its own can also set other attributes that we'll be covering in the future.
Here's how mine turned out:
Even though our map is nothing more than random garbage, it still has some pleasing qualities to it. The two colors look like they make some kind of abstract pattern, and the randomized characters add some texture to break things up.
These two things are important to remember. We'll be using other kinds of random noise functions (albeit more skillfully) in the future to create interesting maps, and it's often useful to use multiple characters for natural ground to make it more interesting to look at. As a closing piece, here's two screenshots from my last 7drl entry, To the West, demonstrating this very thing:
Keep this in mind, too: I used nothing but good old rand() to make To the West's map, and it still looks decent. Even with the most basic techniques, you can make some really nice-looking areas.

Next Steps

  1. Instead of taking random color pairs and characters, try to pull tiles from a predetermined list of combinations.

Important Terms and Functions

Colors and Text Attributes

  • attron(int attrs) Turns on a particular text attibute.
  • attroff(int attrs) Turns off a particular text attibute.
  • COLOR_PAIR(n) A macro that returns the particular attribute for a given color pair.

Map Terms

  •  Tile A single portion of a map, often rectangular. These are like thew squares in a grid, and hold basic terrain and visual data.
  • Entity An object in the world, usually one that blocks movement and/or is active and can move on its own.
  • Item An object that is held and used by entities. Generally, these are stored separately to entities and cannot act on their own.

Final Code

 Full code: here

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 }