Skip to content

Onward Together is an experimental create-your-own-adventure story with a multiplayer aspect. It was created as part of an honors thesis on using interaction design to improve the viewer's retention of the story.


Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time

Onward Together

Onward Together is an experimental create-your-own-adventure story with a multiplayer aspect. It was created as part of an honors thesis on using interaction design to improve the viewer's retention of the story. The entire project, with the exception of preliminary research, was completed in the span of 1 month, including the writing, illustration, design, development, and user testing.

Check out the live website for more context.



The story itself is not incredibly interesting. This is because if the story were interesting, and users had a positive response to the experience, it would be less clear if this response should be attributed to the experience itself rather than the story. As such, the writing relies on common fantasy tropes and does little to stand out.


The illustration style is based on storyboards and concept art done for films. The loose stroke-based style manages to create a sense of space with minimal detail, allowing the viewer's imagination to fill in the rest. All illustrations and animations were created in Adobe Photoshop.


The animation is kept to simple (usually 4-frame) repeating animations to match the vague art style. Although the animation itself is quite minimal, it does make the interaction feel more dynamic, when in reality it is simply pressing buttons.


There are six primary strategies employed in this website to make the experience more memorable. The story itself is designed to be rather unremarkable. By keeping the subject of the story familiar, any positive reaction from the audience can be attributed to the interaction design rather than the story itself.


Some studies have shown that serif typefaces improve retention. Furthermore, fonts that are hard to read (like handwriting) improve memory of the text they display.


Text broken into smaller pieces is less daunting to read. This website progressively reveals information by letting the user click on to the next section when they feel ready.

Art Style

An art style based on concept art is used to create a mood of mystery and drama. The art is purposely kept vague and abstract to allow the user to fill in the rest with their imagination.

Variable Paths

Cognitive offloading is prevented by having 48 possible story paths. The user is more inclined to remember their choices because they may not be able to repeat them next time.


Information is repeated through story "flashbacks," the game's replayability, and social aspects. In order to find different story outcomes, or compare with others, the user must recall the decisions they made previously.


Although the multiplayer parts are subtle, the presence of another player encourages the user to think about how their actions affect both the story and other people. It also encourages replayability.


Prerequisite: Discount Bootstrap

"Discount Bootstrap" is my lightweight take on Bootstrap's responsive grid system. It has its own repository and documentation over at It provides most of the website structure through use of classes like:


Designed for HTML

Most of the planning is done in the document HTML, which allows for the scripts to stay simple and rely on pulling attributes from the clicked elements. Due to this organization, all of the scripting for the entire story is under 300 lines of code. However, the tradeoff is that the HTML has to be organized very carefully.


The entire story is contained in a single HTML document (index.html), and the scripts show or hide specific elements (called "stages").

<div class="container stage stage-3">

All stage containers have the class .stage as well as an additional class to indicate which stage specifically it is (.stage-3 in this example).

.stage.trigger, [stage], and [path]

Some stages of the story do not provide buttons to progress the story down any particular path. In these situations, the .stage element has an additional class, .trigger, and some custom attributes to indicate the desired destination. The [stage] attribute lists which stage should be made visible next, and the [path] attribute dictates which information to show in the next stage. Adding these three attributes makes it so that the user can click anywhere in the stage to progress.

<div class="container stage stage-2 trigger" stage="3" path="O">

The [path] attribute can have numerous values. In this project, alphabetical values from "A" forward are used to indicate that the desired path is one of several options. However, the value "O" is used when the desired path is the only option available. It is important that in the next stage, there are .option elements with a corresponding attribute [path-option] value. Read more about this in the .option section.


Similar to our .stage.trigger elements, buttons must have the class .trigger, and attributes [stage] and [path].

<span class="button trigger" stage="4" path="A">
  I’m lost!

var userPath

