Learning C With Game Concepts/Designing A Roleplaying Game

From Wikibooks, open books for an open world
Jump to navigation Jump to search

Now that we've covered the basics of compilation and modularity (we'll refine that section later) lets move on to game design. Our game will be implemented using the terminal and will not rely on any third party libraries, save for the ones that come with C by default. We'll take a Bottom-Up approach to creating our Role-playing Game (hereafter referred to as an RPG).

Specifications[edit | edit source]

When undergoing a new project of some complexity, it is important to do some brainstorming about what your program will do and how you'll implement it. While that may sound painfully obvious, it is often tempting to jump right into coding and implement ideas as they pop into your head. It isn't until the code becomes hundreds of lines long does it become clear that organizing your thoughts is necessary. The initial brainstorming phase takes the main idea, an RPG in this case, and breaks it down into more elementary pieces. For each element, we either break it down into still smaller elements, or give a short summary of how we might implement it. In a commercial environment, this brainstorming session produces a Specification Document.

At the heart of every RPG, we have players.

Players shouldː

  • Have stats that tell us about their abilities.
  • Be able to interact with each other (i.e. Talking, Fighting)
  • Be able to move from one location to another.
  • Be able to carry items.

Statsː Easy. Just a variable telling us what the stat is for (like, health) and containing an integer value.

Talkingː To facilitate conversation, players need scripted dialog. This dialog could be stored with the main character, or the person with whom the main character will interact, and in the case of the latter, must be accessible by the main character.

Fightingː A function, that when given a player (to attack) initiates a battle sequence, that persists until someone retreats or a player's health is reduced to 0.

Mapping: A vast and epic journey involves many locations. Each location node tells us what the player sees once he reaches that location, and where he can go from there. Each location node has the same structure. This new node will be structured the same as the first, but contain different information. Each location node is assigned a unique location ID that tells the computer where another location node can be found. "Where he can go" is traditionally an array of up to 10 location IDs, to allow the player to go up to 10 directions -- up, down, north, south, east, northeast, etc.

Movingː A player should contain a node for a linked list or binary tree. The first location ID tells us where the player is currently. The second location ID tells us where the player came from (so players can say "go back"). Moving will involve changing the player's location (Swamp, let's say), to the location ID of another node (Forest). If that sounds confusing, don't worry, I'll make pictures to illustrate the concept. ː)

Inventoryː Inventory will start out as a doubly linked list. An item node contains an item (Health Potion), the number of that item, a description of the item, and two links. One link to the previous item in the list, and a second link to the next item in the list.

This preliminary specification acts as a blueprint for the next phase, the actual coding portion. Now that we've broken the main idea into smaller elements, we can focus on creating separate modules that enable these things. As an example, we will implement the player and player functions in the Main file for testing. Once we're positive that our code is working properly, we can migrate our datatypes and functions into a Header file, which we'll call whenever we want to create and manipulate players. Doing this will significantly reduce the amount of code to look at in the main file, and keeping player functions in the player header file will give us a logical place to look for, add, remove, and improve player functions. As we progress, we may think of new things to add to our specification.

Player Implementation[edit | edit source]

Because a player is too complex to represent with a single variable, we must create a Structure. Structures are complex datatypes that can hold several datatypes at once. Below, is a rudimentary example of a player structure.

struct playerStructure {
    char name[50];
    int health;
    int mana;
};

Using the keyword struct, we declare a complex datatype called playerStructure. Within the curly braces, we define it with all the datatypes needs to hold for us. This structure can be used to create new structures just like it. Let's use it to make a Hero and display his stats.

player.c

#include <stdio.h>
#include <string.h>

struct playerStructure {
    char name[50];
    int health;
    int mana;
} Hero;

// Function Prototype
void DisplayStats (struct playerStructure Target);

int main() {
    // Assign stats
    strcpy(Hero.name, "Sir Leeroy");
    Hero.health = 60;
    Hero.mana = 30;

    DisplayStats(Hero);
    return(0);
}

// Takes a player as an argument and prints their name, health, and mana. Returns nothing.
void DisplayStats (struct playerStructure Target) {
    // We don't want to keep retyping all this.
    printf("Name: %s\nHealth: %d\nMana: %d\n", Target.name, Target.health, Target.mana);
}

