Creating A Text-Based Game Engine: A Documented Learning Experience

Jun 8, 2017
24
3
25
Hey guys, I'm Horizon, and you may remember me from my last thread, Adventures In Modding. I abandoned that project due to not knowing the code base nearly well enough, and not being invested enough to teach myself. Still, I wanted to do SOMETHING, and so I figured that when it came to a text-based adventure, it shouldn't be THAT hard to do, right?

This project is pretty simple: I want to create, largely from scratch, a text-based adventure engine like what's used to run Corruption of Champions or Trials in Tainted Space.

This is a group project, by which I mean this is me wildly flailing about in my own ignorance while 80% of the audience watches in amusement and 20% offer advice. However, if your advice comes from Google... please don't. I know how to use Google. I'd prefer advice that comes from personal experience or a classroom, or even just talking to a friend/colleague of yours. Thanks in advance!

Let's get into it, shall we?

Alright, so, to start off, since we're making our own engine pretty much from scratch, we need to pick a language. Corruption and Trials are both, to my knowledge, coded in ActionScript. And while I'm sure ActionScript works perfectly fine for the team... honestly, I don't know it very well, and I don't have any particular desire to learn it in the process of making this engine. So, instead, I'm going to pick a language I'm already familiar with, and that leaves me with three choices, the Big Three: Java, Python, and C++.

Seeing as how the computer science class I'm currently in is teaching C++, I feel that continuing to work in C++ is probably the best choice. Okay, it doesn't have a standard graphics library- so what? I'm sure I'll figure something out. Probably. Maybe. Okay this project might be stuck using the standard iostream horseshit for a while until someone who knows C++ better than I, a 19 year old idiot, comes in and tells me exactly which graphics library I want for recreating the CoC engine and how to use said library.

So, now that I've decided on C++, let's begin. Open up your favorite text editor or IDE for C++; for me, it's Dev-C++ because that's what my professor recommended we download and so far it's been working pretty well for me. Now, since we are, for the time being, using the iostream library, it'd be useful to define a few getInput methods for chars, ints, and strings. Make sure that you include error-checking and error-messaging in the methods.

Code:
void getInput(int &arg){
   bool loop = true; //This is entirely for the while loop coming up next.
   while(loop){ //This is so that, if you do get an error, you can try entering the info again.
       if(cin >> arg){ //This is error checking. Returning a true means there's no error, while false means there is an error.
           cout << "Valid input. Proceeding...\n";
           return;
       }
       else{
           cout << "\nInvalid input: not a number. Please try again. \n"; //Always important to be specific with error messages
           cin.clear(); //Also important to clear the buffer
           cin.ignore(1024, '\n');
       }
   }
}

void getInput(double &arg){
   bool loop = true;
   while(loop){
       if(cin >> arg){
           cout << "Valid input. Proceeding...\n";
           return;
       }
       else{
           cout << "\nInvalid input: not a number. Please try again. \n";
           cin.clear();
           cin.ignore(1024, '\n');
       }
   }
}

void getInput(char &arg){
   bool loop = true;
   while(loop){
       if(cin >> arg){
           cout << "Valid input. Proceeding...\n";
           return;
       }
       else{
           cout << "\nInvalid input: not a character. Please try again. \n";
           cin.clear();
           cin.ignore(1024, '\n');
       }
   }
}

Pay special attention to the getInput(char) method. You want all of these to work, obviously, but the char one is going to be especially important, because we're going to be using it for how options are chosen in this game.

Alright, so, next up, let's test these methods, and make sure they work. If you don't know how to do that, just throw some code into main that'll create variables of char, int, and string, and call the methods to define all of those. If the methods don't work, then fix them. Debugging is an important skill. If necessary, explain your code out loud to a rubber duck, and listen to the words that come out of your mouth. Also, always, always, always document your goddamned code. This is not optional and if I catch you not documenting your code I will hit you with a bag full of rubber ducks until you finish documenting your code.

Now, next step: go back and document your code. One of C++'s major flaws is being less human-readable than other languages, so documentation is especially important here.

Alright, is your code documented? Then it's time to get into the real meat of this here first post: the Event class.

An Event, for the purposes of this game, is going to have a few components. It's going to have body text, it's going to have incidental effects, and it's going to have options. The body text is easy enough to deal with; it's just a really big string. The incidental effects are tougher, and kind of rely on having something to affect, so we'll just have some comments in place to remind us to add that in when we add in the Player class. The one that really matters right now is the options.

The options are going to, themselves, have a few components- they are going to have flavor text(a string) and something that points at another Event. So maybe let's define an Option class that combines the flavor text and the pointer.

