Basic Game Programming Part 3

Here’s an experiment to try purely in C++ (or the language of your choice). Print any character you want on a blank terminal screen at a specific location (say 10 characters over and on the 4th line). This is a crucial idea in game programming, because even something as simple as using the arrow keys to move something around a screen requires you to do this.

You’ll find it gets tedious quickly. Let me guess what you came up with. You have two auxiliary functions. One of them prints “\n” an appropriate number of times to move you to the correct line. The other prints a bunch of ” ” to move you over. It works.

Now suppose you want more information on the screen like health remaining or an inventory list. You can’t use that method anymore, because it depends on where the cursor ended during the last thing you drew. This, in turn, depends on information that could be changing (Health: 9 vs Health: 10 places the cursor one more space over).

Matters get even worse if you want to draw something on the right edge of the screen, because you have no idea what the player is looking at. One could use a standard terminal size, another could have maximized it to fullscreen. This changes the number of spaces on a line.

Until you actually try to do these things, or at least seriously attempt a thought-experiment doing it, you will never appreciate why adding an external library to handle this stuff is important. There are tons of these in existence, but probably the most common and universal ones are built from SDL (we will definitely not get into that today).

The simplest for C++ is probably ncurses. It stands for “new curses” and has a pretty interesting history (the name implies there was a curses). It effectively emulates a terminal, which sounds ridiculous at first (why not just use the terminal!) until you think through all the problems above. Today, we’ll make an “@” symbol move around the screen using the arrow keys.

For our purposes, the two most useful functions in the library will be clear() and mvaddch(). As we saw last time, clear() was annoying because it was OS and terminal dependent. Since ncurses emulates a terminal, there is one function that clears the screen independent of OS. The function mvaddch takes 3 arguments, the first two are ints and give the y-coordinate and x-coordinate where you want to place a character, and the third argument is a character that gets placed in that location.

Even though I want to think in terms of x and y coordinates, mvaddch actually takes the row then the column number. This means you put in y first and x second and the y-coordinate counts down from the top of the screen which can be confusing to new users of ncurses.

At this point, we understand the structure of the game loop, so you should basically be able to fill in the rest of the program. There are quite a few subtleties, though, so I’ll go slowly through it.

The only variables I need this time are the x-coordinate and y-coordinate, so I’ll make these global. I started it at (5,5) randomly. We get input using getch(). This returns an integer, but we don’t need to worry about what that integer is. Since we are using the arrow keys we can use KEY_UP, KEY_DOWN, etc which are integers corresponding to the correct arrow directions. Also, using integers is nice rather than characters, because in C++ we can more cleanly write a bunch of “else if” statements with a “switch.”

This lets us make the whole Update() function by changing the x and y coordinates depending on which arrow direction was pressed.

void Update() {
	switch(userInput) {
		case KEY_LEFT:
			XPOS = XPOS - 1;
			break;
		case KEY_RIGHT:
			XPOS = XPOS + 1;
			break;
		case KEY_UP:
			YPOS = YPOS - 1;
			break;
		case KEY_DOWN:
			YPOS = YPOS + 1;
			break;
	}
}

As before, our draw function clears the whole screen and then draws everything. Because of how ncurses works, nothing will be drawn to the terminal until refresh() is called. This is basically so you can store a whole bunch information about what is going to be displayed, and then it can work out how to display it separately:

void Draw() {
	clear();
	mvaddch(YPOS, XPOS, PLAYER);
	refresh();
}

The only thing left is to initialize a bunch of stuff, but the content of the program is really that simple. I commented the initialization lines so you can see what they do, but for the most part, you’ll just copy and paste that whole chunk in every use of ncurses.

#include <ncurses.h>

int XPOS = 5;
int YPOS = 5;
char PLAYER = '@';
int userInput;

void Update() {
	switch(userInput) {
		case KEY_LEFT:
			XPOS = XPOS - 1;
			break;
		case KEY_RIGHT:
			XPOS = XPOS + 1;
			break;
		case KEY_UP:
			YPOS = YPOS - 1;
			break;
		case KEY_DOWN:
			YPOS = YPOS + 1;
			break;
	}
}

void Draw() {
	clear();
	mvaddch(YPOS, XPOS, PLAYER);
	refresh();
}

