Create a Choice-based Text Adventure Game With Tinkercad Circuits

One of my fondest childhood memories is visiting my grandparent’s house and reading my uncle’s old collection of “Choose Your Own Adventure” books. While I always got stuck playing text-based computer adventure games with finnicky commands, I was hooked to the simplicity and variety of CYOA books (albeit the abundance of dramatic bad endings was traumatizing to say the least…).

While similar projects have been done before, I wanted to try my hand at making my own Arduino-powered text adventure to improve my coding skills and at the same time experiment with Tinkercad Circuits. I also wanted to make sure this text adventure framework was reusable, making it easy for others to write stories, and flexible, making it easy to add custom actions (like lighting a LED or playing a tone) as the story progressed. Without that, we might as well do the whole thing as a computer program!

As a programmer at heart, I’ll be spending a good part of the Instructable explaining how the sketch works, but feel free to skim through those parts if you just want to write a story.

The cool thing about using Tinkercad is that it makes sharing these stories super easy, which is great in these socially-distanced times. You can try out a sample story below!

Supplies

This Instructable assumes you already have basic knowledge of Arduino and Tinkercad Circuits.

Step 1: Build the Circuit in Tinkercad

The project is composed by the following components:

  • Arduino Uno
  • A LCD screen¹ connected to pins 7, 6, 5, 4, 3, 2 (RS, E, DB4, DB5, DB6, DB7 respectively)
  • Two push buttons connected to pins 12 and 13
  • Five 220 ohm resistors
  • An RGB LED connected to pins 9, 10, 11

You can build the circuit by manually recreating the schematic above and then copying and pasting the code found here.If you want to be quick though, you can simply remix my project on Tinkercad by pressing the “Copy and Tinker” button.

¹ Tinkercad only has a 16×2 LCD, but if you’re building this project with real components a bigger screen would probably be more comfortable.

Step 2: Design a Data Structure for the Story

Before we start actually writing the story, we have to figure out the best way to implement it in our code. Your first instinct might be to create an array of strings to store the sentences that compose the story. While it sounds like it could work, we must also store the choices the player can make (which includes not only the sentence but the “page” each choice leads to) and some way to reference any additional code we may want to run.

Keeping all this information in different arrays would quickly get messy and make it hard to write stories, so let’s aim for something better and create our own data structure using a struct.² A struct is a just a group of variables we can use to store information in a well-organized manner.

Let’s define three structs for our text adventure:

  1. Passage represents a part of our story. It is composed by an array of Message structs that contain the text of this passage and an array of Choice structs that contain the choices the player can make.
  2. Message represents a single message we will show on the LCD screen, which we’ll save as an array of strings (one string per row on the screen). We will also include an “action number” that will be used if we want to reference any additional code. More on that in step 4!
  3. Finally, a Choice struct stores the choice text and a target number to indicate which Passage this choice leads to. As a convention, if any choice has “-1” as its target number we will assume that passage was an ending and will switch to the end screen.

With the data structures clear, we can define constants that allow us to shape our structs:

/* Set these to your liking! */
const int MESSAGES_PER_PASSAGE =  5;  // Number of messages before choices are presented.
const int CHOICES_PER_PASSAGE  =  2;  // Choice that are presented to the player at each fork.
const int TYPEWRITER_DELAY     = 50;  // Pause in milliseconds inbetween characters appearing on screen.

 /* These depend on the LCD screen you are using. */
const int LINES_PER_MESSAGE   =  2;  // Number of rows on your LCD screen.
const int CHARACTERS_PER_LINE = 16;  // Number of columns on your LCD screen.

Using the constants, we can now define the structs in code as so:

/* Message that will be presented to a player on screen. */
typedef struct {
    char lines[LINES_PER_MESSAGE][CHARACTERS_PER_LINE + 1];
    int action;
} Message;

/* A choice that will be presented once all messages have been read. */
typedef struct {
    char lines[LINES_PER_MESSAGE][CHARACTERS_PER_LINE + 1];
    int target;
} Choice;

/* Structure that contains a collection of messages and choices. */
typedef struct {
    Message messages[MESSAGES_PER_PASSAGE];
    Choice choices[CHOICES_PER_PASSAGE];
} Passage;

With these, we can store our entire story and related data in a single array of Passage structs. Convenient!

If you have intermediate programming experience and wish to understand implementation details, continue to step 3.
If you rather skip the intermediate programming topics, skip to step 4.

² If you’re familiar with object oriented programming, you might have thought about using a class. Since we do not need features like constructors, methods, or inheritance, a struct is enough for our needs. Keeping it simple!

Step 3: Understanding PROGMEM to Write Longer Stories

With data structures to store our adventure we are ready to starting writing… or not?

There’s one important technical detail we mustn’t overlook for this project to work, and that is memory. Whenever you define a constant variable, the compiler will generate code that copies this value into dynamic memory (RAM) at program startup. While this isn’t much of a problem when just defining integers and short strings, it’ll quickly become a problem if we try to store a whole story. An Arduino Uno has only 2 kilobytes of SRAM, so we’ll start to run out of space only after a couple of pages’ worth of words!

