I have always loved collaborative games that descend into chaos, with every player shouting
at
each other. Sadly, this subgenre hasn’t offered many titles, so in 2020, inspired by
games like
Overcooked and Keep Talking and Nobody Explodes, I decided to create my own and embark on the
journey of developing my first commercial video game.
I learned a great deal on the road to release, not only in technical nature, but also in
collaborating with artists (for environment art, music, and icons), graphic design
with Figma
and Photoshop, video production and animation with Blender and Davinci Resolve,
marketing and 3D Modelling
with Blender. The technical part was, of course, the biggest one. I relied heavily on
Unity’s
built-in systems, customizing them and creating my own tools with C#. I wrote shaders
with
custom lighting pipelines in HLSL, set up automated testing and build pipelines using
Unity's Testing Framework and GitLab CI,
and integrated everything deeply into the Steamworks ecosystem for Remote Play Together
multiplayer, Steam Cloud saves, and Steam Input. A few of the coolest features I implemented
are
described below.
Git / Gitlab -
Code
Distribution & Management, CI, Planning
Figma - Design,
UI Prototyping, Vector Graphics
Photoshop -
Graphics
Davinci Resolve
-
Trailer, Tutorial Clips
Trailers
Screenshots
Pathfinding
Truck Pathfinding
Pathfinding was one of the first things I’ve implemented when starting to develop Garbage Crew!
Every street tile is a node in a weighted graph and I use Dijkstra’s algorithm to find
the
shortest
path from the level start to the goal randomly placed on the map with a few special cases that
are
considered like only generating paths in the trucks driving direction (it can’t turn by itself)
or
handling driving into dead ends.
After the path to the goal is determined, a track for the truck is determined by a few factors
like
driving on the right in Level 1, moving to the middle of the road in curves to not get stuck and
avoiding parked cars by driving either to the left or the middle of the street. As parked cars
are
spawned randomly, a probe is spawned on each track node that checks for cars on the
track.
The Green Path shows how the truck defaults to
driving
on the right but avoids parked cars
and
uses the middle of an inner curve to not get stuck on objects on the sidewalk.
After all of this is done, the track is smoothed by generating a Bézier curve that
will
define the
final track.
The green path shows the final smoothed track
The green path shows the final smoothed track
As players have the possibility to both change the truck's driving direction at every
intersection
and control the truck themselves with the right power up this calculation is done not only on
level start but also when changing the truck's direction or when exiting the driver's cabin.
NPC AI
To add a little life to the world, I've added NPCs that randomly wander around the map.
This
was the perfect job for Unity's built-in navigation system.
By setting different weights for sidewalks, roads and road crossings, it was easy getting the
NPCs
to
cross the road on marked road crossings.
Screenshot of the navmesh of the first level
showing
the differently weighted sections of the map
Screenshot of the navmesh of the second level with
only
one weight as there were no road crossings in 1325AD
Generating Levels with reachable goals
Due to the nature of randomness, randomly generating every aspect of the level comes with
a
few
pitfalls like levels that simply not beatable. So I developed a system to make sure every level
the
players are presented with can be beat.
This was particularly hard because the levels are far from deterministic (unless you use a
seed).
When a level is generated the three level incentives, the truck's goal on the map as well as
the
garbage can an event spawns on the map are randomized. But challenging the players with
a
level that
they can’t complete would quickly result in frustration. A lot of these things can be solved by
proper balancing but, due to the nature of randomness it’s always possible that a level was
generated
that the players can’t beat, especially when playing on harder difficulties where the
spawn
rates
are quite tight.
To make sure that only beatable levels are generated, every possible path that the players
can
take is determined at level start. For each of the possible paths, the garbage can
and
event
count along the path is calculated. If there is at least one viable path, the
level
starts. In
case there isn’t
any path that allows to complete all incentives in time, the level will be regenerated
with a
new seed.
The time goal will be set based on the shortest viable path and a predefined value
for
every action
the players have to complete during their play through.
The players won’t notice anything about this process, as this all happens during the levels
loading
phase.
Debug menu view showing all possible paths and
whether
an incentive can be reached on said
path (green) or not (red). The shortest viable path is shown in yellow.
This might leave you asking what happens if you start the level with a predefined seed that
results
in a non-beatable level, as it isn’t possible to just generate with a new random seed. I used a
small hack for this:
This is certainly not the most elegant way to do this, but it ensures players can share seeds
that
always generate deterministic, beatable levels.
Systems, Data & Tools
Most things shown on this page are visually interesting, but a huge portion of the game, and a lot
of cool stuff, is happening under the hood in the form of performance optimization,
batching, game
flow, player interaction, scene management and other systems only existing
in code, with no
real
option to record a
video or take a
screenshot. I’ve compiled some of the coolest systems I’ve designed and tried my best to explain
why
they are noteworthy.
Tooling
Everything that is happening on the map is built around the road. To make it easier to build
said
road, I’ve created a road builder tool that allows me to quickly create a road layout for a
level as
well as change road tile design.
Over the course of developing this game, I’ve built a lot of tools. Most of them don’t look
special
as they just test fire events or recalculate the normals of models, but they were
super
useful
nevertheless. Other than the ones already mentioned, I’ve created a tool to easily edit the
savegame, debug the
placement of
fires around the map, set the editor camera view to various camera positions or
show/hide debug
markers and world-space UIs.
Savegame
Garbage Crew! is a roguelike game, so there isn’t that much to save when quitting the game, right?
Despite the fact that the player can’t save mid-run, there is a lot that is saved in between runs to
make progression and the overall experience seamless. From the skins of each player to the powerups
they have already unlocked, there are 34 properties a Garbage Crew! savegame stores.
[Serializable]
public class SaveData {
public int savegameVersion = 0;
public string appVersion = "";
public bool alreadyBought = false;
public int playthroughs = 0;
public int cleared = 0;
public bool clearedReset = false;
public float money = 0.0f;
public int players = 0;
public long lastPlayTime = 0;
public int lastPlayLevel = 0;
public int lastDifficulty = 0;
public int maxDifficulty1 = 0;
public int maxDifficulty2 = 0;
public int unlockedLevel = 0;
public int powerupMask = 0;
public int boughtPowerups = 0;
public string multiplePowerups = "";
public int boughtPowerupsPlayer1 = 0;
public string multipletPowerupsPlayer1 = "";
…
public int skinPlayer1 = 0;
public int skinColorPlayer1 = 0;
…
public int bucketColor = 0;
}
As you might have noticed, all bought powerups are saved as integers. This works by using
bitmasks.
Every powerup gets an ID that is a power of two (like 1, 2, 4, 8, …, 256, …). This
way, registering
a bought powerup is as easy as:
boughtPowerups |= powerupId;
And removing one:
boughtPowerups &= ~powerupId;
Or checking if the player has bought a powerup:
bought = (boughtPowerups & powerupId) != 0;
This is super efficient in terms of storage, with the serialized Garbage Crew! savegame being
only 1
KB in size, but it’s hard to debug since you need a calculator to quickly edit the save if
you
quickly want to give the player an item to test something. I built some tools like a savegame
editor.
Migrating Savegames
Including a savegame version from the beginning of development was a real lifesafer.
Balancing and
game flow significantly changed during early phases. By detecting old savegames and patching
them
accordingly I could make sure no players end up frustrated because they ended up in an illegal state
because they loaded an old savegame.
private void MigrateSavegame(ref SaveData data) {
if (data.savegameVersion < 4) {
// Remove watering can powerup
data.boughtPowerups &= ~256;
}
if (data.savegameVersion < 6) {
// Remove difficulty cap of older versions
data.maxDifficulty1 = 3;
data.maxDifficulty2 = 6;
}
…
data.savegameVersion = SAVEGAME_DATA_VERSION;
}
Continuous Integration (CI)
For testing and building the game, I created automated pipelines using GitLab CI and a
local build
server, running builds and tests inside Docker containers. This ensured that core
functionality
wasn’t broken by new features and allowed me to automatically produce different releases of the game
from anywhere, without tying up my personal computer.
Build and test pipelines in GitLab
Text & Language Management
I developed the game with support for multiple languages from the start by building a
custom
localization system. No text is hard-coded in the UI or inside the code. Instead, all
strings are
looked up in tables corresponding to the selected language. When the game starts, it
loads all
*.locale tables from the StreamingAssets directory. This makes adding new languages extremely
easy
for both me and the community.
To streamline usage, I built a script that automatically scans every UI element
for placeholder
variables and replaces them with the corresponding text in the selected language. The system
also
supports variables within text, which makes it easy to handle languages that require
different word
orders.
UI showing various placeholder variables
As you can see, there are two different placeholder types. Text in {curly brackets} is
static and
gets replaced when the scene loads, while text in [square brackets] is dynamic,
containing values
like time remaining or tries left to complete an incentive. Dynamic placeholders are also replaced
using the lookup table but require additional data binding to specify which data fills the
placeholders.
Text can not only be translated from variables in the UI but also directly from the code:
To make configuring the game easy I’ve added a lot of custom UIs and Scriptable Objects. Here are a
few of them:
Level settings
Incentive settings
Minimap
The minimap of Garbage Crew! is automatically generated or better rendered. Every map
object
that
should be marked on the minimap has its icon/marker already attached. By setting
said
marker on a
culling layer invisible to the default camera, the player won’t see this marker. A
second,
orthographic, camera is configured to cull to no layer but the one with the minimap
markers. If I
now decide to highlight enemies on the map, I just attach a red circle to their feet which will
automatically be visible on the minimap.
Bottom view of the map showing minimap markings
Top view of the map showing minimap markings
Shader
As you have probably noticed by now, I styled Garbage Crew! to look like a colored comic book.
At
the
heart of this style is a comic style surface shader with a custom lighting pipeline
built
with HLSL.
The toon-shader consists of a few components:
Halftone Shading and Dithering
In my opinion, halftone shadows are the most iconic part of comic style shading. To add this
styling
to my game, I replaced Unity’s default Lambert/Blinn lighting with a fully custom lighting
pipeline, giving me full control over how light, shadows, and stylization behave. Once I
had
everything in place and I understood what this meme was all about, I was ready to go. After
I
had
my
custom lighting pipeline, the only thing that was left was to isolate shadows and apply a
halftone
texture to them. All parts that aren’t shadows get a dither texture applied.
Posterization
Posterization is a classic in comic shading. I achieved this by simply rounding the final
color
output in an after-color pass.
Only the halftone shading and dithering part of the
shader
Only the posterization part of the shader
Outline
No comic or cartoon style game is complete without an outline shader, so I definitely also
needed
one. There are a few ways to render an outline around an object, some better, others worse. I
used a
mix of shading and preprocessing models by smoothing their normals to make them perfect
for
generating an outline. Upon level start, a script scans all models in the scene and converts
their
vertex normals to soft normals by averaging them. I then use an outline pass that
draws
back-facing geometry scaled along the smoothed normals using depth testing, causing only
the
silhouette to remain visible.
Outline scaled by smooth vertex normals (but with
smaller outline)
Only the outline part of the shader
Now we only need some minor details like making outlines scale with distance from the
camera
and adjusting for scaled objects so all outlines look equally thick no matter how far they are
or
how they are scaled.
All outlines look have the same thickness, no
matther
their distance or object scale
Visualization of outline distance to the camera I
did
when building this feature that looked
kinda funky
Snow
For the winter event, I needed a way to cover the world with a layer of fluffy white snow. To do
this
I used a simple trick by lerping between the original texture and white based on the surface
normal. This way, every object that faces up gets colored white.
Only the snow part of the shader
Level 2 with active snow shader
Everything comes together
Every part of the shader comes together to achieve
the
look of Garbage Crew!
Make obstructed objects visible
A lot is happening at once in Garbage Crew! and every player needs to know where they and the
objects they need to interact with are. To make sure this is possible, I developed a stencil
shader so important objects are visible even if they are obstructed by another, less
important object or the UI. This is done by applying a priority shader that writes to the
stencil
buffer. When drawing less important objects, the stencil buffer is used to draw a
silhouette overlay.
The arrows show the silhouette of objects behind
the UI
and less important objects
Water
The second level has a small river that runs through the medieval village. To let this match the
look, I created a toon-water-shader. I used a noise texture to draw a wave
pattern,
distorted obstructed objects based on depth, and added a foam layer around the
corners.
Models
Most models in Garbage Crew were created by the super talented people at Synty Studios, but I’ve
also built and textured a lot myself or extended existing ones to suit my needs. Here is a selection
of some of them:
Various garbage truck attachments
Selection of models I've created
Garbage Pile
To draw the big garbage pile in the base of the first level without compromising performance
by
drawing thousands of objects, I created a special texture and corresponding normal map by
simulating
a pile of garbage in Blender. This way the dump consists of only one big hill and a few
single
garbage pieces on the edges for better visual blending.
Thousands of garbage pieces right before falling down on the pile to
create a texture and normal map from them
The created texture without normal map
With the normal map applied it finally looks like a garbage pile
A few garbage pieces scattered around the edge of the pile to assist
blending on the edges
UI
For the UI of Garbage Crew! I chose a look that matches the cartoony style of the
rest of the game, which is also heavily inspired by the classic comic book style. The
main
menu takes place across the two pages of an open comic book, with the camera panning between
panels
depending on which menu the player is on.
Level and difficulty select
Shop menu
How to play with video backgrounds showing key
mechanics
Message box for showing messages to the player
To bring the UI to life and highlight the comic book nature of the style I've added transitions
for
opening and closing menus.
To visualize player and enemy health, possible interactions, or power up
states,
I've added a lot of world space UIs. To add a little more life to the world, NPCs got
little
thinking bubbles visualizing their thoughts with random icons. Placement of world space UIs
isn't
that trivial in a 3D world, so I've developed a solution that positions the UI so it's always
pointing to the object it is highlighting.
Screenshot showing some world space UIs
The two pages in whole (never seen this far zoomed
out
ingame)
Savegame Thumbnail
To visualize the savegame, I implemented a system to generate a Polaroid style snapshot
showing the party with their gear and money, including a version with partially
transparent gear to show the player what they would lose if they start a new game. To achieve
this,
I studied what makes the iconic Polaroid image style. It turns out the most important
element is that the photo is taken in a dimly lit environment, with a bright flash
coming
from the same point as the camera.
To generate the thumbnails, I follow this principle exactly. I spawn the last party
with their skins, set them in pose, decorate the scene with all their gear, point a spotlight at
them, and render everything into a texture with a camera placed at the spotlight's
origin.
The setup for thumbnail generation placed in the
main
menu hidden from the players eyes
Same setup but for the snapshot without the gear
Savegame thumbnail ingame
Savegame thumbnail that visualizes losing all gear
The day count on the Polaroid shows how many runs were completed with that party. The year
indicates
the date of the last run, with 700 years subtracted when the players were
playing the medieval level.
Trailer
For the trailer of Garbage Crew! I relied on both gameplay recordings and cinematics. I’ve built
the
three news shots all in Unity by scripting NPC and vehicle movement, adding effects, and
using
Cinemachine for camera paths. Creating promo material like pictures or videos for games
is
always so
much fun, as you can just drag everything where you need it without having to think about game
logic
or player interaction. After you are done, you reverse everything in Git, and you are good to
go!
The trailer starts with a news intro. I’ve built and animated this completely in
Blender to resemble
a classic news show intro. At the end of the video, I fade to green to use chroma
keying in DaVinci
Resolve to transition from the intro into the news shots.
The news shots themselves were all built with existing material and by using components I have
already built. I mainly used Cinemachine for camera paths, utilized the existing NPC
pathfinding for
character movement, and built backdrops from props and level components.
One of the news shots, showing a street overflowing
with garbage
Another news shot showing a burning house with a
firefighter running away
For the gameplay shots, I used footage recorded during playtesting. I recorded over 24 hours
of
video material and spent three days watching all of it, marking shots and rating them to
find the best ones.
To bring everything together, I built a living room scene in Blender with a TV
showing
the news and
gameplay parts and a comic book that shows a few of the features of the game. I used
Blender's
built-in cloth simulation to flip through the comic pages that show short
gameplay
video clips. To
match the look of the game, I used Blender's Grease Pencil to add an outline around
the
items in the
scene.
It's actually pretty easy to use a video as texture
in
Blender
Blender Grease Pencil outlines
Now that all parts are ready, we just need to cut everything together, do a little color
grading,
and add the amazing track David Engel composed
for us over the video, and we have a trailer:
I am really happy with how the trailer came out but I think next time I'll shift my focus towards
gameplay and less cinemtatics 😅
The story framing is very long and story doesn’t seek to be a key pillar of the
game,
so I’d remove it.
Garbage Crew! has basically only one cutscene. When you successfully complete the first level with
all incentives met, you unlock the time machine. A device that allows you to rewind time during
gameplay. To encourage the players to use this feature, I spawned them in a level with insanely high
incentives and spawn rates so they can’t catch every garbage can and fire and are forced to rewind
time eventually. Every time they activate the time machine, a piece of the device explodes and when
using it the third time it completely malfunctiones and rewinds time a little too much, teleporting
the players right into the 13th century.
To set the mood for the scene (of course it is raining when something bad is about to happen), I’ve
added rain particle effects and little splash animations on the ground where the
raindrops hit.
Whenever the players activate the time machine, a VHS-style rewind screen space shader is
shown, and the truck
drives backwards on a recorded path it was previously driving forwards on. When activating
the time
machine for the first time, it won’t stop after a few seconds but rewinds faster and faster with the
screen fading to white eventually. I’ve added a special white loading screen that is only
shown in
this very special case to not break the immersion that much. After the second level is loaded, a
portal made entirely from particle effects and a few lights opens, and the truck is thrown
out
violently on a dolly track. And that’s all the magic of time travel.
Level summary UI showing unlocked time machine
Rain particle effects in the time machine level
Splash sub particle effects showing small circles where raindrop hit
Garbage truck with dangerous looking time machine attached
VHS rewind screen space shader
Garbage truck getting thrown out of the portal with all kind of
magical effects
Other VFX
I did a lot of VFX work in Garbage Crew! that I've already talked about in the trailer and and
Shader section. Check them out to learn more!
Platform Integration
Steam
Garbage Crew! uses several Steamworks features such as Steam Cloud to sync save games,
Steam Input
to support a wide variety of gamepads and allow the player to remap buttons, Remote Play
Together controlled directly from the in-game UI and Achivements with beautiful icons
drawn by Anna Tragelehn. All
features
were implemented via Steamworks.NET
Main menu showing the Remote Play Together controls
Steam's Remote Play Together overlay opened from the main menu
All 16 Steam Achivements
Discord
To let your friends know what you are playing I’ve added Discord Rich Presence by directly
talking
to the discord API.
Discord Rich Presence showing game, level and player information
Discord Rich Presence showing game, level and player information
History
I’ve worked over three years on Garbage Crew! During this time I’ve added a lot of features,
removed
others, and changed things hundreds of times. Here are a few screenshots I’ve taken during
development that show unused/removed features or old states of the game.
The first screenshot I've taken showing the trucks
pathfinding
In the beginning cars, players and garbage cans
were
all represented by a colored cube
The first model I've added to the game
This is what the game looked like without toon
shading
First draft of the level UI
First draft of the intro UI. At this time you
joined
the game directly in the level as the main menu didn't exist at this time
First draft of the map shop UI
First draft of the interaction overlay
First draft of the new UI
First draft of the level intro UI
Before the shop was inside a UI you could buy
randomly
spawned items directly in the base
First screenshot of the second level with a rough
terrain laoyout
I have no idea what I was trying to visualize here
but
it definetly looks funky!
The message that was shown when completing the
first
level before there were a second level
First draft version of the main menu
Stickers I've created for the release party
The "fotoshooting" I did for the cover artwork
Early drafts of the logo
Stats
Over the years, Garbage Crew! grew quite big. Because stats are cool, I've compiled some that
show
the
size of the project.
I haven't tracked the time I've worked on Garbage Crew! but I've started on 8th of April 2020
and
released the game on Steam on 8th of December 2023 after 3 Years and 8 Months of development
time. In this time I've created 1192 commits on 316 unique days and closed 409
issues on GitLab.
C# were the main programming language I've used, followed by HLSL. I've created 129 C#
Scripts
with a total of 25702 lines of code and 12 HLSL Shaders with a total of 1594
lines
of code.
Overall I've added 42886 lines of code that made it into a commit and removed
17164.
View of all the scripts and shaders I've wrote for
Garbage Crew!