evaporate - devlog and post mortem
evaporate is a game I made for Ludum Dare 50. It's a first-person 3d game where you encounter a camel-obsessed snowman, in the middle of a desert... And you try to help him survive. This is a devlog and a post-mortem, with source code included.
The theme for this Ludum Dare was "delay the inevitable". I struggled for quite a while to come up with a good gameplay idea. In the end I had a half-baked idea that you should help a snowman survive in the desert. You would give him cold drinks, place fans, prepare ice cubes in the freezer. That sort of thing.
When I'd used up half of the first day, all I had was a snowman, a parasol and an empty desert, and no actual gameplay. Heck, even no player controls. I'd originally planned to join the 48-hour compo version of the jam, and it was pretty clear that I'd probably have to crunch like hell to complete my original idea in time, so instead I just stopped working on the game.
While I quit the jam for the rest of the day, I felt bad for wasting what was a quite cool-looking desert and snowman. After getting a pep-talk from my fiancee, I decided I'd at least deliver something and tried to think of ways I could make a game with what I had already made.
I scrapped most of my existing ideas, and instead focused on just the parasol and the snowman. I decided I'd make a very simple first-person game where you just have to keep the snowman in shade for a single day, and then everything else I had time for would be a bonus.
I spent quite some time making a re-usable day-night light direction system based on real-life planet movement. In the end it was just a couple of lines of code, but it was a bit hard to figure out in which order to multiply all the rotations and make it convenient to use in the editor.
The gist of it:
[Range(0, 1.5f)] [SerializeField] float timeOfDay;
[Range(-180, 180)] [SerializeField] float northRotation;
// for now it's always middle of summer
[Range(-90, 90)] [SerializeField] float axisTilt = 23.4f; // default to earth
[Range(-90, 90)] [SerializeField] float latitude = 59.9f; // default to oslo
// start at night
var spin = -90 + timeOfDay * 360;
// pretending the sun is towards positive z and rotate a planet accordingly
var planetRotation = Quaternion.Euler(0, axisTilt, 0);
planetRotation *= Quaternion.Euler(spin, 0, 0);
planetRotation *= Quaternion.Euler(0, 0, latitude);
// figure out in which direction the sun is on this planet and rotate it back
var sunDirection = Quaternion.Inverse(planetRotation) * Vector3.forward;
var sunRotation = Quaternion.FromToRotation(Vector3.forward, -sunDirection);
// tweak north direction to fit the scene
sunRotation = Quaternion.Euler(0, northRotation, 0) * sunRotation;
transform.rotation = sunRotation;
It turned out to be quite easy to use and it was simple to tune the length of the day how I wanted it and also get the right feel for how the shadows should move.
sun exposure detection
After adding some really simple first-player controls, an interactable system, parasol carrying and some introductory dialog I set about actually melting the snowman.
I decided to keep it really simple stupid, and just made a script that raycasts in the directional light's direction and sets a bool to true or false.
I then switched on mesh colliders for all my models and covered the snowman in objects with this script on it on all sides, and simply counted the number of exposed sensors.
It means I have 120 gameobjects each raycasting every single frame, which feels kind of wasteful, but hey, at least I managed to avoid falling into the premature optimization trap this time.
going for the jam instead
So at this point, I had something very rough, but I had a long way to go and lots of issues. I wanted to actually finish writing dialogue, adding some ui and win/lose conditions (and actually melting the snowman and adding some camels).
Luckily, ludum dare has two (or actually three categories), each with different rules. I decided to go for the jam instead of the compo, which meant I got an extra day.
On the flip side, it meant I had followed all the strict rules of the compo (work alone, no premade assets, only open-source code) and my game will now be compared against others using assets packs, people in teams and no restrictions on re-using previously made assets. However, since I'm mainly doing this for fun anyway, I'm trying not to care too much about ranking.
art-style and rendering
One of the things I was most happy out in this jam was the rendering and art-style. The models are extremely simple low-poly (mostly) smooth-shaded objects
They are then rendered once using the standard lit shader. I tried keeping this pass pretty close to the final thing:
I then simply have a render feature (Render Objects) that renders everything again with a custom material that does the ramped/cartoony shading by sampling the opaque texture from the lit pass. Everything in the scene uses the exact same replacement material, but the colors are overriden using
MaterialPropertyBlocks, which I'm setting using a simple script. This means it's quite easy to choose colors that go nicely together and tune on what light levels they start and stop.
I guess rendering everything twice is a bit wasteful, and I could probably do my own lighting as well, but it's really nice to not spend all that time on that, and get it for free (dev-time-wise) from unity instead. For instance, I get rim-lighting and bounce-light simply by adding regular reflection and light probes.
I will probably re-use parts of this in future projects.
- It's my first game where I have quite a bit of narrative. I think it turned out surprisingly well, it adds some life to something that would otherwise be a quite simple and dull gameplay experience.
- I'm quite happy with the art style
- I wasted lots of time writing clean and re-usable code.
- I started out with unclear intentions on gameplay and spent way too much time on visuals before figuring out what the game was about. I think this is what led to my mid-jam demotivation.
- The game has a "puzzle" element to it when you try to figure out how to save the snowman, but you have little time to do so. When you fail, you have to restart and wait a long time. I think that results in unnecessary frustration for the player.
- Start with gameplay, then visuals
- Re-use ramped re-rendering, it's simple cheap, and little work involved.
- Consciously avoid the mentioned timed-puzzle-with-a-long-wait-to-try-again gameplay.
- Focus on the experience, not the code. Only refactor when making new content is really impractical.
Hope you enjoyed reading this. The source code is available on gitlab.