Let's review what our code does. We've included a new standard library called <string.h> which contains functions that are helpful in working with strings. Next, we define the complex datatype playerStructure and immediately declare a playerStructure called Hero right after it. Be aware, the semicolon is always necessary after defining the struct. Unlike higher level languages, strings cannot be assigned in C using the assignment operator =, only the individual characters that make up the string can be assigned. Since name is 50 characters long, imagine that we have 50 blank spaces. To assign "Sir Leeroy" to our array, we must assign each character to a blank space, in order, like so:

name[0] = 'S'

name[1] = 'i'

name[2] = 'r'

name[3] = ' '

name[4] = 'L'

name[5] = 'e'

name[6] = 'e'

name[7] = 'r'

name[8] = 'o'

name[9] = 'y'

name[10] = '\0' // End of string marker

The function Strcpy() essentially loops through the array until it reaches the end of string marker for either arguments and assigns characters one at a time, filling the rest with blanks if the string is smaller than the size of the array we're storing it in.

The variables in our structure, Player, are called members, and they are accessed via the syntax struct.member.

Now our game would be boring, and tranquil, if it just had a Hero and no enemies. In order to do add more players, we would need to type "struct playerStructure variableName" to declare new players. That's tedious and prone to mistakes. Instead, it would be much better if we had a special name for our player datatype that we could call as wisfully as char or int or float. That's easily done using the keyword typedefǃ Like before, we define the complex datatype playerstructure, but instead of declaring a playerStructure afterward, we create a keyword that can declare them whenever we want.

player2.c

#include <stdio.h>
#include <string.h>

typedef struct playerStructure {
    char name[50];
    int health;
    int mana;
} player;

// Function Prototype
void DisplayStats (player Target);
 
int main () {
    player Hero, Villain;
    
    // Hero
    strcpy(Hero.name, "Sir Leeroy");
    Hero.health = 60;
    Hero.mana = 30;
 
    // Villain
    strcpy(Villain.name, "Sir Jenkins");
    Villain.health = 70;
    Villain.mana = 20;

    DisplayStats(Hero);
    DisplayStats(Villain);
    return(0);
}

// Takes a player as an argument and prints their name, health, and mana. Returns nothing.
void DisplayStats (player Target) {
    printf("Name: %s\nHealth: %d\nMana: %d\n", Target.name, Target.health, Target.mana);
}

There is still the problem of creating players. We could define every single player who will make an appearance in our game at the start of our program. As long as the list of players is short, that might be bearable, but each of those players occupies memory whether they are used or unused. Historically, this would be problematic due to the scarcity of memory on old computers. Nowadays, memory is relatively abundant, but for the sake of scalability, and because users will have other applications running in the background, we'll want to be efficient with our memory usage and use it dynamically.

Dynamically allocating memory is accomplished through the use of the malloc, a function included in <stdlib.h>. Given a number of bytes to return, malloc finds unused memory and hands us the address to it. To work with this memory address, we use a special datatype called a pointer, that is designed to hold memory addresses. Pointers are declared like any other datatype, except we put an asterisk (*) in front of the variable name. Consider this line of codeː

player *Hero = malloc(sizeof(player));

This is the standard way of declaring a pointer and assigning it a memory address. The asterisk tells us that instead of declaring a player, with a fixed, unchangeable address in memory, we want a variable that can point to any player's address. Uninitialized pointers have NULL as their value, meaning they don't point to an address. Since it would be difficult to memorize how many bytes are in a single datatype, let alone our player structure, we use the sizeof function to figure that out for us. Sizeof returns the number of bytes in player to malloc, which finds enough free memory for a player structure and returns the address to our pointer.

If malloc returns the memory address 502, Hero will now point to a player who exists at 502. Pointers to structures have a unique way of calling members. Instead of a period, we now use an arrow (->).

player *Hero = malloc(sizeof(player));
strcpy(Hero->name, "Leeroy");
Hero->health = 60;
Hero->mana = 30;

Remember, pointers don't contain values like integers and chars, they just tell the computer where to find those values. When we change a value our pointer points to, we're telling the computer "Hey, the value I want you to change lives at this address (502), I'm just directing traffic." So when you think of pointers, think "Directing Traffic". Here's a table to show what pointer declarations of various types meanː


Declaration What it means.
char *variable Pointer to char
int *variable Pointer to int
float *variable Pointer to float
player *variable Pointer to player
player **variable Pointer to a pointer to player

Now that we're using pointers, we can write a function to dynamically allocate players. And while we're at it, let's add some new ideas to our specification.

Players shouldː

  • Have stats that tell us about their abilities. DONE
  • Be able to interact with each other (i.e. Talking, Fighting)
  • Be able to move from one location to another.
  • Be able to carry items.
  • Have classes (Warrior, Mage, Ranger, Accountant) NEW
    • Classes have unique stats when they are created. Exampleː Warriors have high health, mages have low health. NEW