The user's choices make up a unique path. There are five points in the story at which the path direction is chosen, so paths are referred to as 5-digit numbers. The paths are detailed as follows:

  1. Original or Echo - This path is determined randomly by the client. The user gets lost in the Echo Cliffs, and is either the first to call out, asking for direction, or the one who answers. This is stored in the first positon of the userPath array as either a 1 or a 2. (Original = 1, Echo = 2).

  2. Alone or Together - This is the initial choice the user makes, stored in the second position of the userPath array as either a 1 or a 2. (Alone = 1, Together = 2).

  3. Traveling Cutscene - This path has three options, meant to represent three possible travel cutscenes as the directions given on the Echo Cliffs if the user chose "Together" for the first option. However, even if the user chose to go "Alone", there are still three possible paths; they just all have the same cutscene. This choice occupies the third position in the userPAth array. (Mushroom Forest = 1, Crystal Tunnel = 2, Wisp River = 3).

  4. Help the Monster - This choice allows the user to either have mercy on a trapped monster or ignore it. This does not change the outcome of the story other than showing a silhouette of the monster on the replay scene (above the tavern). This choice is stored in the fourth position. (Ignore = 1, Help = 2).

  5. Alone or Together - The user is presented with their original choice again and can either join an adventurer or leave them behind. Choosing to go together causes the "happy" ending. Going alone causes survival, but does not defeat the dragon. This choice is stored in the fifth and final position of the userPath array. (Alone = 1, Together = 2).

Throughout the story, values are added to the userPath array in the correct place. This array is converted to a 5-digit ID when the information gets submitted to the database. By default, the values of each index to be used are set to 0.

userPath = [0,0,0,0,0];

[choice] & [place]

Two additional attributes can be applied to buttons: [choice], which indicates to the script that clicking this button should store a specific choice value in the story path, and [place], which indicates which position in the userPath array this new value should occupy.

<span class="button trigger" stage="4" path="A" place="2" choice="2">
  I’m lost!

When clicked, the button above will store the choice "2" at userPath[2].

.option & [path-option]

Anything contained in a span with class .option and a [path-option] attribute of "B" will only display if the previous .trigger had the attribute [path] equal to "B". All other [path-option] values within the parent .stage will be hidden before the .stage is made visible.

<span class="option" path-option="B">

If the [path] attribute of the previous .trigger was set to "O", the script will check if the user is currently on an "Alone" path or a "Together" path and treat any child .option spans as though [path-option="A"] = alone, and [path-option="B"] = together.

.server-load-info &

At some points in the story, especially the flashback sequence, it is necessary to load choices made earlier in the story (not created by the last .trigger click event). In these cases, all options altered by this choice need to be wrapped in a span with the .server-load-info class. There is also a required attribute, [req-place] which indicates which position in the userPath array we are retrieving the information from.

<span class="server-load-info" req-place="0">

This example is retrieving information from the first place in the userPath array, userPath[0], which is the Original or Echo choice.

Inside of the .server-load-info span there should be as many options as there are possible values for the designated position in the array. In this situation, since userPath[0] can be either a "1" or a "2", there should be two elements contained in the parent .server-load-info span. The correspondence to each option is set by the [server-choice] attribute. Any elements with a [server-choice] attribute that does not match the value found at the index of userPath indicated by the [req-place] attribute of the parent span will be hidden.

<span class="server-load info-option" server-choice="1">

.server-random & .server-load.random-option

Similar to .server-load-info, this pair also relies on a parent-child relationship. The span with class .server-random must surround elements with the classes .server-load.random-option. However, rather than looking to a position in the existing userPath array, this set of classes chooses a random number with a maximum indicated by the attribute [random-max], and 1 as the floor (inclusive of both).

<span class="server-random" random-max="3">

The above example will choose a random number between 1 and 3, and then show any .server-load.random-option elements with an attribute [radnom-option] that is set to a matching value. If the value generated by the random number generator is 1, anything within the following span will be visible:

<span class="server-load random-option" random-option="1">

while anything in spans with other values for [random-option], like the following, will not be visible.

<span class="server-load random-option" random-option="3">

Make sure that there are options for each random integer between 1 and the value indicated by [random-max].

Request Processor

When a .trigger is clicked, the attributes talked about are located, and their values are assigned to values. These variables are then passed into two functions, editPath and advanceStage.