But before we do that, let's first go back and document any code we've written so far(No, I'm never going to stop reminding you to document your code, because it's never going to stop being a thing you need to do). After that, let's go into main, and define a few instances of the Event class. We'll call them EventHub, EventA, EventB, EventC, EventD, and Event E. We'll give them very brief placeholder text, and then we'll go back to the Option class, and define it to contain a string for flavor text and an Event. Then we'll go back to Event and have it contain an array of five Options. This feels like bad programming already, but I've only spent like an hour and a half on this, plus I'm an inexperienced beginner, so fuck it.

Alright, so now let's define what, precisely, the options are for each event. I'm thinking that for EventHub, the options are all the other Events we've defined so far- because why not. So from EventHub, you can get to any of the other five Events. And from EventA, you can get back to EventHub or to EventB, and vice-versa for EventB- you can get back to EventHub or EventA. For EventC and EventE, you can only go to EventD, and from EventD, you can only go back to EventHub.

You got all that? Well, pretend you do, and then go program it.

Now, for an important thing I completely forgot: making the Event class actually, like, do something. Let's give it a simple runEvent() method, in public because why not(if there's a compelling reason why not, please tell me, I have no idea and don't really understand why public and private matter). Now, the runEvent() method should output the flavor text- which came in with the formatting already(THROW THAT IN AS SOME DOCUMENTATION- "You have to format the flavor text before inputting it; the program won't do it for you."). It should also display each of the Options in its array, and you know what that means:

We need to make a displayOption() method in the Option class! Make it require an Int as an argument, and when you call it, pass in the index plus one. For the displayOption() method, I decided to use a Switch statement, and each case just outputs a single character. After that, it's a simple cout statement that outputs a close-paren, a space, the flavor text, and an end-line statement.

Okay, so, here's my code so far:

Code:
//TEXT BASED ADVENTURE ENGINE MARK 1

//Main and Get Input


# include <iostream>
# include <string>

using namespace std;

//GET INPUT
void getInput(int &arg){
   bool loop = true;                                                        //This is entirely for the while loop coming up next.
   while(loop){                                                            //This is so that, if you do get an error, you can try entering the info again.
       if(cin >> arg){                                                        //This is error checking. Returning a true means there's no error, while false means there is an error.
           cout << "Valid input. Proceeding...\n";                            //This is temporary, and only hear as long as the engine is in alpha.
           return;
       }
       else{
           cout << "\nInvalid input: not a number. Please try again. \n";    //Always important to be specific with error messages
           cin.clear();                                                    //Also important to clear the buffer
           cin.ignore(1024, '\n');
       }
   }
}

void getInput(double &arg){ //This is pretty much identical to the one above it. See its documentation if you need it.
    bool loop = true;
    while(loop){
        if(cin >> arg){
            cout << "Valid input. Proceeding...\n";
            return;
        }
        else{
            cout << "\nInvalid input: not a number. Please try again. \n";
            cin.clear();
            cin.ignore(1024, '\n');
        }
    }
}

void getInput(char &arg){ //This is pretty much identical to the one above it. See its documentation if you need it.
    bool loop = true;
    while(loop){
        if(cin >> arg){
            cout << "Valid input. Proceeding...\n";
            return;
        }
        else{
            cout << "\nInvalid input: not a character. Please try again. \n";
            cin.clear();
            cin.ignore(1024, '\n');
        }
    }
}

//OPTION

class Option{
    public:
        Option(string flavorIn, Event pointerIn){
            flavor = flavorIn;
            pointer = pointerIn;
        }
        void runEvent(){
            pointer.runEvent();
        }
        void displayOption(int i){
            switch(i){ //The "i+1" is important because I forgot to count at zero and reformatting this would've been a pain in the ass.
                case 1:
                    cout << "A";
                    break;
                case 2:
                    cout << "B";
                    break;
                case 3:
                    cout << "C";
                    break;
                case 4:
                    cout << "D";
                    break;
                case 5:
                    cout << "E";
                    break;
            }
            cout << ") " << flavor << endl; //Outputs the char of the option, then the flavor text, plus some formatting.
        }
    private:
        string flavor;
        Event pointer;
}


//EVENT

class Event{
    public:
        Event(string flavorIn){//No longer asks for an array of options, because there's a sort of circular dependency going on and remember what I said about that being a bad programming choice?
            flavor = flavorIn;
        }
        void addOption(Option in[]){
            options = in; //Completely replaces options with in.
            return;
        }
        void runEvent(){
            cout << flavor << endl;
            for(int i = 0; i<5; ++i){
                if(options[i]!=void){//Making sure we don't display options that aren't there!
                    options[i].displayOption(i+1);//Pass this along to the Option itself, so that I can use that fancy displayOption method I made. The "i+1" is important; you'll see why.
                }
            }
            char input;
            getInput(input);
            switch(input){
                case A:
                    options[0].runEvent();
                    break;
                case B:
                    options[1].runEvent();
                    break;
                case C:
                    options[2].runEvent();
                    break;
                case D:
                    options[3].runEvent();
                    break;
                case E:
                    options[4].runEvent();
                    break;
            }
        }
    private:
        string flavor; //You have to format this yourself, because the machine won't do it for you.
        Option options[5];
}