dynamicPlayers.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Classes are enumerated. WARRIOR = 0; RANGER = 1, etc.
typedef enum ClassEnum  {
  WARRIOR,
  RANGER,
  MAGE,
  ACCOUNTANT
} class;

typedef struct playerStructure {
  char name[50];
  class class;
  int health;
  int mana;
} player;

// Function Prototypes
void DisplayStats(player *target);
int SetName(player *target, char name[50]);
player* NewPlayer(class class, char name[50]);    // Creates player and sets class.

int main() {
  player *Hero = NewPlayer(WARRIOR, "Sir Leeroy");
  player *Villain = NewPlayer(RANGER, "Sir Jenkins");

  DisplayStats(Hero);
  DisplayStats(Villain);
  return(0);
}

// Creates player and sets class.
player* NewPlayer(class class, char name[50]) {
  // Allocate memory to player pointer.
  player *tempPlayer = malloc(sizeof(player));
  SetName(tempPlayer, name);

  // Assign stats based on the given class.
  switch(class) {
  case WARRIOR:
    tempPlayer->health = 60;
    tempPlayer->mana = 0;
    tempPlayer->class = WARRIOR;
    break;
  case RANGER:
    tempPlayer->health = 35;
    tempPlayer->mana = 0;
    tempPlayer->class = RANGER;
    break;
  case MAGE:
    tempPlayer->health = 20;
    tempPlayer->mana = 60;
    tempPlayer->class = MAGE;
    break;
  case ACCOUNTANT:
    tempPlayer->health = 100;
    tempPlayer->mana = 100;
    tempPlayer->class = ACCOUNTANT;
    break;
  default:
    tempPlayer->health = 10;
    tempPlayer->mana = 0;
    break;
  }

  return(tempPlayer); // Return memory address of player.
}

void DisplayStats(player *target)  {
  printf("%s\nHealth: %d\nMana: %d\n\n", target->name, target->health, target->mana);
}

int SetName(player *target, char name[50]) {
  strcpy(target->name, name);
  return(0);
}

Before we move on to the next major development, you'll want to modularize what you've written. Start by making two header files, one named "gameProperties.h" and another called "players.h". In the game properties file, place your playerStructure and classEnum typedefs. The datatypes defined here will have the possibility of appearing in any other headers we may create. Therefore, this will always be the first header we call. Next, all functions related to creating and modifying players, as well as their prototypes, will go in our players header file.

Fight System[edit | edit source]

Rome wasn't built in a day and neither are good fight systems, but we'll try our best. Now that we have an enemy we are obligated to engage him in a friendly bout of fisticuffs. For our players to fight, we'll need to include two additional stats in our player structure, Attack and Defense. In our specification, all that our Fight function entailed was an argument of two players, but with further thought, lets do damage based on an EffectiveAttack, which is Attack minus Defense.

In the gameProperties header, modify playerStructure for two more integer variables, "attack" and "defense".

gameProperties.h

// Classes are enumerated. WARRIOR = 0; RANGER = 1, etc.
typedef enum ClassEnum  {
    WARRIOR,
    RANGER,
    MAGE,
    ACCOUNTANT
} class;

// Player Structure
typedef struct playerStructure {
    char name[50];
    class class;
    int health;
    int mana;
    int attack;    // NEWː Attack power.
    int defense;   // NEWː Resistance to attack.
} player;

In the players header file, modify the case statements to assign values to the attack and defense attributes.

players.h

// Creates player and sets class.
player* NewPlayer(class class, char name[50]) {
    // Allocate memory to player pointer.
    player *tempPlayer = malloc(sizeof(player));
    SetName(tempPlayer, name);

    // Assign stats based on the given class.
    switch(class) {
    case WARRIOR:
        tempPlayer->health = 60;
        tempPlayer->mana = 0;
        tempPlayer->attack = 3;
        tempPlayer->defense = 5;
        tempPlayer->class = WARRIOR;
        break;
    case RANGER:
        tempPlayer->health = 35;
        tempPlayer->mana = 0;
        tempPlayer->attack = 3;
        tempPlayer->defense = 2;
        tempPlayer->class = RANGER;
        break;
    case MAGE:
        tempPlayer->health = 20;
        tempPlayer->mana = 60;
        tempPlayer->attack = 5;
        tempPlayer->defense = 0;
        tempPlayer->class = MAGE;
        break;
    case ACCOUNTANT:
        tempPlayer->health = 100;
        tempPlayer->mana = 100;
        tempPlayer->attack = 5;
        tempPlayer->defense = 5;
        tempPlayer->class = ACCOUNTANT;
        break;
    default:
      tempPlayer->health = 10;
        tempPlayer->mana = 0;
        tempPlayer->attack = 0;
        tempPlayer->defense = 0;
        break;
    }

    return(tempPlayer); // Return memory address of player.
}

