Designing a Branching Story App With React UI
So, my daughter *loves* Choose Your Own Adventures, and everything like it. The one thing that has driven her nuts though — she cannot truly customize them. Instead, she loves the stories I tell — she can participate and customize them, to her heart’s content.
For instance, when she gets to paint a robot, she wants to specify the color. When she names her pet, she wants to name it a different name than the story specifies. She gets pretty irritated — but I don’t want to call it “Gus”!
So, I got to thinking, how could we do this in a sort of interactive storytelling way on a tablet?
Initially, the idea seemed pretty simple. However, as happens with a creative mindset and a lot of ideas… As I began putting down ideas to paper, I had to be careful — by the time I was done, I had designed a mini-rpg system, complete with branching choices, Actions, difficulty checks, etc.
It also forced me to stop and think — what is a story? What is a choice? A location?
This… turned out to be more of a project than I thought.
First, I started to think about what a story is…
A Story is composed of:
- A Title — The identifying mark of a story
- An Author — one or more people that wrote it
- An illustrator — one or more people that put pictures in it
- An ID — because let’s assign IDs to everything
- Variables — Things that will be used to customize the story. These are persistent, but can be cleared.
- And lastly, locations — one or more locations within the story.
Okay — that seemed easy enough. But as I started thinking more, I realized, I had to think about what a Story DOES. How does a story interact with a person, and how does a person interact with a Story?
- The person chooses a location in the story to read.
- The person interacts with the choices at that location.
- The person views the pictures at that location.
- The story displays text and pictures to the person.
- The story customizes text for the person.
- The person customizes options in the story.
- The story displays choices to the person.
- The choices take the user to other locations.
- The choices alert the user to failure to meet conditions.
- The choices alert the user to meeting conditions.
- The location enables or disables choices based on conditions.
Okay — this looks like it could go on for days!
Next, each Location — essentially this is what in old media would be called a Page of a Book. If you aren’t familiar with those terms, I humbly ask you to look them up — you may be astounded at what technology has gone before.
However, in our new world, a location is a wrapper for:
- Text — this is the text that the User will read
- Pictures — 1 or more pictures associated with the location
- Choices — 1 or more choices with the location.
- Ending — the special Ending Marker — that you have reached the end of a branched path
- Location ID — b\c everything needs an ID. These are unique, and will be used to figure out what location is on display.
- Title — A title that describes the location
That doesn’t seem too bad.
A choice… hmmm… A choice will be the text that user sees for the choice, something like “Do you want to jump the puddle full of hungry phantasmal sharks?” Everything else should be hidden from the user. Unless they hit Up-Up-Down-Down-Left-Right-Left-Right-B-A-B-A start.
So — Choice:
- Action — an action that the user will take
- Text — the Text that is displayed as the initial query to the user
- ID — A choice ID, get it before it’s not ripe!
- Title — Something to display. I’m not sure about this one — do we need a title here or not? Will we really display more info to the user? Seems like the “Text is probably a good title” title time, so this may be redundant, since Text should be fairly short.
Action. I went back and forth on this one. I chose to encapsulate Action into its own object mostly b\c I didn’t want to make Choice too messy. An Action has additional properties and text that I don’t feel a Choice should have to deal with. So, in keeping with the opaqueness theme, let’s push those off into the Action.
Action:
- Difficulty Check — something to hide the actual mechanics of the check itself
- Text[Array]— the text to return, based on the difficulty check
- Location[Array] — the location to return, based on the difficulty check
- Enabled — is this Action enabled or not?
- ID — it’s a very active ID
I know — right about now, you’re wondering what happens if the check is a range check — ie — 0–20, you go here, 21–30, you go there, 31–100 you go there. For most checks, a simple 0 (PASS) or 1(FAIL) will suffice — 2 dimensional array. However, for more complicated checks, there may be multiple bits necessary:
- 0 — this is the location for the first range, text for the first range (0–20)
- 1 — this is the second range and location (21–30)
- 2 — this is the part where you die (31–100)
Apparently, this is a brutal book.
Lastly, we need to encapsulate the Difficulty Check. This is the thing that opaquely handles the real magic of the system — making it seem like this story is interactive.
First, I started to list all the types of difficulty checks I could think of: Percentile, Textual (Easy, Medium, Hard), DieRoll (5 or less out of 8), NumericRange (0–20, 21–30,31–100), Pass\Fail (CoinFlip), Random (Not really a check, but random result)
All of these seem to break down to the following:
- Perform some check — roll a die, pick a random number.
- Compare against an Algorithm — where does the number fall against certain boundaries
- Return the slot that the number falls into
For instance:
- Pass\Fail — 2 slots, 50\50 shot. Pick a number, see if it’s higher or lower than 50, return 0 (lower) or 1 (higher)
- Range — Pick a slot, see which bucket it falls into, return the bucket. 0–20, 21–30, 31–100: Bucket 0, 1, 2
- Percentile and Die Roll are both pretty much the same thing — you could translate a die roll into a percentile if you wanted. Essentially though, you have 2 buckets — this turns into a Pass\Fail check with the percentile being the bucket separator.
- Textual — Predetermined buckets
So, with those ground rules, let’s get some of this out:
- Type — What type of check is this?
- Check Levels — Specific to the type of check — this defines the buckets.
- ID — this is a very difficult ID to form
- Modifiers — any modifiers to the check
The Difficulty check does not know anything other than what algorithm to apply, and what buckets are available. It will return the bucket that the user ended up in, after applying Modifiers. (Trust me, I can tell you from personal experience that it’s easier to jump the puddle of phantasmal acid when you have rocket boots!)
This bucket is interpreted by the Action class, which returns the Text and Location to the Choice class, which move the user to a new Location after displaying the text.
In terms of implementation, a Factory is probably the best option for this. The external interfaces are all the same — the only thing changing is the underlying difficulty check algorithm. This algorithm is opaque to the caller, who only depends on having a consistent result returned to tell them what just happened.
A Factory would allow us to create a new difficulty check object, set the correct Strategy for the algorithm in use, and return that object to the caller. This keeps the algorithm set at run time, and hides it from the caller.
There’s one last class we need at a high level, if we want to truly have an interactive story — the Variable class. This class represents information that the user can give to the story to customize the story. It also represents information that the user may find along the way to customize the story — potentially helpful items, hints, etc. (Always think big!)
So, without further Ado — the Variable:
- Text — Text to present when querying the user
- ID — a variable ID, numeric
- String — A unique string, because who wants to remember all the Variable IDs
- System — A modifier to note that the user is not queried, used for internal variables, like noting you have a sword of truth! (You can only tell the truth now.)
These Variables are written to stable storage on exit, and usable during the course of the story.
Now that we’ve got some basic class attributes laid out, let’s assign some basic functions to each of them:
Story:
- DisplayStory() — Pull up the starting location of the story — location 1.
- ListMetadata() — Display the metadata to the user
Location:
- GoToLocation() — Go to a new location
- DisplayChoices() — Iterate over choices, displaying them
- SelectChoice() — Select a single choice, going to whatever location the choice eventually returns
- DisplayLocation() — display any text or pictures to the user.
- ModifyChoices() — enable or disable choices based on other variables
Action:
- RunAction() — Runs the Action, returning a location and text.
Choice:
- DisplayText() — Displays the text of the choice. One way to implement this would be to use a decorator function — with each choice at a location modifying the location itself.
- DisplayResult() — Displays the result of the Action to the user, alerting them to pass or fail
Variable:
- SetVariable()— displays a prompt to the user and stores the result
- GetVariable() — get the result of what’s in a variable
DifficultyCheck:
- RunCheck() — Runs a check, passing back a result to the Action.
Now that we’ve got that setup, here’s what our UML diagram looks like. I used http://staruml.io for this, and I’m still working to learn it. Would love to have any feedback on this.
There’s one last thing we need to do before we go on to mocking up some interface screens.
Let’s do some back of the envelope calculations to ensure that we are not going to do something stupid with the user’s storage\cpu\network.
First, how large is a book?
- Various internet searches put the average jpg size at 11.8KB
- If we assume 2 paragraphs at the average location, plus some choices… the average paragraph has around 100 words, so 200 words, at 5 characters average per word… that’s… 8KB/location.
- So, the average Location is around 20KB.
- Assuming 50 Locations per book — that makes the entire book about 1MB.
I don’t see any issues with that from a storage perspective.
This will reside locally, so there’s no real issue with network bandwidth.
If you haven’t noticed it by now, a branching story is pretty much a Tree. Thus, the only CPU issue is going to be building the tree from the location references in the very beginning. I highly doubt this will be an issue, but if it is, we can always add to the file format a storage method that contains a binary representation of the tree, which we can load directly into memory.
With that done, let’s look at some mockups of the interface.
The first one we need to design is the Story List interface:
This was built using the following code:
<View style={styles.item}>
<Pressable
onPress={() => {
console.log(‘Pressed’);
}}
>
<Text style={styles.title}>{title}</Text>
<Icon name=”downcircle” /></Pressable>
</View>
Ignoring the ugliness for a bit, let’s concentrate on the functionality. Form follows function and all that.
First, A straight list of titles doesn’t really show us much of anything. Which then brings up a point — we don’t really have a summary or abstract property attached to the Story. Guess we need to add that in.
Originally, I wanted a drop down with more info about the story. This would keep the reading list to a minimum, allowing the user to quickly scroll through story titles.
After seeing this though, I’m thinking that perhaps we need a more listicle centered design, with summary underneath the Title — so that you can see a few sentences about what exactly the story is. No one in a book store or library really looks at just the title. Incidentally, this is exactly what kills me about the Kindle App — i have to click on things to see what the story is about — I’m literally reading with my eyes for the cover picture. I bet that if you had some stats about the books I’ve read, this would show how much my reading has changed over the years.
Which of course, brings up a good point, these stories need a picture here. Let’s add that in.
There are 2 ways that I can think to do this:
- Add a new Title Picture attribute and Summary attribute to the Story Class.
- Use a Location(0) to store the picture, the summary, and the “Choice” of “Read this book”.
Using a Location to store this information feels like it would be overloading the Locations in a way I don’t like, so for now, we’ll go with Adding 2 new attributes to the Story Class.
Try #2 was using Icon.Button, just a quick mockup, to see if it looked better:
The drop down would then display things like the summary, tags, etc, and further link to read it.
This was built using:
<Icon.Button name="downcircle" > Story Title </Icon.Button>
As I worked through this, I began to wonder if there was just simply too much information for the user to see at once. When the drop down happened, it would overlay or move all the lower cards down. In addition, there was no way to know if the user was interested in the story, other than by the title. And of course, we all know… you can’t judge a book by its cover!
So, stepping back, of all the things we could display here, we need to allow the user to know if they are interested in this book. The important things for this are — Summary, Image, and Title. With these, the user can determine if they are interested in the story at all.
The user might also want to see if they’ve finished all the endings of the book. This is a branching story setup — so just finishing the book isn’t enough — you need to have all the endings finished completely!
Abandoning the drop down nature of this — probably not the best way to display this info, as we want to see all this info at the same time while scanning, so that we can make a decision. Let’s go with more of a full card based list at this point, see if we can emulate that.
This was easily achieved using a couple of nested views and some buttons:
<View style={{backgroundColor: ‘lightblue’, padding: 20, marginVertical: 8, marginHorizontal: 16, height: 250, flex: 1}}> <View style={{height:’100%’, borderStyle: “solid”}} >
<View style={{flexDirection: ‘row’, flex: 1, height:50, padding:15, borderBottomWidth:2}} >
<Image style={{width:40, height:40}} source={item.picture} />
<Text style={{flexDirection: ‘row’}}>
{item.title}
</Text>
</View> <View style={{flex: 3, padding: 10}}>
<Text> {item.summary} </Text>
</View>
<View style={{flex: 2 }} >
<View style={{flexDirection: ‘row’, justifyContent: ‘space-evenly’}} >
<Button title=”Read” style={{flex:1}}/>
<Button title=”Stats” style={{flex:1}} />
<Button title=”Endings” style={{flex:1}} />
</View>
</View>
</View>
</View>
Note — I am using inline styles here to keep the code shorter for this article. Please use style sheets or something else instead.
Also note — I’m not sure about the small icons. I wonder if moving to a picture that was the first 25% or so of the left side of the card would be better?
Also also note — looking at this now — I wonder if we need a favorite function to show favorite stories…
Also also also note — should we add a “last read” stat directly to the card itself?
Also also also also note — is the series important? I know that I often read or want to know all books in a single series. One might argue that this should be a section list with sections being individual series or age ranges or some other sub-category.
See? The questions go on and on. Sometimes though, it is best to get to “Good enough”, and move on. There’s a lot left to look at here.
Data is easy — Information is hard.
Now, we are planning on having a lot of stories — I’m quite the prolific writer! Also, my daughter reads… a lot. We go through many, many books in a day. :)
So, instead of the user scrolling endlessly… we need to have a filter screen!
First, let’s get the navigation thing going.
Our new story screen will look like this:
import {
getFocusedRouteNameFromRoute,
NavigationContainer,
} from '@react-navigation/native';import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';const TabNavi = createBottomTabNavigator();const StoriesScreen = () => {
return (
<>
<TabNavi.Navigator
tabBarOptions={{
activeTintColor: ‘black’,
inactiveTintColor: ‘gray’,
inactiveBackgroundColor: ‘lightgray’,
}}
screenOptions={({route}) => ({
tabBarIcon: ({focused, color, size}) => {
let iconName;
if (route.name === ‘Stories’) {
iconName = ‘book-reader’;
} else if (route.name === ‘Filters’) {
iconName = ‘sort-alpha-down’;
} else {
console.log( ‘ERROR: StoriesScreen TabNavi called with route’ + route);
}
return <FontAwesome5 name={iconName} size={size} color={color} />;
},
})}
initialRouteName=”Stories”>
<TabNavi.Screen name=”Stories” component={Stories} />
<TabNavi.Screen name=”Filters” component={Filters} />
</TabNavi.Navigator>
</>
);
};
So — let’s break this down:
- First, we import the required libraries. Note that we are importing the default export of FontAwesome5 as FontAwesome5. You can tell it’s the default export b\c we are not using {} around the import name.
- After that, we import the required libraries for navigation. You’ll have to previously set these up using NPM.
- Lastly, we go ahead and setup the TabNavi Navigation routes.
There’s a bit of stuff going on here:
- name=XXX component=YYY — this tells the navigator component what the name is of a particular component. The component is what will be displayed. Components are not mounted until they are displayed — so only what we set in our initialRouteName property will be loaded\mounted until the user navigates to it. In the case of our setup, that means we have no filters to look at.
- screenOptions — this is a way to setup the icons we want to use for the various tabs. It takes the route in to the arrow function. We then define another arrow function, instead of directly returning a property, and return a React Node containing what will be displayed in this particular tabBarIcon property. See the API reference for more properties that can be modified like this.
- the tabBarOptions sets our active colors up.
We want to ensure that we have basic accessibility setup:
- Using icons that communicate the intent. Instead of randomly picking icons, I’m building on previous experiences with icons. For instance, I wouldn’t suddenly pick a round circle for the Filters — in general, there are accepted standards for filtering and sorting. This gives the user a hint of what the tab will do by building on previous experience.
- Secondly, we want to ensure that icons are distinguishable from backgrounds. This helps users who can’t see or differentiate between colors or contrasts.
- Thirdly, we want to include text and altText so that screen readers can read aloud to people that are not able to use the icons for whatever reason.
- Lastly, we use other cues, like dimming inactive regions for instance, to ensure that we have drawn the user’s eye to where we need it and clearly show what is active.
Incidentally, you’ll need to add a navigation container (NavigationContainer) to your top level App.js or initial screen for this to work. Otherwise, nothing will be displayed, but Flipper\Layout Investigator will show that it was drawn on your screen… somewhere.
Before designing a filter screen, we need to discuss what we can filter and sort on.
Let’s start with sorts:
- Most recently \ Least Recently Read
- Newest \ Oldest Stories
- Most Completed\Least Completed Endings
- Alphabetical
- Series (Alphabetical in the Series, Alphabetical Series)
Filters:
- Tags
- Fully Completed
- Series
- At some point in the future, we might add Age ranges and other sub categories — but these will take the same form as Series — that is, if we design this correctly, we can have an unlimited set of sub-modifiers here.
With that said, the following mockup might work.
Just looking at this right now, there are some things I don’t like:
- The primary sort and secondary sort seem kind of confusing. What exactly does that mean?
- The Flat List of Cards on the Stories page needs to become better — it will need subheadings to handle the possibility of secondary sort. In fact, I’m thinking that maybe I need to have something like “Subheading”.
- Thinking about this even more, I come to the conclusion that this really won’t work. For instance, what if I want things by Series, then by book # in series? What about by tag, then alphabetical? Or by tag, but newest to oldest?
- Maybe I should rename this — Primary Sort => Overall Order, and Secondary Sort => Subheader Organization?
- This also raises the question — what do tags do? Do they apply during all times? Ie — if I sort by series, do I only show books with a certain tag? Or do they only apply when the subheader\secondary org is None?
I’m going to punt on this for now — this article is getting kind of long. Perhaps this will give you an idea of the kinds of things that a user interface designer needs to ask and consider.
Lastly, there is one more thing we need to discuss — infrastructure.
First, I’ll need something to create the books with. For now, I intend to use a straight JSON editor. However, with the power of react, I intend to build an editor which will let me layout with a bit more graphical power the overall book.
Secondly, when loading in a book, we need to ensure that everything is valid. This is accomplished through multiple ways:
- Overall md5sum. This ensures that no bits were changed in transit.
- Individual location verification — this is accomplished by ensuring the JSON conforms to a specific schema, as well as by ensuring that data, like locationIDs is unique. It ensures that the overall tree of the book (since that is what a branching book is), has a path to every ending. It ensures that all named picture files are accessible for each book.
I’m sure there is more I’ll discover along the way. Check back in 2 months — I’ve got just a bit of time to make this for my kid. Here’s to hoping she likes it. :)
LINKS:
- React-Native navigation: https://reactnavigation.org/docs/getting-started
- Bottom Tabs API: https://reactnavigation.org/docs/bottom-tab-navigator
- React-Native Picker: https://github.com/react-native-picker/picker
- React-Native Vector Icons: https://oblador.github.io/react-native-vector-icons/
- Images from the Noun Project: https://thenounproject.com/
- Instructions for use, build, etc: https://github.com/oblador/react-native-vector-icons
- 3 methods for styling components in React Native: https://blog.echobind.com/a-comparison-of-three-methods-for-styling-components-in-react-native-88ece2fdcdea
- React-Native Flexbox: https://reactnative.dev/docs/flexbox
- Layout Properties: https://reactnative.dev/docs/layout-props
- React-Native Flat List: https://reactnative.dev/docs/flatlist