//MAIN

int main(){
    //Initialize all of the Events
    Event EventHub("This is the event hub. You can get anywhere from here.");
    Event EventA("This is Event A. You can go back to the hub, or back to B.");
    Event EventB("This is Event B. You can go back to the hub, or back to A.");
    Event EventC("This is Event C. You can only go to Event D.");
    Event EventD("This is Event D. You can only go back to the hub.");
    Event EventE("This is Event E. You can only go to Event D.");


    Option hub{toA("To Event A",EventA),toB("To Event B",EventB),toC("To Event C",EventC),toD("To Event D",EventD),toE("To Event E",EventE)};
    Option fromA{AtoHub("Back to the Hub",EventHub),AtoB("To Event B",EventB)};
    Option fromB{BtoHub("Back to the Hub",EventHub),BtoA("To Event A",EventA)};
    Option fromC{CtoD("To Event D",EventD)};
    Option fromD{DtoHub("Back to the Hub",EventHub)};
    Option fromE{EtoD("To Event D",EventD)};
}

You can see a few instances of "Oh shit I forgot that needed to be there." Such as "Oh shit I need to make choosing an option actually do something." There's also quite a few errors, the biggest of which is "You can't have an Option hold an Event if the Event also holds an Option." So I need a solution to that.

However, all that nonsense can wait until later. I'm done working on this for now- I've already spent three hours on it, and I'm kinda sick of looking at it. If anyone's got an "Idiot's guide to using GitHub instead of copypasting source code into a forum post" lying around, send it my way. If anyone has, like, a better way of handling the Option/Event problem in a way that will work immediately as well as later on, that'd be lovely.
 

CleansingFire

Well-Known Member
Aug 27, 2015
163
42
Okay, it doesn't have a standard graphics library- so what? I'm sure I'll figure something out. Probably. Maybe. Okay this project might be stuck using the standard iostream horseshit for a while until someone who knows C++ better than I, a 19 year old idiot, comes in and tells me exactly which graphics library I want for recreating the CoC engine and how to use said library.

You have multiple possibilities. For example, Qt is free, multi-platform and it has a convenient IDE (QtCreator) with integrated documentation.

"Idiot's guide to using GitHub instead of copypasting source code into a forum post"

You need to learn the fundamentals of Git first - at the very least, how to use a local repository. I suggest the git book.
 
Jun 8, 2017
24
3
25
Okay, so, after seven hours away from programming, I've come back to the problem, and rethought the whole "options as a separate object" thing. Clearly, that does not work. But we still need to transition from one Event to another, and... I'm not sure how to do that. Maybe have just the names of events it leads into stored as a variable, and the Main method handles the transitioning? Fuck if I know, my guys.
 

TheDarkMaster

Well-Known Member
Creator
Aug 28, 2015
1,052
259
Okay, so, after seven hours away from programming, I've come back to the problem, and rethought the whole "options as a separate object" thing. Clearly, that does not work. But we still need to transition from one Event to another, and... I'm not sure how to do that. Maybe have just the names of events it leads into stored as a variable, and the Main method handles the transitioning? Fuck if I know, my guys.
You have two options there. Either have a library of events stored in a list that is referenced every time you change from one event to another, or you directly reference the event you want to go to in each one when transitioning. In either case, you should drop back to the main function when changing events by returning either the ID/name of the next event, or the reference to that event.
 

Void Director

Well-Known Member
Aug 26, 2015
198
6
Sounds like a good learning project. I learned a lot when I was building my text games.

In chosen (my text based webgame) I give event a unique id and have a map which stores them all. Then when the engine needs to make a transition we simply pass in the id and look up the next event in the map. You could also have events contain pointers to every other event as TDM notes. But even in that case building all of those pointers manually will probably be a nuisance as you first have to initialize all the events, then later pass in pointers. So the map is probably easier.

I would note that using C++ is definitely going to make things harder. Java has a much nicer standard library and less rough edges (in exchange for worst performance, which isn't important for a text game). But if you want to learn C++ its reasonable to use it.
 
Jun 8, 2017
24
3
25
Okay, so, after some more thinking, I've decided that using C++ for this maybe isn't the best idea, since 1) I don't know enough C++ to do it, and 2) it'd be very awkward to explain to my professor what I'm trying to do so he can help me. So instead, I'm going to switch over to Javascript and HTML5, for a few reasons:

1) HTML pretty much is a graphics library. Using it means making the interface is going to be so much easier.

2) HTML means it's cross-platform, which means I don't have to port it.

3) HTML and Javascript both have free courses on CodeAcademy.

4) Assuming my future employer never stumbles across this forum thread, having this sort of program and these languages under my toolbelt will look great on a resume.