Global Game Jam 2017 recently ended. It was a mad weekend where 36,000 game makers gathered in more than 700 venues around the world, and made 7000 games based on a common theme: “Waves”.
Instead of organising in Rome, this year I flew to Prague and made a game with a graphic artist named Jana Kilianová. Our game is called Splash Clash, and it’s a 2-player brawler on a tiny island, where two pixel characters jump to produce waves and bump each other out. This final concept is just one of the many we got during brainstorming: for instance, we toyed for a while with the idea of having players produce waves by making a sound in the microphone, but after some experiments we settled for this one - the one that seemed achievable in 48 hours.
From the very start we knew we wanted a cool and somehow believable water effect, so we didn’t want to use any approximation like 3D wave objects spreading in a circle, or particles. Instead, I thought we could use a circle texture on a Displacement shader, so the wave would be created from the surface (a simple plane) and if we then scaled this circle, we could have an animated wave.
Disclaimer: This post is intended to show you how I created the effect quickly during GGJ, not how it should be done! There are probably better ways in regards to performance! (perhaps doing it all with shaders?)
Before setting off, I made a mockup by creating circle textures in Photoshop and using the first sprite found on the web, Batman:
Being no shader wizard, I found a very simple free Displacement shader in the actual Unity Manual, which also supports tesselation (so I wouldn’t have to worry about the geometry). Once applied to the water, the Material looks like this:
As you can see it has a tessellation amount (that I pumped to the max!) - which controls how many times the surface is divided - and a displacement slider - which controls the height of the extrusion.
First thing, I wanted to verify that what I wanted to do was actually feasible. So I started with a single white circle texture (with transparency), then wrote a function to stamp it on top of a black texture. Once applied as a displacement map, it looks like this:
After that, I expanded the function to allow stamping more circles.
I had to composite multiple white circles together, to achieve having multiple waves moving on the surface. As you can see from the image above, the circles were quite high resolution at this point (I think they were 256x256 on a 512 black texture).
Since each wave is independent, the whole texture has to be deleted each frame and recalculated with the scaled-up circles. Once animated, they looked like this:
At this point, I found out that I had to reduce the texture size, A LOT. The method I was using is not optimised at all: after all for a 512x512 bump texture it means 262,144 read and write pixel operations, for each wave! (remember: the waves were 256x256 but they would scale in time, needing to rewrite the whole thing each frame) This brings us to a grand total of 1,048,576 pixel operations every frame with just 4 waves on screen!
So I scaled down everything, settling on a mere 32x32 for the ring textures and 64x64 for the whole bump. After a few tests, I ran into a problem: having a small texture, scaling it down (since the ring starts very small) and then scaling it up again destroys it until it’s not a circle anymore.
So what I did is cache the original ring texture, and use that to re-generate the rescaled circle every frame, instead of just transforming one texture all the time (which would make it useless after a couple of scale-ups). This means that each wave has a reference to the original ring texture, but instead of using that it just copies the pixels onto the rescaled one.
It worked fine on a plane, but when I applied the material to a circular disc shape I got this weird effect, with the sides of the disc extruding outwards:
I just had to apply one small modification to the shader: instead of extruding along the pixel normals, I would just extrude up. I changed this line:
v.vertex.xyz += v.normal * d;
… to this:
v.vertex.xyz += float3(0,1,0) * d;
Voilá, I had my waves finally working!
Once the bump was done, it was time to take care of the colour texture. As you can see from the animation above, at first I had a shadow from the Directional light to highlight the waves’ profile, but it wasn’t enough at all. I needed something visible, something that the players could use as a feedback to time their jumps properly.
At first I basically applied the same trick again: white texture rings printed on top of the colour texture, exactly in the same position as the ones in the bump/displacement:
In the above animation you can clearly see how the white circles display the low resolution of the textures used, especially when the circle is big.
This is when I decided to modify the shader to paint the pixels at the top of the wave in white. After all it makes everything faster, since I cut half of the texture readings each frame.
I just changed the line that calculates the pixel color in the surf function of the shader, to something like:
half4 c = tex2D (_MainTex, IN.uv_MainTex) * _Color * (30 * IN.worldPos.y + 1);
By using IN.worldPos.y, I make sure to take into account the height of the pixel in world space. The higher the pixel, the brighter it will be, tending towards white. The result is:
Once I had the graphics in place, it was time to make sure that the two characters got pushed by the wave. This was an easy one: I made each wave a gameObject with a Sphere Collider, marked as trigger. As the texture is scaled up, the Collider is scaled up as well.
Once it collides with one of the players (OnTriggerEnter), I just verify check the y coordinate of the character’s gameObject, and if it’s below a certain threshold (which is connected with the wave strength - a float that decreases in time) the character is pushed back by applying a force to its Rigidbody proportional - again - to the wave’s strength.
I then filter the collisions by tag, so waves from player 1 only hit character 2, and vice versa.
Just a small mention regarding Drag. When going for a fully-physical approach, usually you apply a strong force (AddForce) to characters to make them accelerate quickly, then use Drag to balance that out so they don’t drift. But adding too much Drag will make the character descend slowly when jumping, creating some terribly floaty physics.
In this case I implemented my own fake drag, by multiplying only the x and z components of the Rigidbodies’ velocity by an arbitrary factor, to keep the y component (the gravity) intact:
rb.velocity = new Vector3(rb.velocity.x * .8f, rb.velocity.y, rb.velocity.z * .8f);
Finally, as you can see from the animation below, the game is a mix of 2D and 3D, with the characters being Sprites rotated 30º on the x axis to match the camera rotation, as are the backdrop and the rocks below the scenario. The disc of water is basically the only proper 3D object in the game, as I needed it to get complex and believable waves.
The characters have a Capsule Collider, and a Rigidbody whose rotation is locked so they just more around without tipping.
I have added particles to the jump to provide feedback on the jump, and to provide the so-called juicyness (remember Martin and Petri’s talk “Juice it or lose it”?):
For the waterfall particles, I just used a big Circle emitter and I separated the front particles from the back ones (which are spawned by another particle system) by putting it on 180º in the Shape module. The back ones don’t change in colour and have a way shorter life span, since they are never going to be seen.
As usual I randomize every parameter I can (lifetime, speed, gravity, etc.) as long as I’m using it, but in this case I kept the size fixed because I want the particles to have a pixel art feel. For the same reason I used a Fixed gradient in the Color over Lifetime volume instead of a Blend one, so they change colour instantly, instead of fading away as I would usually do with realistic particles.
As you can see, most of the solutions I adopted this weekend feel dirty, unoptimised, and almost like a hack. That’s fine! Jam games ARE meant to be quick and dirty, they don’t need to be performing well.
In fact when I have to decide what to spend my time on at a jam, I prefer to spend it on code quality instead of optimisation. I like to have a relatively solid approach at coding (say, less spaghetti code and more logical class structures and information flow) to avoid getting bugs at the last minute. This usually keeps me away from nasty, incomprehensible, last-minute bugs!