Text Adventure Engine and Data

Backend Data

SQL

SQL databases, also called relational databases, are generally faster and more efficient than the alternatives. Relational databases work by having tables filled with items with unique keys, and references between those unique keys in other items. For example, if we had products that we made in a set number of colors, our relational database could look like this:

Product IDProduct NameProduct Color
0001Egg ChairC02
0002Modern CouchC01

This table in our database defines our products. The product ID is a unique ID for the entry, the name is a string that holds the name, and the product color holds a reference ID to the color table which might look like this.

Color IDColor Name
C01White
C02Yellow

This is a simplistic example, but it shows the workflow of designing a relational database.

After all of that, I have to say that this kind of database is not suited to our text adventure project!

NoSQL and DOD

The most important data we are storing in our project is the text adventure game itself, which will be one large JSON file that includes every part of the game. This means that the JSON file will have sections for rooms, NPCs, items, and even commands. Because one of our project goals is to offer a fair amount of flexibility for the educators making these text adventures, there may be classes of data that we can’t anticipate to add to our database. Knowing this, the design of Document Oriented Databases fits our project best. A DOD stores documents (often JSON files) and doesn’t care what the values and keys within the document are. This perfectly aligns with our design of storing the whole game as a JSON file, and then having the game engine load that JSON file into memory and generate the game from it. When querying the database, the game engine will never need part of the document, and will just call on the whole game at the beginning, which is also better suited to DOD.

Game Engine Data

Entity Component System

Intro

ECS is a software architectural pattern that is common in game design. It is almost a response and refutation of the popular Object Oriented Programming. OOP will have objects storing their own data, IDs, and functionality through methods. Objects can inherit properties of other objects, but it can get very complicated, and you often can end up having difficulty writing rules for all of the objects. The Entity Component System architecture aims to solve all of this by first breaking up your code into entities, components and systems.

I’ll describe each of them and we can build an example ECS for our text adventure game engine.

Entity

Entities represent a game object. This can be a room, the player themselves, an item, or anything else. They usually have a unique ID, but hold no data about the object itself. Lets build a few example entities.

001: Player
002: Troll
003: Key
004: Tree

Component

Components are data that can be connected to entities. Components are not methods or functionality, but purely data. So an example of a component might be the position of the entity, but it would not include the method by which to move the entity. Lets make some components.

Position: int RoomNumber
Visible: bool
Description: str
Carryable: bool
AI: str Type
Alive: bool
Inventory: list

I’ll give a brief explanation of all of these proposed components. Position shows which room the entity is in. Because this example is a text based adventure game, we don’t need to render a position using X and Y coordinates, but only really use it to include it in a room’s functionality. Visible is a boolean that tells the game whether to describe the object. Description would hold the sentence describing what the object is if it were inspected. Using these two components as an example, something a player does could make an object visible, and then inspecting it would give the description. If the object is just visible, it might be described the moment they walk into the room for the first time. Carryable tells whether an attempt to pick up an object would be accepted or not. The AI component is interesting, as for this example it would be a string that tells what kind of AI exists. Some entities might have enemy AI, and others might have friendly AI. The AI system would check through the active entities for all of the ones with a given type of AI and execute the movement based on that. Alive is a boolean that is pretty straightforward, and can flip to False if the entity dies. Finally, inventory is a list of items that the entity has picked up.
Lets attach some components now.

Player.Position(1)
    .Visible(True)
    .Description("Yourself!")
    .Carryable(False)
    .Alive(True)
    .Inventory()
Troll.Position(1)
    .Visible(True)
    .Description("An angry Troll")
    .Carryable(False)
    .AI("Enemy")
    .Alive(True)
    .Inventory("Small notebook", "Cutlass")
Key.Position(1)
    .Visible(True)
    .Description("A small brass key")
    .Carryable(True)
Tree.Position(1)
    .Visible(True)
    .Description("An evergreen of some sort")
    .Carryable(False)

Now our items have some data attached to them. The exciting thing is that components can be added or taken away or changed at any point in the execution of the program. So if you befriend the troll, his AI can be changed to friendly and he could follow you around helping instead of being an enemy. If you accidentally got glue on the key and it stuck to a table, you could change the Carryable component attached to it to false. Conversely if the glue gets taken off you could change it back to true. Finally, if for instance the Troll dies, you could remove the AI component completely, and the game will never have to worry about moving him again.

System

The system acts on all entities with desired components. For instance the AI system might go through the list of entities and perform their AI logic on all entities that have the component.

def AI(local_entities[]):
    for entity in local_entities:
        if entity.AI == "Enemy":
            //Enemy logic
        if entity.AI == "Friend":
            //Friend logic
def pickup(entity):
    if entity.Carryable:
        player.inventory.add(entity)
        return "{entity} added to inventory"
    else:
        return "You can't pick up the {entity}"

In this instance the AI works exactly as described, iterating through the list of local entities and performing its logic on them. This AI system would be called automatically every turn so that all AI entities would also have a turn after the player. The pickup system would be called only when the player enters the command to pick up an item. The interesting thing is that if the AI allows for it, and the NPC has an inventory, your NPCs could have logic to pick up dropped items, or even pick something up you need and run away from you. Because the components and systems are so modular, there is a lot of flexibility in its application.

Conclusion

Overall, I think these systems will work well together. I think that the one tricky part could be making a good system for taking the JSON file format, and using it to construct all of the entities in the game. I think there will need to be a bit of tinkering to find the best way to easily interpret the JSON in this way. But I’m pretty excited about it!

Print Friendly, PDF & Email

Posted

in

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *