An exploration of how Downwell might play in 3d.
For Inspire Jam, I reinterpreted the game Downwell, changing it from a 2d pixel art game to a stylized lowpoly 3d game.
Goals Design and Dev Procedural Generation Shaders Audio
Design and Dev
Player and Camera
To control the player, I started with the popular Kinematic Character Controller. I like how the asset gives you full control of the movement logic while encapsulating the underlying collision system. I hooked that up with Unity's new input system - it has some quirks but I appreciate how it abstracts multiple input types into actions. Even for small projects gamepad input is a priority for me because I just enjoy using one, but most players will use keyboard+mouse.
The player movement is snappy, with high acceleration starting and stopping, identical on the ground and in the air. Jumps are variable height and there's some 'coyote time' after walking off a ledge.
Camera aiming without using the right analog stick or mouse was a fun design challenge. If I could ditch any need to look left or right, I'd only need to adjust the pitch. It seems clear enough that if the player is on the ground they don't want to be looking down, but I also wanted to be able to peek over ledges so you could see what challenges lie below. So to determine the camera pitch, I use a series of raycasts which detect floors and walls in front of the player, as well as other contextual clues like if the player is jumping.
Implementing caves without camera rotation also required a creative solution. Since the caves are completely safe, you don't need to be able to see where you're going on the way in. So all the cave entrances are on the back-side of the well, allowing the player to see obstacles as they exit the cave. An arrow pointing into the cave should help players know it's safe to back into the entrance.
The first thing I modeled for this project was some enemies. I love reimagining pixel art sprites in 3d. They simply use vertex colors instead of texturing. I followed some of the Downwell color conventions - enemies that are mostly white on top can be stomped on, mostly red have to be taken out with your weapon.
All of them are takes on Downwell enemies, but I changed some behaviors. The snails in Dropchute have spikes that fire outwards periodically. The evil eye AI tries to go to a location below the player before attacking them directly - this actually helps them intercept a descending player more often. Enemies have some variations - versions that are faster, have more spikes, or have attachments.
Attachments are a new concept, there are two types: damage crescents and shields. Damage crescents add rotating hurtboxes around an enemy, shields act like the turtle shell and block bullets.
I tried to use these ideas to make navigating the 3d space more challenging. More difficult enemies and variations spawn the further the player falls. In the end, this wasn't enough on its own, I also increase the player's terminal velocity + gravity with depth to make the game difficult. I think with more enemy types and traps it would be possible to have all the difficulty come from a bullet-hell-like descent.
When enemies get stomped or hit by bullets, a hitstop effect amplifies the impact: freezing them in place, boosting their scale, and flashing white.
The stage is generated endlessly from a set of
SectionSpawner prefabs. Instead of instantiating copies of these prefabs, they're used to create
WellSectionholds the static geometry of a level section.
- This includes the walls and any blocks that always spawn with the well section.
- The top and bottom bounds and total height of the section are defined here. This allows the
WellBuilderto fit sections together.
- A transform for a
CaveInsertionPointcan be specified if a cave can be inserted into the wall of the section.
- Use object pooling for efficient creation and removal.
Entitycomponent is used by game objects that are spawned and destroyed/removed.085
- Enables object pooling and spawn chance.
- Enemies, destructible blocks, and spikes all use the
Dynamicboolean notes if the entity can move between well sections (explained later).
SpawnProbabilitycontrols whether an entity is spawned. This has four factors:
- base chance
- depth chance modifier: a positive or negative number that adds or subtracts from the base chance depending on the player depth. E.g. at 300, the base chance will be increased by .5 when the player is at 150 depth.
- min depth: the depth chance modifier starts counting from this depth, and the entity never spawns above this depth.
- max chance: the spawn chance will never be increased above this when the depth chance modifier is applied.
SpawnProbabilityGroups are also tracked by the
- Allows linking the chance of spawning entities and terrain blocks
- For example a group may contain:
- Destructible blocks with 1.0 spawn chance
- Enemy that stands on the blocks with 0.5 spawn chance
- Spawning of the entire group is controlled by a
OnValidate() is used by
SectionSpawner to preprocess the children of the prefab root, finding
Entitys and saving their transform information + a link to their original prefab. Without getting into too much detail on the object pooling system, this allows any
Entity with the same original prefab to be object pooled together, while using unique transform and
Overall I think the set of pieces I created for well generation (16 total) could have more variation, and there's occasional sloppyness with the placement of some enemies and objects. It would be nice to have more unique gimmick pieces that spawn rarely (like you have to blow up some destructible spike blocks to get through). It's just a bit time consuming to construct and curate many
SectionSpawner prefabs for a small project.
The full graph has some more features:
- Variables to control the influence of the 3 sources. This way, materials for things such as the destructible blocks can primarily show the UV texture which highlights the edges of the cube. Walls and larger blocks use materials with stronger influence from noise and the triplanar mapped texture.
- The 3D noise repeats over a Y-distance set by script. This is important for the seamless endless falling technique mentioned above, otherwise when the game is snapped back to the origin the wall texture would suddenly have a different noise pattern.
Other objects used simple vertex color shaders. For enemies, a variable can brighten the whole mesh when hit by bullets. Gems have a fake reflection using the view normal. Outlines for things like gems and spikes are just implemented in Blender with the flipped normals trick.
Two shaders are applied to the whole screen, distance fading and the palette. To apply their materials I use two instances of a
Blit, based on this one by Cyanilux (excellent tutorials about everything URP!). In Dropchute, I use camera stacking to draw the UI/HUD on top of the game, and here it's important to insert the two
Blit effects one time each in the correct order:
- Render game camera
- Apply distance fading
- Render UI camera
- Apply palette
By default, the
Blit features would run at some point during each camera render. Not only is this unnecessary, but the UI would be erased by distance fading when the level geometry behind it was far away. My edit takes a look at some data attached to the Camera component found in
RenderingData in the
AddRenderPasses method to only insert the render pass for the correct camera. This works perfectly even though it feels a bit hacky - I'm curious if there's a more elegant way to do it, or if this is just something Unity plans to fix by supporting custom effects in the Volume system.
The sounds and music of Downwell have a wonderful arcadey, crunchy aesthetic (credit Joonas Turner and Eirik Suhrke). My amateur sound and music creation for Dropchute involved:
- The Krush VST to emulate some of the creative bitcrushing going on in Downwell
- Bleeper for chiptune-y layers in some sound effects
- Sound effects from Splice
- Bitwig to layer Splice and Bleeper sounds, apply Krush and other effects
- Bitwig to compose the main track below, an above ground variation, and shop music (with a secret alternative inspiration)