|
Beginner's Guide to Roguelikes in C/C++
Updated January 2013
|
Every program that has ever been created can be reduced to three key phases: input, processing and output.
Sometimes, it seems so obvious that we miss it. Take for example the following program:
When you compile and run the code, your inputs are the numbers you've assigned to the variables a and b when you were entering the code. Your processing is the arithmetic - adding the two variables a and b and storing the value in variable c. Your output is drawing the value of variable c to the screen.
You may be asking: what does this have to do with writing a game? Well, a game is based on exactly the same principles - only that the input, output and (especially) the processing phase are much more complicated.
Normally the program flow in any game can be broken down into the following sequence of events:
Can you see the sequence of events occurring here?
As you can see, there are only two real differences:
To word it the differently: you present a situation to the player, the player evaluates it and decides how to act and the game reacts to the player's action. The trouble comes in the details but that is essentially the framework of any game.
To actually show how this works, let's create a very simple "game" in which the user controls a character on the screen and can move it around. One of the first (and frankly, one of the most difficult) obstacles you'll run into is how to draw text to the screen and to get keyboard input from the player.
Most roguelike developers do this through the use of a console interface library, something like libtcod or 'Curses. For the guide, I've written a (hopefully) turnkey and easy-to-use console library that we'll be using for all of our examples.
The first thing we should always do when the program runs is to clear the screen of any junk that was there before. If you don't know what state a system is in, assert it. It's good practice to get into. The code for this is here:
Much like how iostream.h would give you access to the cout object, the console library gives you access to an object called console, which behaves like a very simple version of cout. Don't worry, it's not as complicated as it seems.
Right, now that's out of the way, we can move on. Remember how the program flow for games repeats? We're going to need a loop somewhere in there, so we might as well add it now. Let's make it an infinite loop since we don't know how we will terminate it.
So putting it all together, the code should look something like this now:
Alright, it's starting to take shape!
The first and most important thing is to show the player what's going on. It's not much of a game if all you see is a blank screen.
To do this, we need to draw the player's character on the screen. In homage to Rogue, we'll use the "AT" symbol, @, to represent the player. But it's not enough to simply throw it at the screen, we need to make sure that it's in the right spot.
Regardless of the operating system involved, in console mode the screen is divided up into a grid, much like graph paper or a chess board. Only one character can occupy one tile at a time. This is grid perfectly suited to console-based games as it gives a sense of space and distance in your game.
What we need to do now is to determine on what square tile on the console the player is at. To do this, we need to make sure we record the player's position in some sort of variable. Since the screen is two-dimensional, we'll use an (X, Y) coordinate system to indicate position.
Where the nPlayerX variable determines how many tiles from the top-left of the console the player is at horizontally and the nPlayerY variable determines the vertical offset from the same corner.
We need to initialize these variables this so that the player starts at a definite location (rather than whatever garbage data was residing at their addresses). A console screen by default usually is 80x25 characters big, so let's try for something in the middle.
Now all we need to do is get the computer to draw the ' @ ' character at that precise location on the console. To do that we need the following code.
This makes our total code look like this.
Pretty straightforward, right?
Now we have drawn the player's character to the screen, we can now let the player tell us what they want to do.
So how do we get this input? The easiest way to do this is with the getch() function, which does two things:
These two things do exactly everything we need to do for this phase. All we need to do is save the key that the user pressed so that we can process it later.
The idea here is that we can check for specific keys and perform different operations depending on the keys. This would look like:
There are a few problems with getch() though:
So to counter this, most console libraries like libtcod or 'curses have their own special routines to read input from the keyboard. In addition, these libraries are often designed to behave the same regardless of system platform or compiler. Our console library tries to do the same thing, through a method called WaitForKeypress().
The WaitForKeypress() method returns a structure containing information regarding a key press that your game experienced from the user. The most important thing about this structure is the "eCode" property which tells you what key was pressed (the other properties tell you if control keys were pressed at the same time).
This is everything we need for our input phase, so the final product looks like this:
With input and output done, all that's left is to actually do something when a key is pressed. In this case, we're going to make that the arrows keys move the player around. If we keep NUMLOCK on, then the only key presses we need to worry about are:
Movement occurs by incrementing or decrementing the coordinate variables nPlayerX and nPlayerY. The question is, which way is up? How do we move left? To figure this out, take a look at the following picture. It contains two characters, each put at a different coordinate.
The second character is down and to the left of the first, but according to the coordinates, it is (-10, 5) away. Using this, we can determine the following:
nPlayerY–)nPlayerY++)nPlayerX–)nPlayerX++)This can be implemented using either if or switch statements. We'll use a switch statement as it's a little cleaner to look at. Pay close attention to the increments and decrements here.
Putting it all together we get the following:
That's pretty much it. Compile and run the code and see what we've got. The first thing you notice is the following effect:
It keeps on drawing the same character all over the place!
This is actually to be expected. We're drawing things to the screen, and moving around, without ever once erasing anything. This can be fixed by adding a simple, one line command at the beginning of the output phase, resulting in an output phase that looks like this:
Basically, we wipe the entire screen clear and draw the player in the new position. Save, compile and run the fix and see how things work now.
If I have done my job, you should now be getting comfortable with structuring your code, and have begun to get your feet wet with the whole concept of drawing to the console and receiving user input.
There are a few ignored problems with this example, the most blatant to the experienced developer is that there is no code to prevent the user from moving off the screen which can lead to data corruption, program crashes and other major errors. Before you move on, I would recommend playing around with this application to make it limit the user to only moving within the screen itself.
If you can do this you are getting the hang of things quite well. Also, it may help to play around a little and try to intentionally mess up the code to get used to how the compiler reports errors.
1.8.3