Tailspin Triage
Project Info
Language: C++
Engine: Unreal 5
Team size: 9
Time frame: 4 weeks
My role: Systems programming
GitHub: Click here
Project summary
For this project, we had to build a physics-based games in teams of only designers and programmers. We received a small budget for purchasing art assets as compensation for the lack of artists. We also had a longer pre-production phase, as we had an extra week to explore several prototypes and choose one to continue working on in this project.
Our chosen prototype relied on a “simple but difficult” player input idea. Players would control helicopters in a constant state of tailspin, and could choose to go upwards (which would not stop the spinning motion) or go forwards (which would stop the spinning). Using these two inputs, players took on the role of rescue helicopters competing for patients in a cutthroat healthcare system.
Camera system
The main feature I worked on for this project was our camera system. We needed a camera that tracked all of the four players at all times, and also kept the patients for whom the players were competing on screen. My first instinct was to rush headlong into a code solution, writing a class that got all of the players’ locations, calculated the largest distance between two players and moved and zoomed the camera based on that. I thought it was a fast, math-forward solution that would work great.
When I brought it to the designers, however, it was clear that it did not meet their requirements. I had gotten so excited about my implementation that I had not even stopped to consider how this camera would be used and what it actually needed to do. After an enlightening discussion where I understood that my camera was moving too slowly and did not correctly take the patients’ positions into account, we elected to pivot to a blueprint version instead.
This time, with the design requirements in mind, it was easy for me to construct a system that did what we needed it to do. I would create a feature for the camera, show it to the designers and iterate based on their feedback. Since we could use our common language of Blueprints, the end result turned out much better than my code solution. The camera moved in relation to a midpoint object in the level, and makes sure all players and patients are on screen. The zooming was handled by a spring arm component.
Subsystems in Unreal
The other systems I worked on in this project made use of Unreal’s Subsystem class. As the project went on, we were afraid that we were starting to bloat our GameInstance with a lot of features that needed controlled lifetimes. So we asked our code mentor for help and he suggested using Subsystems to modularize our code. As we saw it, two features whose lifetime we needed to manage were candidates for Subsystem implementations - our round timer and our score tracking.
I first elected to try out Subsystems when working on our timer. It felt like a smaller feature that would be good to learn a new system on. I needed the TimerSubsystem to both be connected to our GameInstance, since it would be used across all levels, and be Tickable, since it would handle time. UGameInstanceSubsystem did not have a Tick() so I had to make my subsystem implement FTickableGameObject as well. After figuring that out, the implementation of the actual timer was pretty straightforward.
Together with Emilia Lokrantz, another programmer on my team, I then started implementing our StatTrackingSubsystem. Previous projects had taught me how useful pair programming was, so we decided this system was a good candidate to work on together. We needed a way to track the score of players during rounds and in between levels, so that their points could be correctly updated at the end of each round. At the end of a game, after all the levels had been completed, this score would be used to select a winner. Not only would we need to keep track of scores, but our designers also wanted MVP titles for players on the end screen based on other stats like “amount of patients stolen from other players” or “longest time holding a patient”.
The subsystem was connected to our GameInstance and used a struct we called FPlayerData to keep track of all the required stats. In the subsystem, we made sure to add any players connecting to the game in our lobby to the list of PlayerData structs and then used the ID of that player data when updating most of the stats.
Takeaways
An important lesson I learned during this project was that creating features that are meant to be used by others in the team requires that you actually talk to them about how they need the feature to work. Assuming you know how something is supposed to work is costly - it can lead to having to do double work, or wasting time on needlessly complicated features where a smaller solution would have been much more preferable. I learned to speak to designers and read their requirements - not to just start implementing things based on a hunch.
Regarding our use of Subsystems, our GameInstance ended up being very slim once we migrated out functionality to the timer and stat tracker, so we may have over-engineered our solution a little bit. In any case, it was a great learning experience to figure out how Subsystems work.
Another good takeaway, one learned again and again at FutureGames, is that it is definitely worth it to prioritize the team’s health over the product in the long run. We had many sick absences during this project (I myself was gone for the first week of the prototype stage) and the designers had to crunch their portfolios to meet deadlines for meet & greet events. Consequently, we were working with reduced manpower for most of the project. This meant cutting corners, but I am proud to say that due to the great internal communication, we were able to do that by not overscoping and cutting less prioritized features instead of working a lot of overtime.
In the end, I think our game is all the better for it. It’s a small but polished game that does what we envisioned it to do. Much of this is thanks to the time we had to really think about what we wanted the game to be like and try out different ideas. Prototyping makes all the difference in the world, and we could work in a focused way because our vision was so consistent from the start and shared amongst the entire team.