Search code examples
c++game-developmentadventure

Text Adventure Game - How to Tell One Item Type from Another and How to Structure the Item Classes/Subclasses?


I'm a beginner programmer (who has a bunch of design-related scripting experience for video games but very little programming experience - so just basic stuff like loops, flow control, etc. - although I do have a C++ fundamentals and C++ data structures and algorithm's course under my belt). I'm working on a text-adventure personal project (I actually already wrote it in Python ages ago before I learned how classes work - everything is a dictionary - so it's shameful). I'm "remaking" it in C++ with classes to get out of the rut of having only done homework assignments.

I've written my player and room classes (which were simple since I only need one class for each). I'm onto item classes (an item being anything in a room, such as a torch, a fire, a sign, a container, etc.). I'm unsure how to approach the item base class and derived classes. Here are the problems I'm having.

  1. How do I tell whether an item is of a certain type in a non-shit way (there's a good chance I'm overthinking this)?

    • For example, I set up my print room info function so that in addition to whatever else it might do, it prints the name of every object in its inventory (i.e. inside of it) and I want it to print something special for a container object (the contents of its inventory for example).
    • The first part's easy because every item has a name since the name attribute is part of the base item class. The container has an inventory though, which is an attribute unique to the container subclass.
    • It's my understanding that it's bad form to execute conditional logic based on the object's class type (because one's classes should be polymorphic) and I'm assuming (perhaps incorrectly) that it'd be weird and wrong to put a getHasInventory accessor virtual function in the item base class (my assumption here is based on thinking it'd be crazy to put virtual functions for every derived class in the base class - I have about a dozen derived classes - a couple of which are derived classes of derived classes).
    • If that's all correct, what's an acceptable way to do this? One obvious thing is to add an itemType attribute to the base and then do conditional logic but this strikes me as wrong since it seems to just be a re-skinning of the checking class type solution. I'm unsure whether the above-mentioned assumptions are correct and what a good solution might be.
  2. How should I structure my base class/classes and my derived classes?

    • I originally wrote them such that the item class was the base class and most other classes used single inheritance (except for a couple which had multi-level).
    • This seemed to present some awkwardness and repeating myself though. For example, I want a sign and a letter. A sign is a Readable Item > Untakeable Item > Item. A letter is a Readable Item > Takeable Item > Item. Because they all use single inheritance I need two different Readable Items, one that's takeable and one that's not (I know I could just make takeable and untakeable into attributes of the base in this instance and I did but this works as an example because I still have similar issues with other classes).
    • That seems icky to me so I took another stab at it and implemented them all using multiple inheritance & virtual inheritance. In my case that seems more flexible because I can compose classes of multiple classes and create a kind of component system for my classes.
    • Is one of these ways better than the other? Is there some third way that's better?

Solution

  • One possible way to solve your problem is polymorphism. By using polymorphism you can (for example) have a single describe function which when invoked leads the item to describe itself to the player. You can do the same for use, and other common verbs.


    Another way is to implement a more advanced input parser, which can recognize objects and pass on the verbs to some (polymorphic) function of the items for themselves to handle. For example each item could have a function returning a list of available verbs, together with a function returning a list of "names" for the items:

    struct item
    {
        // Return a list of verbs this item reacts to
        virtual std::vector<std::string> get_verbs() = 0;
    
        // Return a list of name aliases for this item
        virtual std::vector<std::string> get_names() = 0;
    
        // Describe this items to the player
        virtual void describe(player*) = 0;
    
        // Perform a specific verb, input is the full input line
        virtual void perform_verb(std::string verb, std::string input) = 0;
    };
    
    class base_torch : public item
    {
    public:
        std::vector<std::string> get_verbs() override
        {
            return { "light", "extinguish" };
        }
    
        // Return true if the torch is lit, false otherwise
        bool is_lit();
    
        void perform_verb(std::string verb, std::string) override
        {
            if (verb == "light")
            {
                // TODO: Make the torch "lit"
            }
            else
            {
                // TODO: Make the torch "extinguished"
            }
        }
    };
    
    class long_brown_torch : public base_torch
    {
        std::vector<std::string> get_names() override
        {
            return { "long brown torch", "long torch", "brown torch", "torch" };
        }
    
        void describe(player* p) override
        {
            p->write("This is a long brown torch.");
            if (is_lit())
                p->write("The torch is burning.");
        }
    };
    

    Then if the player input e.g. light brown torch the parser looks through all available items (the ones in the players inventory followed by the items in the room), get each items name-list (call the items get_names() function) and compare it to the brown torch. If a match is found the parser calls the items perform_verb function passing the appropriate arguments (item->perform_verb("light", "light brown torch")).

    You can even modify the parser (and the items) to handle adjectives separately, or even articles like the, or save the last used item so it can be referenced by using it.

    Constructing the different rooms and items is tedious but still trivial once a good design has been made (and you really should spend some time creating requirement, analysis of the requirements, and creating a design). The really hard part is writing a decent parser.


    Note that this is only two possible ways to handle items and verbs in such a game. There are many other ways, to many to list them all.