The Project
Fatebook is a social platform for creating and sharing interactive, choose-your-own-adventure stories. As an Angular on Rails application, it is the culmination of my Rails and Angular studies through Bloc.
Tools
Rails, Javascript, Angular, UI-Router, ngCookies, ActiveModelSerializers, Underscore.js, ckEditor, Bootstrap, Atom, Ubuntu
The App
Upon entering the app, users are able to play through any existing published story. When playing through a story, they will see a paragraph or two of text, followed my a set of options. Each option will take the user down a new story thread. Users may select these chooses by clicking or pressing the corresponding number on their keyboard.
Should a user want to create their own story, they must log in and select “create story”. From here they can provide a title and description of their story. Upon creating the story, the first page is automatically generated. After populating the content of the page, they can click “create option” to define a new option players can choose. They can then click the “Edit Page” button for that new option to go directly to a page generated for that option.
At any time, users can use the buttons along the top to navigate to the parent page, parent story, or view sibling stories on the same level of the story tree. Additionally, users may view the story tree at the bottom of the page, allowing them to easily traverse their story and identify which story threads remain unfinished.
The Process
While planning the project, I defined two main states: Play mode, where a user is playing through someone’s story, and Edit mode, where a user is creating or editing one of their own stories.
I began with the Play state, starting initially as a pure Angular application. I created a system where a Story
object would contain an array of Pages
. Each Page
would contain an array of choices
, which would be an object hash with the attributes of text
for the text of the choice the users sees, and dest
for the ID of the destination page.
To make nested seeding of page data easier, I used a page ID system I came up with for a short branching story I wrote years ago called Doors (which I ended up using as the example story for my app.) The naming convention consisted of alternating numbers and letters to indicate the number of levels down through the story a user had navigated. For example, the first page would have a id
of “1”, with child pages of “1A” and “1B”. This could be chained indefinitely, so a page like “1A2B2” would be the child of page “1A2B” and sibling to “1A2B1”.
This id
was auto-generated using the following code (annotations added as comments):
Using this id
system and the functions built into the Page
object, I set up a system for easily populating a test array of pages. To do this, I first created a dummy first page, then used an editPage
function I created to create an repeatable format I could use to populate all pages.
With a working story navigation system working, I decided to add in keyboard controls as well.
With a working frontend implementation, I then moved on to add a Rails backend. Following a guide I found on youtube, I was able to get Angular to communicate with Rails, though it was a tricky process. I had to do some research to find the gems I required, namely angular-rails-templates
to enable my Angular templates to be used in the Rails asset pipeline.
My Rails backend became an API to store and pass data to Angular via the UI-Router service. It accomplished this by passing returning JSON to my Frontend, after being processed by the appropriate ActiveModelSerializer
to ensure the relevant attributes were properly passed.
It took me a little while to make sense of creating these accessor services, but eventually it clicked and made sense how the urls they used corresponded to the routes created by Rails.
I hit some friction coming up with the best approach for setting up the ActiveRecord
associations between Stories, Pages, and child Pages. My initial thought was to create a has_many :through
relationship, using a branch
model to link a parent page to a destination page. This created an odd situation where each page has_many
pages and belongs_to
a parent page, which just wasn’t working right.
After some thought and consultation with my mentor, I realized there was a better approach. Rather than having pages be linked to each other directly, I could instead define a Branch
join table to link pages together. The result was a much cleaner association.
With the associations created, I set to out to seed the database with a full, branching story I wrote back in 2007. Using the naming conventions I set up before, I was able to fairly easily import my story into the database page by page. While no longer functionally required, I used a similar naming convention for Pages
to keep track of which stories were connected to each other, using a create_branch
method to link them together in the database.
For each Page
, I created a hash containing the choice text and reference to the page ID (combining the page initialization and id getter into a single statement for brevity). create_branch
would then take this new object and use it to define the child page for the parent page via a branch.
On the parent page (from the second parameter), a new branch would be created, passing in the choice text (choice_text
) of the child object as well as the id of the associated child page (destination_id
). To ensure all branches are associated with the correct Story
, the new branch is associated with the story’s branch collection via the shovel operator.
Returning to the view, I wanted to ensure the author was able to easily navigate through the pages of their story. I decided to add buttons to allow a user to quickly navigate to the parent page, parent story, and sibling stories. To do this, I had to create a system to ensure any given page rendered routed to the appropriate page.
To do this, I created a few additional routes for my BranchSrv
service, integrated with the Rails BranchesController
:
PageEditCtrl.js
BranchSrv.js
branches_controller.rb
Getting sibling pages was comparably simpler, as that only involved looping through the branches of the parent page.
PageEditCtrl.js
While navigation between pages was functional, I still felt the linear display of pages within a story wasn’t visually appealing or useful to users. Thus I set out to create a tree view that would render out all the pages, visually displaying the various story paths a player could go down.
To do this, I used a recursive function to populate a multidimensional tree
array, then a recursive Angular template to render it out.
StoryEditCtrl.js
branch_tree.html.erb
tree_element_renderer.html.erb
With these navigation features implemented, I moved on to adding a User system. For the sake of the project, I kept the user system fairly simple, allowing easy signup and login without special authorization or authentication. This was done to constrain the focus of my project to its core features, though it is an area I intend to revist in the future.
The User system primarily served as a means to associate stories with their authors. Whenever a User creates a Story, by default the story is not published. When a User clicks the button to save their story as published (via their published
boolean attribute), then it will be visible to all Users. By the same token, Pages
can be saved as finished or unfinished, and a Story cannot be published until all its child pages are marked as finished (via their finished
boolean attribute).
With these features implemented, all that remained was styling, cookies, and integration of ckEditor for text editing, which were comparably straightforward.
The Outcome
As intended, this proved to be my most challenging and sophisticated project to date. While I felt I had a solid handle on Angular going into the project, integrating it with a Rails backend took some effort and research to tailor my code to work with it. Once I understood how ngResource
queried the routes generated by Rails, the connection clicked, and it became much easier to work with.
The best way to set up my Page
and Branch
associations was something I agonized over for a while, but ultimately what cemented my decision was the question of flexibility. If a page directly knew its parents and children, then that would mean the path to any given page would be fixed. Hypothetically, a user may want to have certain choices lead back to an existing page, and I wanted to be able to support that. In its present form, my app has no easy way for players to do this, but I made the decision with an eye for where I want to take the app in its future incarnations.
The final challenge was deployment to Heroku. When my page failed to load, my first thought was that it wasn’t properly loading the dependencies needed to initialize Angular properly. I tried switching from npm to bower, and using RequireJS for loading dependencies instead, but ultimately it came down to an error with minifying the code. Once that was disabled, the page worked perfectly.
The Future
As with DSet, I created this app as a proof of concept of a larger app I intend to build. My ultimate goal is to create a platform for users to create and share not only choose-your-own-adventure stories, but text-based games complete with dice rolling mechanics, inventory, and even multiplayer. This app marks the first step down that road, laying out the core functionality for future development.