void DisplayStats(player *target)  {
  printf("%s\nHealth: %d\nMana: %d\n\n", target->name, target->health, target->mana);
}

int SetName(player *target, char name[50]) {
  strcpy(target->name, name);
  return(0);
}

Finally, include your header files in your main program. Instead of sharp brackets <> we use quotation marks instead. If the headers are located in the same folder as the executable, you only need to provide the name. If your header file is in a folder somewhere else, you'll need to provide the full path of the file location.

Let's also develop a rudimentary fight system to make use the attack and defense attributes.

player3.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "gameProperties.h"
#include "players.h"

// Function Prototype
int Fight (player *Attacker, player ̈*Target);
 
int main () {
    player *Hero = NewPlayer(WARRIOR, "Sir Leeroy");
    player *Villain = NewPlayer(RANGER, "Sir Jenkins");

    DisplayStats(Villain);   // Before the fight.
    Fight(Hero, Villain);    // FIGHTǃ
    DisplayStats(Villain);   // After the fight.
    return(0);
}

int Fight (player *Attacker, player *Target) {
    int EffectiveAttack; // How much damage we can deal is the difference between the attack of the attacker
                         // And the defense of the target. In this case 5 - 1 = 4 = EffectiveAttack.
 
    EffectiveAttack = Attacker->attack - Target->defense;
    Target->health = Target->health - EffectiveAttack;
    return(0);
}

If we run compile and run this we get this outputː

Name: Sir Jenkins
Health: 35
Mana: 0
Name: Sir Jenkins
Health: 34         // An impressive 1 damage dealt. 
Mana: 0

TODOː Adjust class stats to something more diverse.

Now that we've figured out how to deal damage, lets expand on our earlier specificationː

Fight() shouldː

  • Loop until someones' health reaches zero, retreats, or surrenders.
  • Display a menu of options to the user before getting input.
    • Attack, Defend, Use Item, and Run should be basic choices.
  • Tell us if we gave the wrong input.
  • Give both sides a chance to act. Possibly by swapping the memory address of the Attacker and Target.
    • This means that we'll need to distinguish between the User's player and non-user players.
    • We may need to revise the swapping idea later if fights involve more than two characters. Then we might use some kind of list rotation.
      • Games that use speed as a factor (Agility), probably build lists based on stats before the fight to determine who goes first.

I'll refrain from posting the whole program when possible but I encourage you to continue making incremental changes and compiling/running the main program as we go along. For the battle sequence, we will modify the Fight function to loop until the Target's health reaches 0, whereupon a winner is named. A user interface will be provided by a "Fight Menu" which will pair a number with an action. It is our responsibility to modify this menu as new actions are added and make sure each individual action works when called.

When the User chooses an action, the associated number is handed to a Switch, which compares a given variable to a series of Cases. Each case has a number or character (strings aren't allowed) that is used for the aforementioned comparison. If Switch finds a match, it evaluates all the statements in that Case. We must use the keyword break to tell the switch to stop evaluating commands, or else it will move the to next case and execute those statements (sometimes that's useful, but not for our purpose). If Switch cannot match a variable to a case, then it looks for a special case called default and evalutes it instead. We'll always want to have a default present to handle unexpected input.

int Fight(player *Attacker, player *Target) {
    int EffectiveAttack = Attacker->attack - Target->defense;
 
    while (Target->health > 0) {
        DisplayFightMenu();
        
        // Get input.
        int choice;
        printf(">> "); // Indication the user should type something.
        fgets(line, sizeof(line), stdin);
        sscanf(line, "%d", &choice);
 
        switch (choice) {
        case 1:
            Target->health = Target->health - EffectiveAttack;
            printf("%s inflicted %d damage to %s.\n", Attacker->name, EffectiveAttack, Target->name);
            DisplayStats(Target);
            break;
        case 2:
            printf("Running away!\n");
            return(0);
        default:
            printf("Bad input. Try again.\n");
            break;
        }
    }
   
   // Victoryǃ
   if (Target->health <= 0) {
     printf("%s has bested %s in combat.\n", Attacker->name, Target->name) ;
   }
 
      return(0);
}
 