Is there anywhere else we can store our adventure? Thankfully, yes! An Arduino Uno has 32 kB of program storage space or PROGMEM where your sketch is saved. The code for this Instructable only takes up about 3kB, leaving you with 29 kB to write whatever you want! It might not sound that impressive, but that’s enough space for roughly over 5000 words. You can see how much space you’re using by verifying your sketch and checking out the “program storage space”.

Telling the compiler to store the constant in program memory is simple enough, just use the PROGMEM keyword as so:

const Passage story[4] PROGMEM = ...

The slightly more complicated part is getting the variable out of program memory. You first need to define a “container” variable that will be ready to receive the value from PROGMEM. In this case, we want to retrieve a message so we can print it on screen. Once we have defined the variable, we can “copy” the message from memory into it as so:

Message message;
memcpy_P(&message, &story[currentPassage].messages[currentMessage], sizeof message);

Afterwards, you can use the variable as usual. If you need another value, you just repeat the same function using the same container variable. Whew, one problem less!

If you’re interested in learning more about PROGMEM and memory management, check out these excellent write-ups from Nick Gammon and Adafruit.

Step 4: Use Actions to Make Your Adventure More Fun

Use Actions to Make Your Adventure More Fun

We have an adventure where we can make choices that lead to different stories. That leaves us on par with good ol’ CYOA books, but hey, we’re using an Arduino so let’s make it more fun than that!

The way this is implemented is via what I call “actions”. If you recall the data structures we defined, Messages have an “action” number. This is basically an ID that tells our program what additional code to execute at each message, such as playing a beep or blinking a LED. You can see how it works at the bottom of the sketch:

/* Calls an action corresponding to the number you put in the message. Can be anything you want! */
void activateAction(int action)
{
    switch(action)
    {
    case 0:
        lightRGBLED(0, 0, 0);
        break;    
    case 1:
        lightRGBLED(255, 0, 0);
        break;
    case 2:
        lightRGBLED(0, 255, 0);
        break;
    case 3:
        lightRGBLED(0, 0, 255);
        break;      
    }
}

Thus, if you want to add additional actions, you have to add another case with the code you want to execute and reference it in your story. If the number is not found in the switch-case, then no code will be executed, but as a convention I use “-1” be the “no action” number.

An additional note: this only allows us to add execute code during the story, but if you want to do something in the title or end screen, you must edit the “setup()” or “printEnding()” functions respectively.

Step 5: Write Your Story Using the Generator Template

It’s finally time for what you came here for: writing a story! I’ll assume you have no lack of creative energy to come up with a premise and choices for your adventure, and will focus instead on the technical aspect of how to implement it.

If we recall that a story is stored in array of Passage structs, then a story definition will be structured in the following way:

const Passage story[2] PROGMEM = {
    // This is the first passage.
    {
        // This is the array of messages.
        {
            {{"First Line", "Second Line"}, -1},
            {{"Third Line", "Fourth Line"}, -1},
            {{"Fifth Line", "Sixth Line"},  -1}
        },
        // This is the array of choices.
        {
            {{"Choice 1 Line 1", "Choice 1 Line 2"}, 0}, 
            {{"Choice 2 Line 1", "Choice 2 Line 2"}, 1}
        }
    },
    // This where the second passage begins.
    {
    ...

This is… a bit unwieldy. Besides having to make sure you don’t miss any comma or curly brackets, you’ll have to be manually counting the index of each passage to be able to reference it in choices. It might be doable for something short, but it’d become cumbersome for anything long, so I decided it was time to get my hands dirty and make a tool to make writing stories easier.

The result is this Google Sheets template. You can create a copy of the template by using the previous link. While there are instructions on the sheet, the gist of it is the following:

  1. Set the constants according to your components and story. These cannot be changed once you start writing.
  2. Hit the “Add Passages” button to create empty tabs where you can start writing your story.
  3. The first time you press a button, Google Sheets will ask you permission to the run the script. You may have to press the button again after giving permission.
  4. Write your story! If you want to create more passages, just write the number you want to add and press the “Add Passages” button again.
  5. Once you are done writing, hit the “Generate Story” button. Copy the generated text and replace the “story” variable in the sketch with your story. Your adventure is ready to be enjoyed!

This template has worked pretty well in my experiments, but be sure to leave a comment if you encounter any bug or have any feature request! You can find the script the sheet runs here.

Step 6: Share Your Story!

Be sure to set the circuit to public in the project’s settings and you’re ready to share it! I’d love to see whatever you write, so please leave a link in the comments if you finish a text adventure.

While the focus of this Instructables was Tinkercad Circuits due its ease of sharing, this project works pretty well on real hardware too (I’d share a photo, but it’s hard to get a good picture of a LCD screen :p). Combined with some arts and crafts skills, you create create some pretty cool interactive installations to house your story!

I’ve just scratched the surface of the adventures you can write and actions you can invent, so I’m looking forward to see what people come up with. Have fun!

Source: Create a Choice-based Text Adventure Game With Tinkercad Circuits

Leave a Comment

Your email address will not be published.

*
= 4 + 9

(Spamcheck Enabled)

Scroll to Top