Learning C With Game Concepts/Printable version
This is the print version of Learning C With Game Concepts You won't see this message or any elements not part of the book's content when you print or preview this page. |
The current, editable version of this book is available in Wikibooks, the open-content textbooks collection, at
https://en.wikibooks.org/wiki/Learning_C_With_Game_Concepts
Introduction
- This book attempts to introduce the C Language to new users while making the learning process interesting and giving them code to play with and modify as they continue educating themselves. The author of this book is a novice C programmer, writing for beginners, so I may not always use the best practice. Users who are interested in a more indepth understanding of the C language are recommended to check out the excellent C_Programming Wikibook.
Code Disclaimer
[edit | edit source]- I'm new to sharing code with others, so I feel I should state that the code included in this Wikibook is generated by the author and any resemblance to code out in the wild is not intentional unless it is something basic like the implementation of a data structure (linked list, heap, etc).
Work Environment
[edit | edit source]This book recommends beginners use a Linux distribution, such as Linux Mint or Ubuntu. Although I'm unfamiliar with C programming on Windows, Microsoft's Visual C++ compiler should be able to compile C code, if it is available to you. The C language is easy to start learning in the Unix/Linux environment because the Gnu C Compiler (GCC) is included by default on most distributions. GCC is a command line utility that transforms C source code into machine instructions (binary). For writing your code, you'll need a Text Editor or Integrated Development Environment (IDE). An easy to use IDE is Geany, which you can find using your package manager on Linux.
Text Editors can be as basic as notepad or as complex as Vim and Emacs.
A Basic Program
[edit | edit source]Let's cover variables and standard IO in one fell swoop.
intro.c
#include <stdio.h>
int main() {
char input[50]; // # of characters we can read from standard input.
char name[50]; // variable to contain a name
printf("What is your name good sir?\n");
fgets(input, sizeof(input), stdin);
sscanf(input, "%s", &name);
printf("Ah, %s, a pleasure to meet you.\n", name);
return(0);
}
Before we compile this, let's review what our code does.
Header Files
[edit | edit source]The #include statement at the start of the program lets the compiler know that we want to use a library. Libraries in C are called Header files and end in the ".h" file extension. The C language contains standard libraries to perform all sorts of common tasks. STDIO, as the name implies, contains various functions for printing and reading.
Main Function / Statements
[edit | edit source]At the heart of every C program is the Main function. It contains the body of the program and is the first function called when the program is started. All functions follow this syntax:
datatype functionName (datatype argument) {
statement;
return (datatype); // The datatype before the function name lets the
// compiler know this is the kind of variable we will return.
}
A statement can be a variable declaration ( x = 100; ) or a function call ( CallingFunction(); ). All statements end with a semicolon and it is good practice to give every statement its own line. It's also important to check your code carefully to make sure you aren't missing any semicolons.
Data Types
[edit | edit source]There are 3 basic data types in C that we must use to declare our variables. We declare them using data types so the compiler knows exactly what each variable contains. They include:
- char (example 'a' | a single character)
- int (example 123 | a whole number)
- float (example 12.345 | a fractional number)
In our example we use char, but add brackets and a number after it. The syntax Variable[#] lets the compiler know that instead of a single character, we want an Array (string of characters).
Input/Output
[edit | edit source]We are using 3 functions from STDIO: printf, fgets, and sscanf. Our stdin and stdout comes from the command line where we will run the code.
Printf prints a string (as denoted by quotation marks) to standard output. If placeholders are present in the string (%letter) it will also print variables. Fgets reads in a line from standard input and puts it in our variable called Input. Sscanf looks at our variable called input and transfers it over to the variable called name. We'll want to use Input again when we get user input, so we can't leave our information in there or we'll overwrite it!
Returns
[edit | edit source]Return statements can return 1 datatype or variable from a function. The simplest example of why we use returns is to see if something is true or false. If you have a function called isCapital and call it on a letter, for example, isCapital('a') we would return(False). If we called isCapital('B'), we'd have our function return(True). The function that called the isCapital function would be designed to make a decision based on the value we returned.
Functions don't need to return a value though. Sometimes you may see 'void' used before a function. This means that the function doesn't return a datatype at all.
Compilation
[edit | edit source]Now that we've covered what our code does, let's compile and run it. Navigate to the folder that your C program is contained in. If you are unfamiliar with BASH, you'll need to learn about the cd and ls commands. From the command terminal type:
gcc intro.c -o intro
This command calls the compiler (GCC) to compile Intro.c and output an executable called Intro. If for some reason your file is not set as an executable after compilation, you can type:
chmod +x intro
To run your executable, type:
./intro
That wasn't as simple as printing "Hello World" but hopefully it wasn't too difficult to follow. As you continue reading, things should start to make more sense as you begin recognizing common patterns in programming.
Experiment #1
[edit | edit source]Before we move on, take a moment to tweak our Intro program. Declare an int variable called age. Change the first print statement to ask your age and use the integer placeholder %d to print your age in the second print statement.
A Modular Lifestyle
[edit | edit source]It would be very difficult to read large programs if we kept all of our functions and variables in one file. Suppose also, that we create a great function and want to use it in all of our programs, how would we do that?
If we want our reusable function to do what our last program did, we just take all the statements from before, put them in a function with a unique name, and place them in a header file.
greet.h
#include <stdio.h>
int Greet() {
char input[50]; // # of characters we can read from standard input.
char name[50]; // variable to contain a name
printf("What is your name good sir?\n");
fgets(input, sizeof(input), stdin);
sscanf(input, "%s", &name);
printf("Ah, %s, a pleasure to meet you.\n", name);
return(0);
}
Greet.h can now be included in our program like STDIO, and we don't even need to compile it before using it. The only difference is that instead of sharp brackets <> we use quotes "". If your main program and your header file are in the same folder, #include "Greet.h" is all you need to specify. If they are in different folders, you'll need to provide the fullpath for the compiler #include "fullpath/Greet.h"
main_prog.c
#include "greet.h"
int main() {
Greet(); // We declared this function in greet.h
return(0);
}
When main_prog.c is compiled, the compiler looks for the header files and includes them in the compilation.
gcc main_prog.c -o main_prog
main_prog, the executable, will contain the machine instructions for the main program and the Greet file.
./main_prog
What is your name good sir?
Theodore Roosevelt
Ah, Theodore Roosevelt, a pleasure to meet you.
Designing A Roleplaying Game
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.
Inventory System
Inventory System
[edit | edit source]There is nothing quite so satisfying as equipping one's character. Most of the time, you start out at level 1, wearing rags and shackles. After being released from the local dungeon for one reason or another, you meet a Haberdasher with a heart of gold. He gives you some odd jobs so you can earn enough copper to eat. After weeks of punching things, fetching things, running from things, and still without two copper pieces to scratch together, you at last come across the "Unremarkable Sword of Rusty Might". Huzzahǃ It nothing compared to what the City Guard is using, but now those sewer rats shall feel your wrath. So let's get to itǃ
An Inventory Shouldː
- Contain a list of items.
- We should be able to ADD and REMOVE items from the list, as well as FIND items.
- Similar items should be aggregated. When a similar item is added, quantity is increased, not list size.
- Players should have their own inventory, as should chests and other containers.
An Item Shouldː
- Have a name and description.
- Health and mana.
- These are the restorative properties of potions. 20 Health heals 20 health points if consumed.
- Similar items should be aggregated. When a similar item is added, quantity is increased, not list size.
- Have the option of multiple uses.
- Have an ID that uniquely identifies it.
Putting the above specification into practice, we start by defining the kinds of items available, via an enumeration. In the future, we may want to import a list of items from a file, rather than keeping all of them with our source code. However, it's best to keep things small for testing purposes.
// 0 based.
enum itemNumber {
HEALTH_POTION,
MANA_POTION
};
typedef struct ItemStructure {
char name[50];
char description [100];
int health;
int mana;
int quantity;
int usesLeft;
int id;
} item;
// Doubly linked list for items.
typedef struct itemNodeStructure {
item *current; // Pointer to current item.
struct itemNodeStructure *previous; // Pointer to previous item in the list.
struct itemNodeStructure *next; // Pointer to the next item in the list.
} itemNode;
Each item that we create will be embedded in an item node. A node is a single unit which can be linked to other nodes in order to form a data structures such as lists and trees. In this case, the data structure is a doubly linked list. As you can see, the itemNodeStructure points forwards, backwards, and to its own item. When defining the itemNodeStructure, it is necessary to use "struct itemNodeStructure" to declare the pointers because the itemNode typedef is not yet in effect and the compiler will not understand.
Going back to ye olde playerStructure datatype, we add a new value to it.
itemNode *inventory;
Because the functions required for a basic inventory setup are somewhat involved, it cannot be broken up into more digestible pieces. My code is very likely not the most elegant solution to the problem, so there is room for improving the clarity. For now, if you want to understand how the functions for the inventory work, you'll need to read through the comments until you can identify what each piece of code is doing.
inventory.c
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
#include "gameProperties.h"
#include "players.h"
#include "fightSys.h"
// 0 based.
enum itemNumber {
HEALTH_POTION,
MANA_POTION
};
typedef struct ItemStructure {
char name[50];
char description [100];
int health;
int mana;
int quantity;
int usesLeft;
int id;
} item;
// Doubly linked list for items.
typedef struct itemNodeStructure {
item *current; // Pointer to current item.
struct itemNodeStructure *previous; // Pointer to previous item in the list.
struct itemNodeStructure *next; // Pointer to the next item in the list.
} itemNode;
// Function Prototypes
int DisplayInventory(itemNode *node);
int AddItem(itemNode *inventory, enum itemNumber number);
int main () {
player *Hero = NewPlayer(WARRIOR, "Sir Leeroy");
player *Villain = NewPlayer(WARRIOR, "Sir Jenkins");
AddItem(Hero->inventory, HEALTH_POTION);
AddItem(Hero->inventory, HEALTH_POTION);
AddItem(Hero->inventory, MANA_POTION);
DisplayInventory(Hero->inventory);
return(0);
}
// Exampleː DisplayInventory(Player->inventory);
int DisplayInventory(itemNode *node) {
// While there is an item present, print said item.
while (node->current != NULL) {
printf("Name: %s\n", node->current->name);
printf("Description: %s\n", node->current->description);
printf("Health: %d\n", node->current->health);
printf("Mana: %d\n", node->current->mana);
printf("Uses Left: %d\n", node->current->usesLeft);
printf("Quantity: %d\n\n", node->current->quantity);
// If the node points to another node, go to it and print it's item. Otherwise, end loop.
if (node->next != NULL) {
node = node->next; // Move to next node.
} else {
return(0); // Loop ends
}
}
// Inventory pointer is NULL, there are no items.
printf("Inventory is empty.\n");
return(0);
}
// FIND ITEM
// Used in the functionsː AddItem and RemoveItem.
// Can't return 0 because it's interpreted as an int. Return NULL for functions
// that are supposed to return pointers.
itemNode* findItem (itemNode *node, enum itemNumber number) {
// If the node is NULL, it's an empty list. End function.
if (node == NULL) {
return(NULL);
}
// While the current node has an item.
while (node->current != NULL) {
// Compare that item's id to our number.
// If it is a match, return the memory address of that node.
if (node->current->id == number) {
return(node);
}
// If the current item doesn't match and there
// is another node to look examine, move to it.
if (node->next != NULL) {
node = node->next;
} else {
return(NULL); // List ends.
}
}
return(NULL); // List is empty.
}
// Use Exampleː AddItem(Hero->inventory, HEALTH_POTION);
int AddItem(itemNode *node, enum itemNumber number) {
itemNode *previousNode;
itemNode *searchResult;
// See if item already exists.
searchResult = findItem(node, number);
if (searchResult != NULL) {
searchResult->current->quantity += 1; // Increase quantity by one and end function.
return(0);
}
// Generate item if it doesn't exist.
// This requires allocating memory and increasing
// the size of the linked list.
item *object = malloc(sizeof(item)); // Allocating memory for item.
// Just like our class enumeration, our item names are variables
// that stand for numbers. Because cases in C can't use strings,
// this method makes them much more readable.
switch(number) {
case HEALTH_POTION:
strcpy(object->name, "Health Potion");
strcpy(object->description, "Drinkable item that heals 20 health points.");
object->health = 20;
object->usesLeft = 1;
object->quantity = 1;
object->id = number; // ID and ItemNumber are the same.
break;
case MANA_POTION:
strcpy(object->name, "Mana Potion");
strcpy(object->description, "Drinkable item that heals 20 mana.");
object->usesLeft = 1;
object->quantity = 1;
object->mana = 20;
object->id = number;
break;
}
// Now that our object has been created, we must find free space for it.
// If the current node is unused allocate memory and assign item.
if (node->current == NULL) {
node->current = object;
// If the current node is occupied, check the next node.
// If the next node doesn't exist, then we must allocate memory
// to the next pointer.
} else if (node->next == NULL) {
node->next = malloc(sizeof(itemNode)); // Allocate memory to the next pointer.
previousNode = node; // Store location of current node.
node = node->next; // Move to the next node.
node->previous = previousNode; // Link the current node to the previous one.
node->current = object; // Assign item to the current node.
} else {
// If current and next node are occupied, search for the last node.
// The last node will have an empty "next" spot.
while (node->next != NULL) {
node = node->next;
}
node->next = malloc(sizeof(itemNode)); // Allocate memory to the next pointer.
previousNode = node; // Store location of current node.
node = node->next; // Move to the next node.
node->previous = previousNode; // Link the current node to the previous one.
node->current = object; // Assign item to the current node.
}
return(0);
}
inventoryFinished.c
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
// 0 based.
enum itemNumber {
HEALTH_POTION,
MANA_POTION
};
typedef struct ItemStructure {
char name[50];
char description [100];
int health;
int mana;
int quantity;
int usesLeft;
int id;
} item;
// Doubly linked list for items.
typedef struct itemNodeStructure {
item *current; // Pointer to current item.
struct itemNodeStructure *previous; // Pointer to previous item in the list.
struct itemNodeStructure *next; // Pointer to the next item in the list.
} itemNode;
typedef struct playerStructure {
char name[50];
int health;
int mana;
int attack;
int defense;
bool autoPilot;
itemNode *inventory;
} player;
// Function Prototype
void DisplayStats(player *target);
int DisplayInventory(itemNode *node);
int AddItem(itemNode *inventory, enum itemNumber number);
int RemoveItem(itemNode *inventory, enum itemNumber number);
itemNode* findItem(itemNode *node, enum itemNumber number);
// MAIN
int main () {
player *Hero = malloc(sizeof (player));
// Hero
strcpy(Hero->name, "Sir Leeroy");
Hero->health = 60;
Hero->mana = 30;
Hero->attack = 5;
Hero->defense = 1;
Hero->autoPilot = false;
Hero->inventory = malloc(sizeof(itemNode)); // It's necessary to initialize the inventory property with a
// memory address. That way we can pass the address instead of the pointer address.
// Then our function would need to accept a pointer to a pointer to an itemNode
// as an argument and that's too much overhead.
AddItem(Hero->inventory, HEALTH_POTION);
AddItem(Hero->inventory, HEALTH_POTION);
AddItem(Hero->inventory, MANA_POTION);
RemoveItem(Hero->inventory, MANA_POTION);
DisplayInventory(Hero->inventory);
return(0);
}
int DisplayInventory(itemNode *node) {
// While there is an item present, print said item.
while (node->current != NULL) {
printf("Name: %s\n", node->current->name);
printf("Description: %s\n", node->current->description);
printf("Health: %d\n", node->current->health);
printf("Mana: %d\n", node->current->mana);
printf("Uses Left: %d\n", node->current->usesLeft);
printf("Quantity: %d\n\n", node->current->quantity);
// If there is another item in the list, go to it, else, stop.
if (node->next != NULL) {
node = node->next;
} else {
return(0);
}
}
printf("Inventory is empty.\n");
return(0);
}
// FIND ITEM
// Can't return 0 because it's interpreted as an int. Return NULL for functions
// that are supposed to return pointers.
itemNode* findItem (itemNode *node, enum itemNumber number) {
if (node == NULL) {
return(NULL);
}
// Avoid unitialized or unassigned nodes.
while (node->current != NULL) {
if (node->current->id == number) {
return(node);
}
if (node->next != NULL) {
node = node->next;
} else {
return(NULL);
}
}
return(NULL);
}
int AddItem(itemNode *node, enum itemNumber number) {
itemNode *previousNode;
itemNode *searchResult;
// See if item already exists.
searchResult = findItem(node, number);
if (searchResult != NULL) {
searchResult->current->quantity += 1;
return(0);
}
// Generate item if it doesn't exist.
item *object = malloc(sizeof(item)); // Item.
switch(number) {
case 0:
strcpy(object->name, "Health Potion");
strcpy(object->description, "Drinkable item that heals 20 health points.");
object->health = 20;
object->usesLeft = 1;
object->quantity = 1;
object->id = number;
break;
case 1:
strcpy(object->name, "Mana Potion");
strcpy(object->description, "Drinkable item that heals 20 mana.");
object->usesLeft = 1;
object->quantity = 1;
object->mana = 20;
object->id = number;
break;
}
// If node is unused allocate memory and assign item.
if (node->current == NULL) {
node->current = object;
// If node is occupied, check next node.
} else if (node->next == NULL) {
node->next = malloc(sizeof(itemNode));
previousNode = node;
node = node->next;
node->previous = previousNode;
node->current = object;
// If current and next node are occupied, search for the last node.
// The last node will have an empty "next" spot.
} else {
while (node->next != NULL) {
node = node->next;
}
node->next = malloc(sizeof(itemNode));
previousNode = node;
node = node->next;
node->previous = previousNode;
node->current = object;
}
return(0);
}
int RemoveItem(itemNode *node, enum itemNumber number) {
itemNode *searchResult;
itemNode *previous;
itemNode *next;
// See if item already exists.
searchResult = findItem(node, number);
// If item exists, and reduce quantity by 1.
if (searchResult != NULL) {
searchResult->current->quantity -= 1;
// If reduction results in 0 quantity, remove item entirely.
if (searchResult->current->quantity <= 0) {
previous = searchResult->previous;
next = searchResult->next;
// Free the item and then the node containing it.
free(searchResult->current);
free(searchResult);
// Switch linked list together.
// We can't assign the next/previous members if the itemNode is null.
if (previous != NULL) {
searchResult = previous;
searchResult->next = next;
}
if (next != NULL) {
searchResult = next;
searchResult->previous = previous;
}
}
}
return(0);
}