Post

Locomotive 115

Locomotive 115 is an action horror indie game about surviving a deadly train ride while managing resources.

Locomotive 115

About

I worked as a lead software designer for Bucket of Fish on this project and we shipped it in Novemeber of 2024 after a 3 month development cycle. The game was intended as a preliminary test for our startup to see both how our skills meshed, and also reveal pitfalls in the entire software production/game design process. It became a massive success for us because not only did we acheive our goals of learning a lot, but we also sold nearly 1,500 copies to date which was an unexpected but welcome surprise! Below is the trailer for the game and also some gameplay footage!

Official Trailer

Community made gameplay footage - Thanks @The Librarian on Youtube!

Check it out now on steam! Go to Steam

Technical

There were a lot of interesting technical systems involved in the production of Locomotive 115, but I want to talk about the most recent one I implemented, as well as some of the key things I learned throughout this entire project.

Budget Based Difficulty Scaling

For a post-release update, I developed a difficulty scaling system for an endless runner mode (this is a mode where you keep operating the train as the game gets harder and harder until you lose). We have a ton of settings which all relate to specific in-game events which will affect the difficulty. Everything from the time between monster spawns, to the minimum speed allowed, to when track switches will happen can influence the perceived difficulty of the game. I needed to write a system which would scale the difficulty of each setting from as easy as they got to as hard as they got.

There are a lot of options for this, however, I wanted to achieve 3 specific things:

  1. Make each run feel unique
  2. Keep the overall difficulty scaling fair
  3. Allow for easy iteration for the artists and playtesters who would tune the system

To this end I opted out of the naive solution of having each setting follow some curve as the time increased. Instead, I implemented a budget based scaling system which met all three of these goals. To implement this, each setting was given a cost, and each time the setting was selected to get harder, that cost increased. Each time the game increased in difficulty, the system had a fixed budget that it could spend on setting changes. These settings were all stored in a min heap for easy access to the cheapest options, which the system always chose first. This allowed for easy to finetuning of which settings should be increased and when.

To make this system more dynamic, the cost increment that a setting experienced after being selected was also finetunable. As an example, this means that the designer can make some upgrades cheap initially so that they are purchased quickly, then rapidly get more expensive so that other upgrades which remain cheaper for longer get selected before this one again. In addition, I added a random jitter feature which applied a jitter to the cost or priority of every setting in the heap. Setting this jitter to higher and higher values allowed for more and more uniqueness between runs meaning a user could play multiple times and never experience the same parameters, but, due to the fixed budget, still experience a predictable difficulty scaling.

To allow for much easier testing, I wrote a quick graphing tool which simulated and displayed the settings over time so that they could be rapidly iterated on before actually playtesting which takes a really long time. The graphing system is seen here:

alt text

In addition to this system, this project did its job as a test run and I have a lot of technical takeaways from it.

Product Lifecycle Experience

I now have experience taking a software product through its entire lifecycle, from planning, to initial prototyping, to making systems scalable, to releasing and supporting with updates and patches.

Singletons as an Anti-Pattern

This project made me realize that singletons tend to do exactly the opposite of the common reason they are used. While I used to use singletons to attempt to decouple logic because they don’t require specific object references, I found that after making many of them in Locomotive 115, they actually had the opposite effect and increaseed coupling. We could not test anything by itself because it needed the support of 3 other singletons. In my current projects I have moved almost entierly away from singletons.

Need for Automated Testing

The game, though only spanning a 3 month development period, quickly became too large to playtest repeatedly. In my current projects I am using the Unity testing tools and github actions to automate test running.

Interfaces for Testing

Similar to the previous thing, I realized the need for interfaces in order to create mocks for testing. This game quickly became too large to test all together and so we resorted to playtesting rather than unit and integration tests. In my current projects, I am using interfaces everywhere so that testing is easy and able to be automated.

Cachable Event Systems

Another important thing I learned was that the event system package I wrote needed the capability to cache the most recent data send with an event so that late spawning objects who need the data immediately can query for it. Doing this will consolidate all static scope into a single middleware instance rather than having it spread across many classes.

This post is licensed under CC BY 4.0 by the author.