Penguin Noir
Description
Penguin Noir is a fast-paced, momentum and combo based Unity Game that released on Steam in May 2023. You play as P.I. Gisueppe Squawks, a penguin detective as he tries to unravel the mystery of his girlfriend’s death.
Gameplay revolves around getting and maintaining speed, as well as moving through each level in a fluid manner. Players rack up combos in a system similar to the Tony Hawk Pro Skater games, and complete each level when they find the three pearls hidden around it.
Like many programmers interested in games, I had tried to make my own before this project. However, I never attempted to make any of them original enough or polished enough to become commercially viable. This project challenged me to do just that, and let me experience firsthand the goals and challenges of each phase of game development, starting from the very beginning.
What I Did
Agile Development Team: As part of a course sequence at UCCS, I participated in a team of ~15 other game developers to design and create a commercially viable game using Agile methodologies. There were three phases to development: prototyping, main development, and marketing, and I participated in the first two. We made several prototypes of the game in GameMaker Studio 2, but the majority of the development took place in the Unity game engine. Additionally, we utilized Git and Github to facilitate collaborative development.
Feature Lead: I was chosen as one of two feature leads during the development process. As such, my responsibilities included being a “problem solver” for any team member who needed help, and reviewing and approving the work they submitted before it was merged. Within the group, it was known that I was a go-to person whenever someone had questions, especially if they were coding related. I often met with the same team members multiple times before their work was approved in order to hold them to the highest standards possible and help them get there.
Features: I worked on a wide variety of features during the development process, including but not limited to: the state-machine based player controller, a custom component that adds Bézier curve colliders to objects and provides an interface to edit them in the scene view, and a dynamic instantiation system to automatically add commonly needed objects such as the player and HUD to specific scenes.
What I’m most proud of: Dynamic Instantiation
One of the main pain points of our development process is that Unity’s native file formats do not “play well” with git source control. The files that correspond to prefabs, game objects, scenes (and many other Unity concepts) are written in a special markup language that is not easily human readable. Furthermore, seemingly normal development activities, such as adding a new object to a scene, can cause nasty merge conflicts when done by different people at the same time since it is often difficult to determine how these changes will affect the scene file on disk. It is even more difficult to produce a valid scene file with both sets of changes.
Because of this, our team decided we needed a way to place objects in the world that wasn’t saved in the Unity scene file. This is the problem solved by the dynamic instantiation system. I developed the initial system and performed many update tasks related to it as the game grew. There were several technical challenges I had to solve associated with this system.
Excluding Scenes: First, not every scene requires the dynamic instantiation. For example, the title screen does not need the player instance, HUD, etc. , so we must be able to specify some scenes to exclude from automatically adding these objects. I solved this by simply keeping a list of scenes that should not have dynamic instantiation applied, and checking if the current scene was in this list before adding any objects.
Scene Loading Order: Secondly, we use a separate “singleton” scene together with additively loading each level to allow objects like the game manager and audio manager to persist between different scenes. However, the previous implementation of this system did not guarantee which scene (the one with the singleton objects, or the one corresponding to the current level) was active at any time. This caused the dynamically instantiated objects to sometimes end up in the singletons scene, and be unintentionally persisted between scene loads. To solve this, I implemented a coroutine that waits for both scenes to be fully loaded, and then sets the current level as the “active” scene in Unity. After modifying our scene management code to use this coroutine, the dynamically instantiated objects always appeared in the correct scene.
Editor Callback: Finally, it would be very inconvenient for the development process if this dynamic instantiation only occurred when playing the entire game from the main menu. Therefore, I added a callback to the Unity Editor’s scene management so that any time a scene that needed dynamic instantiation was played within the engine, the correct objects were still instantiated. I used a pre-processor macro to compile this code out of all builds, preventing any runtime efficiency from being lost.
What was most challenging: Approval Standards
As a Feature Lead, I was responsible for reviewing the work of each person on my team, and it needed my approval before it could be merged to the master build. Early in the development process, I did not fully understand what this responsibility meant.
Redoing Work: Our development environment used Agile with Scrum, and described features using user stories. Each user story had one or more tasks associated with it, and each task had a set of “Criteria of Satisfaction” (CoS) that must be met in order for the task to be competed. Originally, these CoS were not very specific, and I interpreted them far too loosely. This resulted in work being included in the game that we ultimately ended up having to redo later on, wasting development time.
Selective Approvals: Over time, I learned to be more selective with my approvals. Instead of approving anything that technically met all of the CoS, I started to look deeper at each task. I evaluated not only if it technically worked, but also if the way it worked was fun, and interfaced with the other features of the game well. I tried to think of any way the feature could be improved, even if it was not something that we originally listed on the CoS. Because of this, I started rejecting initial versions of tasks much more often, and giving significantly longer lists of feedback to each person.
Higher Quality: Originally, I thought that this would slow overall development and cause teammates to grow frustrated with me. However, the exact opposite occurred. Pushing my teammates beyond the basic requirements vastly improved the quality of the end product, even beyond what we had planned, or previously thought possible. Since the quality of each feature individually was higher, it was easier to make new features interface well with previous ones, meaning we spent less time fixing bugs caused by sub-par implementations. Overall, I noticed a huge improvement in several aspects of development once I switched mindsets from “Good enough” to “As good as possible”.