$('.trigger').click(function () {
    var stage = $(this).attr('stage');
    var path = $(this).attr('path');
    var place = $(this).attr('place');
    var choice = $(this).attr('choice');
    editPath(place, choice);
    advanceStage(stage, path);

The editPath function updates the userPath array with the information passed in.

function editPath (place, choice) {
  place = place;
  choice = choice;
  userPath[place] = choice;

The advanceStage function begins a number of processes by calling other functions with parameters passed in. This function also hides all .stage elements by removing the class .visible, and then adding the class .visible to the next .stage. By default, the background-image of the body element is removed in case there were any. Should the next stage require a background-image, this is added back by the setBackground function.

function advanceStage(num , path) {
  var newNumber = num;
  var chosenPath = path;
  preLoad(newNumber , chosenPath);
  serverPreLoad(newNumber , chosenPath);
  serverInfoPreLoad(newNumber , chosenPath);
  setBackground(newNumber , chosenPath);
  $('.stage-' + newNumber).addClass('visible');

Several functions are run to prepare the .stage to be made visible with the correct content.

  • preLoad - checks the selected path letter and shows content accordingly
  • serverPreLoad - prepares random number generation and shows content accordingly
  • serverInfoPreLoad - checks the userPath array at a specific index and shows content accordinly
  • loadPartner - only used in final screens, checks database for the user's "ghost partner" (see Firebase section for more info)

However, some functions may have no effect if the elements each function acts upon are not present in the target .stage. For example, the serverInfoPreLoad function specifically will not change the .stage contents if .server-load-info and are not present within it.

function serverInfoPreLoad (num, path) {
  var newNumber = num;
  var chosenPath = path;
  var newStage = $('.stage-' + newNumber);
  var stageLoadInfos = newStage.find('.server-load-info');
  stageLoadInfos.each(function( index ) {
    var reqPlace = $(this).attr('req-place');
    var specificPath = userPath[reqPlace];
    $(this).find('.info-option[server-choice=' + specificPath + ']').css('display','inline');

If the next .stage element has the class .background-change, the function setBackground will set the background-image of the body element to an image with a matching naming convention.

function setBackground(num, path) {
  var newNumber = num;
  var chosenPath = path;
  if ( $('.stage-' + newNumber).hasClass('background-change') ) {
    console.log('if statement triggered');
    $('body').css('background-image','url("images/stage-' + newNumber + chosenPath + '.gif")');
  console.log('setbackground function triggered');

The file "stage-24O.gif" is loaded by the function setBackground if the destination .stage element has the class .background-change, and the .trigger that began this .stage change had the attributes [stage="24"] and [path="O"].

After calling the preparation functions, the destination .stage is given the class .visible to show the new content.


This adventure is powered by Google Firebase. Firebase was chosen due to its ease of use and simple data structuring. Not all players are added to the database. Upon completing the story, the user is given another username (their "ghost partner") and provided the option to submit their path, as well as opt out.

User Storage Method

Rather than storing each unique user as a directory with a path assigned, the possible path IDs are used as the directory, and users are stored underneath the path they took with a timestamp.

Ghost Partner

The entire experience is what we're calling "ghost multiplayer". It is not realtime multiplayer (traffic would not support this type of functionality). Rather, when the player completes the story, the 5 digit path ID is generated. A given user's partner is someone who took almost the identical path, but had one difference: the Original or Echo choice. If the current user was the "Original", then their partner is whoever echoed back. The ID, then, of the partner is different by the first digit. The partner of someone who had the path 12222 is found under the ID 22222. Because multiplayer is not real-time, once the partner's ID has been determined, the database pulls the most recent player to complete that given path and supplies their name.

Data Visualization

The site's about page pulls data from Firebase and shows visually which choices users made by percentages.


Onward Together launched online on February 20, 2020 and passed the honors thesis defense process March 5, 2020. While the data collected through this project is not strictly empirical, many users who played reacted positively, and the average session duration was 4 minutes, 16 seconds, suggesting that users were engaged enough to complete the story, and found the experience memorable enough to discuss it with others. Based on the initial response from this creative thesis, it seems that further investigation into the potential for UI/UX design to help with retention could prove to be quite valuable.


Onward Together is an experimental create-your-own-adventure story with a multiplayer aspect. It was created as part of an honors thesis on using interaction design to improve the viewer's retention of the story.






No releases published


No packages published