void DisplayFightMenu () {
printf("1) Attack\n2) Run\n");
}

Testing the integrity of the program requires running it a few times after compilation. First we can see that if we enter random input like "123" or "Fish" we invoke the default case and are forced to pick another answer. Second, entering 2 will cause us to run away from the fight. Third, if we continue to enter 1, eventually Sir Leeroy will whittle down all of Sir Jenkin's health and be declared winner. Modifying the attack value on Sir Leeroy can help if you're impatient ː)

However, Sir Jenkins is still unable to defend himself, which makes for a very unsporting match. Even if Sir Jenkins was given a turn, the user would still be prompted to act on his behalf. The turn-based problem is solved by the idea proposed in the specification, that we swap the memory addresses of the Attacker and Target pointers on each loop. The solution to the problem of autonomy is to add a new property to our player structure, namely, a bool. A bool has a binary value, true or false, and, for us, it answers the simple question "To Autopilot or not to autopilot?". With the autopilot bool set to true, the Fight function, when modified by us to check for it, will know that they must automate the actions of these characters. To use bool datatypes, we need to include a new header called <stdbool.h>. Bools are declared with the bool keyword and can only be assigned true or false values.

Add the following line underneath int defense in your playerStructure from the "gameProperties.h".

 bool autoPilot;

Next, add this snippet of code to the NewPlayer function from the "Players.h" below the call to SetName.

static int PlayersCreated = 0; // Keep track of players created.
if (PlayersCreated > 0) {
    tempPlayer->autoPilot = true;
} else {
    tempPlayer->autoPilot = false;
}
++PlayersCreated;

The above code creates a persistent variable using the keyword static. Normally, once a function is called, the local variables disappear. By contrast, static variables maintain their value beyond the life of the function, and when a function starts again, it's value isn't reset. Autopilot is only turned on for players created after the first, main character.

That done, consider the following program. We've added our bools and IF statements to determine whether the player needs automating or prompting. The victory IF is moved inside the while loop, and declares victory if the condition is met, else, it swaps players for the next loop.

player4.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include "gameProperties.h"
#include "players.h"

// Function Prototype
void DisplayStats(player Target);
int Fight(player *Attacker, player *Target);
void DisplayFightMenu(void);

// Global Variables
char line[50];	  // This will contain our input.

int main () {
    player *Hero = NewPlayer(WARRIOR, "Sir Leeroy");
    player *Villain = NewPlayer(RANGER, "Sir Jenkins");
 
    DisplayStats(Villain);   // Before the fight.
    Fight(Hero, Villain);    // FIGHTǃ

  return(0);
}

int Fight(player *Attacker, player *Target) {
   int EffectiveAttack = Attacker->attack - Target->defense;

   while (Target->health > 0) {

      // Get user input if autopilot is set to false.
      if (Attacker->autoPilot == false) {
	 DisplayFightMenu();

	 int choice;
	 printf(">> "); // Sharp brackets indicate that the user should type something.
	 fgets(line, sizeof(line), stdin);
	 sscanf(line, "%d", &choice);

	 switch (choice) {
	   case 1:
	     Target->health = Target->health - EffectiveAttack;
	     printf("%s inflicted %d damage to %s.\n", Attacker->name, EffectiveAttack, Target->name);
	     DisplayStats(Target);
	     break;
	   case 2:
	     printf("Running away!\n");
	     return(0);
	   default:
	     printf("Bad input. Try again.\n");
	     break;
	 }
      } else {
         // Autopilot. Userless player acts independently.
	 Target->health = Target->health - EffectiveAttack;
	 printf("%s inflicted %d damage to %s.\n", Attacker->name, EffectiveAttack, Target->name);
	 DisplayStats(Target);
      }

      // Once turn is finished, check to see if someone has one, otherwise, swap and continue.
      if (Target->health <= 0) {
	printf("%s has bested %s in combat.\n", Attacker->name, Target->name) ;
      } else {
	 // Swap attacker and target.
	 player *tmp = Attacker;
	 Attacker = Target;
	 Target = tmp;
      }
   }

   return(0);
}

void DisplayFightMenu (void) {
  printf("1) Attack\n2) Run\n");
}

Now that we've created a very rudimentary system for fighting, it is time once again to modularize. Take the Fight and DisplayFightMenu functions and put them in a new header file called "fightSys.h". This new header will contain all functions related to fighting and will be included in the next iteration of our Main program.