int main() {

	//Initialize ncurses

	initscr(); //Tells it to make a terminal screen.
  	clear();   //Clears the screen.
  	noecho();  //When user types input, it doesn't appear on the screen.
  	cbreak();  //Typed character is immediately available.
  	keypad(stdscr, TRUE);  //Standard screen.
  	curs_set(0);  //Starts the cursor at (0,0).
  	mvaddch(YPOS, XPOS, PLAYER); //Draw '@' in the initial location.
  	refresh();  //Updates the screen to display the '@'.

	while(true) {

		//Input()
		userInput = getch();

		Update();
		Draw();

		if (userInput == 'q') {
			break;
		}
	}

	endwin();  //Closes the terminal screen.

	return 0;
}

This will probably be the last post in this series. I have one more topic I could do (decoupling the draw loop from the game logic loop). For the most part, you should now be in a position to look up tutorials on using fancier stuff like Unity or Monogame.

The problem I found with those tutorials is that they focus solely on how to use their engine and not on what these underlying components of the engine are (and why they exist). Hopefully after these posts, these concepts make more sense allowing you to more easily jump in.

(For the record, these posts keep getting the C# tag even though I type C++. It autocorrects it upon hitting the publish button.)

Basic Game Programming Part 2

So here’s a video of the finished product for today. If you haven’t read the last post, you should do that and try the exercises. Better yet, watch the finished product and try to create it without reading the excercises:

We’ll go through all the changes I made, but we’ll start with the most important one. Last week I gave the suggestion “Feature 6: Figure out where to clear the screen to not be so cluttered.” This is a crucial concept in drawing the graphics when making a game.

This toy example gives us the fundamental idea, but if you actually try it in C++, you’ll find it somewhat difficult. I used Linux, so I could use system(“clear”) to clear the terminal screen. You’ll need different commands depending on the operating system. Fighting with drawing things in the proper place will give you a huge appreciation for tools that aid in doing this (we’ll talk about one next week called “ncurses”).

If you watch the video, I programmed it to give the illusion that certain things were static on the screen and don’t get redrawn. In reality, on every iteration of the game loop, I cleared the entire screen and redrew the whole screen with the updated information. This makes it look like “Your Health:” stays the same while the number clicks down as you lose health.

You obviously don’t have to do it this way, but experience has taught me you can get some nasty display bugs if you don’t fully clear and redraw. As I said before, this merely amounted to adding a simple system(“clear”) at the beginning of the Draw() function. I also added a few lines of information.

void Draw() {
  system("clear");
  cout << "Your Health: " << PLYRHLTH << "\n";
  cout << "Enemy Health: " << ENMYHLTH << "\n";
  cout << "a. Attack \n";
  cout << "b. Run \n";
  cout << PLYRDMG;
  cout << ENMYDMG;
}

The other major change was to rescope the variables (you might have noticed they changed names and are all caps, my convention for global variables). Since I wanted many of the functions to be able to access and change the PlayerHealth, EnemyHealth, and so on, I made them all global. This also cleaned up having to pass all of them to each function and allowed me to refactor Update() into its own function.

At the top of the program I list them and initialize them. PLYRDMG is the message “You hit for 2 damage” and so on (see Draw() and compare to the video). It is initialized to be blank so everything writes in the same place:

int PLYRHLTH = 10;
int ENMYHLTH = 10;
string PLYRDMG = "\n";
string ENMYDMG = "\n";

The only thing that changed in the Update() function is an added if statement in a few places so that half the time you miss and half the time the enemy misses. I also had to come up with a way tell the game loop whether the player chose to “Run” and exit the game. This was easy before, because we could write if (PlayerInput == “b”) { break; } to break out of the game loop.

Now the function isn’t in the game loop, so calling “break;” doesn’t work. The way around this was something people came up with in Operating Systems programming a long time ago. I made Update() return an integer rather than be a void. This way I could always return 0 if the player wanted it to keep going. Otherwise it returns 1 and I know they want to break out of the game. This why in C and C++ main() is an int rather than a void. Returning 0 tells the OS that everything went well in the execution of the program.

int Update(string playerinput) {
  if (playerinput == "a") {
    if (rand() % 2 == 0) {
      int r1 = rand() % 4;
      ENMYHLTH -= r1;
      stringstream ss;
      ss << "You hit for " << r1 << " damage.\n";
      PLYRDMG = ss.str();
    } else {
      PLYRDMG = "You miss.\n";
    }
  }

  else if (playerinput == "b") {
    return 1;
  }

  else {
    PLYRDMG = "This is not a valid option.\n";
  }

  //Enemy attacks.
  if (rand() %2 == 0) {
    int r2 = rand() % 4;
    PLYRHLTH -= r2;
    stringstream ss;
    ss << "Enemy hits for " << r2 << " damage.\n";
    ENMYDMG = ss.str();
  } else {
    ENMYDMG = "Enemy Misses!\n";
  }

  return 0;
}

Lastly, I added a “Start Screen” to get into the game. I return to this when the game ends and ask if the player wants to play again. This was done simply by putting the whole game in a second while loop. As long as the player keeps entering “y” for “do you want to play” I keep the boolean variable WantToPlay = true. Otherwise I switch it to false and the game ends. This is also something a prebuilt engine will do for you in a much more elegant way.

Here’s the whole thing:

#include <iostream>
#include <sstream>
#include <stdlib.h>

using namespace std;

int PLYRHLTH = 10;
int ENMYHLTH = 10;
string PLYRDMG = "\n";
string ENMYDMG = "\n";

void ResetVariables() {
  PLYRHLTH = 10;
  ENMYHLTH = 10;
  PLYRDMG = "\n";
  ENMYDMG = "\n";
}

void Draw() {
  system("clear");
  cout << "Your Health: " << PLYRHLTH << "\n";
  cout << "Enemy Health: " << ENMYHLTH << "\n";
  cout << "a. Attack \n";
  cout << "b. Run \n";
  cout << PLYRDMG;
  cout << ENMYDMG;
}

string Input() {
  string tmp;
  cin >> tmp;
  return tmp;
}

int Update(string playerinput) {
  if (playerinput == "a") {
    if (rand() % 2 == 0) {
      int r1 = rand() % 4;
      ENMYHLTH -= r1;
      stringstream ss;
      ss << "You hit for " << r1 << " damage.\n";
      PLYRDMG = ss.str();
    } else {
      PLYRDMG = "You miss.\n";
    }
  }

  else if (playerinput == "b") {
    return 1;
  }

  else {
    PLYRDMG = "This is not a valid option.\n";
  }

  //Enemy attacks.
  if (rand() %2 == 0) {
    int r2 = rand() % 4;
    PLYRHLTH -= r2;
    stringstream ss;
    ss << "Enemy hits for " << r2 << " damage.\n";
    ENMYDMG = ss.str();
  } else {
    ENMYDMG = "Enemy Misses!\n";
  }

  return 0;
}

int main() {
  int State;
  string PlayerInput;
  bool WantToPlay = true;

  while(WantToPlay) {

    //Reset the Initial Information
    ResetVariables();
    srand(time(NULL));
    system("clear");
    cout << "An enemy approaches. Do you fight (y/n)?\n";
    PlayerInput = Input();
    if (PlayerInput == "n") {
      WantToPlay = false;
    }
    Draw();

    //Game Loop
    while(true) {

      PlayerInput = Input();
      State = Update(PlayerInput);

      if (State == 1)
        break;

      //Break out of the game looop if won or lost.
      if (ENMYHLTH <= 0) {
        system("clear");
        cout << "You Win.\n";
        break;
      }
      else if (PLYRHLTH <= 0) {
        system("clear");
        cout << "You Lose.\n";
        break;
      }

      Draw();
    }

    cout << "Game Over.\n";
    cout << "Want to play again (y/n)?\n";
    PlayerInput = Input();
    if (PlayerInput == "n") {
      WantToPlay = false;
    }
  }

  return 0;
}

Next time we’ll work on improving the graphics by using outside tools and getting something moving around the screen so it feels more game-like.

7DRL Takeaway Lessons

I promise this will be my last 7DRL post, but I thought I should record some thoughts I’ve had about it now that it is over.

This post could alternately be called “Why I Will Not Use MonoGame in the Foreseeable Future.”

First, the motto of MonoGame is “Write Once, Play Everywhere.” This sounded promising to me, because my first game had issues deploying almost anywhere. Of course, what they mean is that you can play anywhere supported by Windows.

There are ways to get builds onto other platforms so that Mac and Linux users can play, but I only had 7 days, and I’m not a trained computer scientist, so that wasn’t going to happen. This was a little frustrating, and also one Windows user already complained they couldn’t get it installed.

Second, MonoGame is effectively a re-implementation of XNA 4. Unfortunately, some of the pipelines (especially with an up to date Visual Studio) are broken and require hacks to work. This caused major problems with sound. So much so that I scratched all sound from the game.

I know that with enough time, I probably could have gotten this working (because lots of games made with it have sound), but I couldn’t waste the time in those 7 days to fight with it. This was frustrating, because one of the main reasons to use MonoGame was to make all of that streamlined and easy.

I also felt trapped into using Visual Studio as an IDE. This is certainly a fine IDE, but I’m most comfortable with Sublime Text 2. Switching editors wastes a lot of time, especially when you “downgrade.”

By this I mean VS doesn’t have half the great features of ST2 (including my most used feature: multiple cursors). In retrospect, I should have edited in ST2 and built in VS.

All this being said, MonoGame was a good enough framework to produce a full working game in 7 days, so I can’t complain too much. Also, if I were part of a larger team with a longer time frame, many of these issues would disappear. So I admit these complaints come specifically from the 7 day issue.

If I do this again, I will almost certainly try Cocos2d-x. It looks like this will fix all the complaints I had. First, Cocos2d-x is open source. This means I can actually see what the engine is doing if I need to! What a concept.

Second, it is C++, so it will deploy across platforms much easier. Lastly, it isn’t in some transition period like MonoGame where content management seems to use outdated, unsupported pipelines. I’m also more familiar with C++ than C# which would have solved a few headaches.

Theseus: a 7DRL 2015 Completed!

Final Stats:

One week.
About 90 hand drawn 64×64 pixel tiles.
Over 2000 lines of code.
A completed one-hp strategy roguelike.

Proof that it can be beaten happened during my stream where I tested for bugs (quality isn’t the best):

Actually, I think I’ve finally gotten it balanced so that you should be able to win if you know what you are doing every time except for extraordinarily rare scenarios dictated by the randomness.

7DRL 2015 Success.

[Edit:] I’ve set up an itch page: http://hilbert90.itch.io/theseus
Download the game to play for free here: https://www.dropbox.com/sh/1ywjy7s3y72bq5k/AABOZWsPOFBYs0qgcxW5Ccqxa?dl=0

7DRL: Theseus, Alpha Testing Phase

Alright. I have to leave for the day. I’ve thrown a few more things in this morning at the last minute, and then built the whole game.

If anyone wants to test it and give feedback, that would be much appreciated. I’ll still do a lot of polishing tomorrow.

Sorry, as of right now I have a build that I’m 99% positive will work on any Windows 7 or 8 machine. Earlier probably works, but hasn’t been tested. I haven’t made any others (the opposite of my first game which people could only get to work on Linux).

I’ve learned my lesson. If I do this again, I’ll probably switch to Cocos2d-x, which is open source, and easily builds into any machine.

Types of things I’m looking for:

1. The whole thing crashes. If this happens, try to remember exactly what happened and leave a comment.

2. The setup doesn’t install it on your computer, but you are using Windows. Let me know something about your computer if this happens.

3. The game is too easy (for example, you consistently scale up too quickly and bash your way to victory with no thought).

4. You can let me know if it is too hard, but I probably won’t change that.

5. Anything else that seems reasonable to need a fix for being on Day 6 of developing a game (you can say the art is not polished, but that will just hurt my feelings … it’s Day 6 and I had a lot of other things to do for goodness sake).

6. The Readme didn’t have enough information.

Tonight or tomorrow, I’ll probably add 2 more items for reference of what will be added before the final release.

Thanks. Have fun.

I’ll think of a better way of distributing this later. For now, go here: https://github.com/wardm4/Theseus

Press Download Zip (button to right). Ignore all the files except the one called “Theseus.zip” Right click on that and save it somewhere (your Downloads folder or whatever). Unzip it and then double click on setup.exe. That will prompt a standard install.

Better yet. I’ve added just the zip to a Dropbox here if you don’t want to get the source code too: https://www.dropbox.com/sh/1ywjy7s3y72bq5k/AABOZWsPOFBYs0qgcxW5Ccqxa?dl=0

7DRL Dev Log Day 5

Today turned out well. I didn’t get trapped trying to fix some problematic C# syntax like in the past two days. The main improvements were to draw the final boss, get him animated, program his AI, and get the final room set up (which has different design structure than the rest of the labyrinth).

I did some balancing with the boss to figure out how much HP seemed reasonable for what level I thought an average person would be at. I also drew, animated, and designed an AI for a new NPC that will make an appearance later in the game. They are pretty tricky, so I imagine a bunch of deaths for people who dare to make an attempt at them.

The game feels reasonably complete at this point. There is a nice progression leading up to the final boss, and you win the game if you kill it. I made a push to get it to this point before today, because it turns out that tomorrow I’ll have almost no time to work on it at all.

I have two more significant additions I want to make, and then tons of polishing I can do (tons of extra animations, sound, balancing, bug finding and fixing, etc). Overall, I’m finding the game a bit too easy, but my alpha tester hasn’t beaten it yet, so who knows. If you know all the enemy patterns (like I do), then I think it is beatable pretty much every time with caution. I never came across an impossible situation today.

Tomorrow I’ll give a download of the most up-to-date version in case any readers want to volunteer a few playthroughs to give some feedback before the end of the last day.

7DRL Dev Log Day 4

Today I’ve put in some more details, but I still haven’t gotten around to programming the final fight, so the game technically goes on forever right now. I plan to do that first thing tomorrow.

I added another weapon and a bunch more items, which will be one of the main things that keep repeated plays interesting. The weapon was a spear (technically a bident) which can damage enemies at range.

I’ll keep the items a mystery, but I will say that items all have positive effects. This choice came from two places. The first is that in a “one HP” roguelike, you cannot have much by way of negative effects without causing an unfair death.

The second is that I really want everything identifiable on sight, because part of the strategy is to make hard choices about what to use and what to keep. You have a weapon slot and a spacebar item slot (in my head these correspond to what you hold in your two hands).

Since the maze has no backtracking, you can’t bring two really good items with you. Items are fairly sparse, which I like. The game should be beatable without any, so finding something will make the run more enjoyable and safer.

I also implemented the dragon I drew last time into the game. Again, today’s changes were mostly behind the scenes, so there isn’t really a screenshot that helps you see what has happened. Instead, I’ll post a video with sample gameplay.

Don’t watch if you want to experience it yourself. I’ve gotten a lot of standard movement patterns down after playing it a bunch, so I might spoil how to handle lots of situations.

7DRL Dev Log Day 3

Yesterday I got through the hardest coding section of the game. I separated out the zone logic into it’s own class, so I could make repeat calls and chain together the zones to make a large labyrinth.

I also implemented experience points, level up, more of the GUI, and fixed many of the bugs I had from day one like spawining in an instant death situation, enemies occupying the same square, and a permanent stun effect.

Also, there is an annoying bug where the game crashes if I put sound in it on some machines (maybe it is just Windows 7?). I may end up not doing sound and music to be safe.

Most of these changes were under the hood, so it doesn’t look much different right now:

theseus_screen3

Today I got hung up on a few technicalities with C#. I guess I should have practiced some basics more before this week to avoid wasting time. Apparently, when you iterate through a list, you can only read the objects you iterate over (you can’t set them to new values). This caused lots of confusion for a bit while I tried to work out what was wrong.

I had a pretty good day. I realized lots of stuff was hard to see/read with the fancy vector swept background. I’m not sure I like what I’ve switched to, but it is certainly better from a readability standpoint, and makes the various sprites pop a lot more:

theseus_screen4

Other than that, I did a lot of art again. I added some items and a new weapon. This meant drawing the various places the item could be, and then redoing the main character sprite with the new weapon in its various positions. The weapon has a pretty cool mechanic attached: it is a damage multiplier, so super over-powered, but it is heavy, so it costs a turn to turn around while holding it. This drawback may make it too risky to be worth it putting in an interesting choice.

I also completed a new enemy, a dragon. It is the best thing I’ve ever made with pixel art (I only started learning a few weeks ago):

dragonsample

I animated him by just bobbing up and down and breathing fire so that I wouldn’t have to deal with the intricacies of moving the wings. It’s coming together and finally feels like a real game.

7DRL Dev Log Day 1

Today the 7DRL began (for me at least)!

I got through everything I wanted to with time to spare. I mostly built the art today and got some stuff displayed. I settled on a 960×545 window (unchangeable, because I don’t want to deal with scaling at the moment) with each segment of the maze being a 40×20 grid of 64×64 tiles. Once you leave a segment, the previous part will change.

Today I drew the title screen, a “lose” screen, a “win” screen, the background, 3 wall tiles, 3 floor tiles, a fire elemental enemy, a raven enemy, and some of the GUI (health, weapon, and item get displayed).

Here’s two screen shots:

theseus_screen1

theseus_screen2

Both of the enemy types are fully animated, but the fire could use some work later if there is time. I’ve implemented movement patterns for the two enemies, and trigger win/lose conditions if you clear the area (not the real win condition, just testing!) or die.

Overall, this was a wildly successful day, but all of the hard coding is ahead of me, not to mention, I’ve only thought of one weapon and one item so far.