A chaotic local multiplayer minigame collection!
A chaotic local multiplayer minigame collection!
Mind Over Penguins is a top-down 2D, competitive mini-game collection focused on maneuvering penguins within icy environments. The mini-games feature unique gameplay styles, each one challenging the players in differing and interesting ways!
Our goal was to design for local co-op, allowing a group of players to compete with one another from a single device. We wanted interaction between players to produce frenetic movement, such as bouncing off one another, contributing to chaotic gameplay.
🔵 Roles: Tools Programmer, Gameplay Programmer, Game Designer
🔵 Collaborators: Emily Barraclough, Robin, Amy Salomon, Madison Renton
🔵 Year: 2022 - 2023
🔵 Timeline: 8 months
I was responsible for programming all features into the project, managing version control, keeping consistent naming conventions in code, logging all telemetry through code, commenting, and cooperating with the artist and sound members to implement assets into the game.
Below I describe my programming work on the features found in our game! Click the dropdown to view its contents.
Snowballs are our main mechanic, and since the team decided we wanted variants, I created a class array to store all the information of new snowballs so we can quickly create new ones and see them in game! The mass and speed both affect how hard-hitting the snowball is towards the player as we use Unity's Physics engine to deal knockback.
I'm proud of the "Chance to get Variant", as each snowball has a random chance of being obtained. I used Super Smash Bros' music selector as a reference, where you can change the chance for each individual track. I first get the sum of every snowball's probabilities on a scale from 0-1, then calculate a new chance of getting each snowball based on the sum, then smash those chances on a scale between 0 and 1 with ranges and select a Random Number to pick the snowball. View the code here.
Pitfalls are ice blocks that break if the player glides above it and stays over it for a certain amount of time. Our maps have many pitfalls and it would be a hassle individually placing them, so I made use of Unity's Game Object Brush, which works the same way a normal tile palette would, but with gameObjects!
Below is a visual of how I calculate if a player has fallen in a water tile. It's not as simple as checking if they entered the trigger, as it would feel unfair to the players, especially if more than half the penguin was on ice and they still fell in. I store the closest point when the player initially collides with the water tile, and then OnTriggerStay, I check to see if the player's position is closer than that closest point. If it is, then we can safely assume the player has fallen in the water. View the code here.
This Salmon appears in our "Salmon Chasers" minigame where players must collide with the salmon to take control of it! For this object, I use an event strucutre where I use a Scriptable Object as a middle-man to remove dependencies between scripts.
The Player and the Salmon both reference this scriptable Object, so when the player gets hit by the snowball, it can call the Scriptable Object to Invoke the event. Then, the Salmon.cs script listens for that event and once it hears it, the "DropSalmon" method will execute. The Salmon also sends an event if a player collides with another player, in which case the salmon gets transferred over, or if the player falls into water, where the Salmon will reset its position to the starting location.
Control Points can be seen in our "King of Penguin Mountain" minigame. Players must stay on the control point to gain score, and the control point switches location after a certain amount of time passes. First, as a group we decided that we want to place the control points down (the locations where the control point object can be). This means we cannot simply place it in a random area inside the screen and need to randomize which control point spawn gets chosen. I made a visual to demonstrate this behaviour.
I designed this system where once the game starts, we find all the control point spawn locations in the map and then set those in a random order in a list. This order cannot have any duplicates so I check to see if it exists already in the list and if it does, I continue searching in a while loop.
After this initial randomization, the control point gameObject's Transform is set to the first control point's position. When the timer depletes, we remove the current control point and add a new random one at the end of the list. This new point cannot be the same as the previous. This loop continues until the end of the game.
Spawning the player works in a unique way here. First, since we have a player select screen, we already know the number of players to spawn in. Next, we add "Player Spawn" game objects with the tag of "PlayerSpawn" in the scene. Then, we place the player prefab at their designated spawn point.
With minigames that allow respawning, when the player falls in water and comes back, they spawn at the next spawnpoint in the list. If spawnpoint 2 was last used, then they will spawn in spawnpoint 3.
I designed this system to make it fair for players as some spawn points may have an advantage over others, so this system ensures the spawns cycle through to allow all players to experience a new start. This also avoids spawn-camping behaviours as it would be more difficult to know where the player will be spawned in.
For the racing minigame, players are spawned at the previous checkpoint they went through. I store the current checkpoint's Transform component in a script attached to the player, and when they fall in water I spawn the player at that Transform point.
Instead of deleting the player gameObject and re-instantiating it, I perform SetActive(false) and then teleport the player. Since we use PlayerInputs, each player prefab is assigned a unique Player ID and if we destroy the gameObject, it will mess with the current player count.
Players must complete 3 laps around the track, whoever is currently in front of all the others is in the lead! This was difficult as our maps are complete loops so how can we tell who's in the lead? I first set up checkpoints around the map (red rectangles) with each assigned an index with the Finish/Start line being index 7. The player now cannot skip any checkpoints and must go through all 6 for a lap to count.
I have to use a for loop inside a for loop to check each player against all other players. If we find that one player is in front of another AND they weren't previously in front, then I swap the positions in an array, and then swap the UI. View the code here.
Since we want Mind Over Penguins to be a local multiplayer game for PC and Console, we liked the idea of having 2 players on the keyboard, 1 controls with WASD, the other with Arrow Keys. To achieve this, I had to use Unity's New Input System and assign different control schemes. The problem is, splitting the keyboard for multiple players is HARD. There's hardly any resources online and making a multiplayer select screen was even more of a challenge.
I set specific buttons for the players to join (WASD and E for one keyboard player, Arrow Keys and LCTRL for another keyboard player, and A for Controller players). When a player presses any one of these buttons, they can join the game and select their penguin colour.
If using the controller, the New Input System has a convenient function called, JoinPlayerFromActionIfNotAlreadyJoined(context);, so this same controller can't join more than 1 time. Since I'm splitting the keybaord, I can't use this function as it will block the other user from joining and restrict it to 1 keyboard player at a time.
The solution is to join them manually using this function: JoinPlayer(activePlayerCount, -1, "Keyboard_1", Keyboard.current); In the quotation marks I assign the name to the player and the activePlayerCount is their ID. Once this player joins, I set a boolean value to true to block them from joining again if they press buttons. I highly encourage you to take a look at the code for a more detailed solution!
We currently have 4 minigames in Mind Over Penguins and plan to add more in the future. Since adding minigames is a big aspect of our game, I needed to make a modular and scalable system to manage all the new minigames we add. I wanted a convenient way to set up a new minigame scene, so I created a prefab and use an enum to set the minigame. In the "MinigameManager" script, I check the minigame set in the enum and add a specific minigame manager class to the scene with the functionality of that minigame. For example, if I set the enum to "Global Warming", on start the minigame manager will add a new component to this gameObject called "GlobalWarmingManager", and delete the "MinigameManager" script.
For this structure, I made use of Inheritance to make base behaviours that can be seen in every minigame such as the player respawning, and a winner at the end of the game. Then with inheritance, I override the base methods and add the specific functionality of each minigame!
Telemetry was collected for two of our playtests to collect data and make improvements based on the data. Below is a list of our Telemetry Hooks, the data our telemetry collected:
After starting a new round of a minigame, how quickly do players die?
For each minigame and each map, where do players die?
What is the average round duration and total duration of each minigame?
How fast do players complete laps in the racing minigame?
How often are snowballs thrown?
Of snowballs thrown, how often do they hit other players?
When ice snowballs hit players do they hit before or after bouncing?
How often do players die after getting hit by a snowball?
How long does it take for them to die after getting hit?
Where do players spawn/respawn on the map?
I used a custom tool my professor created to collect and export Telemetry. The "TelemetryLogger" class contains methods to receive struct data or single variables and send it off to an online database, where we then import that data into an Excel graph.
Above is a sample of our spreadsheet where all of the logged telemetry from the playtest sessions is stored. This spreadsheet logs when and where the players died on each of our maps. In the "King of Penguin Mountain" game mode, players respawn when they die, so we also log the deathCount of the player. As the data shows, one player died (fell into the water) only 3.69 seconds after they initially spawned in. This data in raw form is not that useful, so we convert it to graphs to visually see our results.
These heat maps show where players fall in the water in our "Global Warming" minigame. Players do not respawn, and as we can see in the top middle graph, many players die very close to their spawn points (stars). This results in the rounds lasting only a few seconds and players not understanding the game. Using this visual data we can see where our level design is going wrong and set actionable steps to improve our game's experience.
Jira was used to help organize our workflow and focus on specific tasks, a process I thoroughly enjoyed. We added story points, analyzed our sprint burndown and velocity charts, and made predictions to how much we were able to accomplish per sprint. Click the dropdown to view more information on the processes.
We created a loose schedule for the sprints/work we want to get finished every week. While some of these goals have changed based on how busy workload was in the semester, we had a rough idea of what to implement and this helped us keep track and move forward. The main focus with this was to keep an open line of communication between group members, as well as having a concrete task for everyone to complete each week.
Our team ended up getting ahead on our weekly deliverables which resulted us in changing the schedule slightly to allow for more polish time. Names are also seen as we appointed owners to each feature. The owner would have the best vision for the feature and ensure quality in all aspects from the gameplay to sound effects.
Close to the beginning of the project's development, the team and I created many issues on Jira with features that we plan to implement. The backlog became populated, so our next step was assigning Story Points to each issue to understand its difficulty. It's typical for teams to use the Fibonacci sequence to determine their story points as it can scale up if ever needed, however, we decided to use a scale between 1-10 to simplify the process. We used one issue as our base story point reference and ranked all other issues in the backlog in relation to how easy or difficult it was compared to the base reference.
After assigning story points, we re-ordered the backlog in terms of priority, with high-priority features closer to the top. This way, when creating new sprints each week, we know which features we must work on to deliver the core gameplay experience we hoped to achieve.
Not including the first sprint where we weren’t introduced to story points yet, our average story points completed per sprint is 15.5 with our scale being from 1-10.
Our average story points completed not including the Salmon Chasers and Polish sprint or the first one is around 10 per sprint. The sprint burndown chart below visualizes this specific sprint and reveals that we might have overscopped for this one sprint as we couldn’t get half of the story points completed before the planned end date. Because of this, instead of stopping this sprint and starting a new one, we continued working in this sprint but added more user experiences, and went on for another whole month working on it. The very long horizontal line here is due to us forgetting to put the user experiences in the Done pile.
As the programmer, it was important to document all my changes. Even though Sourcetree and version control allowed for comments with each push, I wanted a way for all team members to easily access the updates to the project and why they have been made.
I created a Changelog document that outlines:
What was added
Why it was added
Problems and Bugs
Comments on the sprint
Now if we ever want to go back to a specific version, or in the future understand why I programmed something a certain way, I can refer back to this document to read my thoughts when making the changes.
I plan on using these techniques on future projects to keep a clean code structure that anyone could look at and tune values.
Jira and the SCRUM workflow will also carry over to future projects. Having a sprint goal that outlines exactly what I need to get done, and their priority has helped me stay focused. Creating an environment where we can all throw out random ideas enhances the game’s creativity! Don’t be afraid to shout out random, out there ideas since they may be what the team is looking for. It’s a designer’s job to take these ideas and make them work with the existing systems.
I approached most of the work in the project knowing that we are all designers. I wanted to make it easy for anyone in the team to fine-tune values, which is why the parameters for every mechanic and minigame can be found in scriptable objects. To complete the work, I would first visit our team’s Jira board and figure out which user experience I’d like to complete. With a notebook, I write down everything I think I need to complete. I have logic of the feature, basic code, and simple visuals to showcase my thought process. Once I have a good idea of how I will implement it, I will start coding, usually writing pseudocode to better help me focus on what I will be coding.
My behaviour of having a positive attitude towards our project and each other helped the team to say out all these creative and wacky ideas we had. I know my capabilities as a programmer, and even if I thought something would be difficult, I said it could be done! This is because I not only liked the idea, but also could learn something new in the process. During development, I found I have a very good sense of clarity in writing tutorials, as well as problem-solving. I discovered my interest in creating systems or tools that other members can use on the team if they want to create maps or tune the values of our mechanics as well. It feels like a revelation or an awakening.
I found success in organizing and structuring all code for scalability with the project. As the project grew larger it was easy to maintain and add new features due to following my responsibilities and creating dynamic and flexible systems. I faced challenges when we did not update our Jira board or start a sprint at the beginning of each week. I enjoy visuals of tasks we must complete, so without this reference, I was less confident on what I needed to add for each mechanic..