I joined a small team calling outselves "Scyllier than Charybdis" to work on the game "Poseidon and the Argonauts." The game itself is pretty simple, you try to push various pieces of flotsam into a number of boats, getting points for the number of boats you destroy (vs boats that escape) and you get bonused for minimizing the number of waves you can generate.
What is a Wave?Early in the concepting, we knew that we wanted a game that showed off some of the key features of waves. Our early concepts were focused on the additive properties of waves. Meaning that if you align the crests of two waves they'll add together, as will the troughs.
|Blue is sin(x) and red is sin(2x). Green is sum of these functions.|
You can see the waves get higher when the crests align.
|Blue is sin(x) and red is sin(x+pi). Green is the sum of these functions.|
You can see the waves perfectly cancel each other out.
My first instinct was to discretize the play area. You can get really nice-looking waves by pushing a vertex down (or pulling it up), then every frame you average each vertex with its neighbors. Once you discretize it like this you could eventually move to more accurate physical models, but it seemed unlikely to happen during a Game Jam.
Another idea we had was to make our game 2D, and have expanding circular wave sprites expand from your finger. I wanted to go slightly more realistic, so I moved to the idea of a number of "wave generators." These were tiny functions that can produce a height given an x and y position in a 2d field. Another team member came up with a compliant solution that, given an x and y position as well as a wave start time, would produce a circular height map whose shape was defined via a Unity AnimationCurve. It would decay over time and looked excellent.
|AnimationCurve defining the shape of the wave rather than an Animation as intended.|
How do Waves Move Things?Eventually we decided to focus on using waves to push objects around, but maintaining the above mentioned features. To do this, we had to model the physics of how a wave moves objects. A wave doesn't actually push an object, it simply lifts it up and down. The object effectively enters a free fall down the face of the wave, with drag caused by water slowing you down.
|Gravity pulls the object down.|
Object pushed forward due to the wave surface.
Free fall is slowed due to drag.
When I take out the height of this normal, I get a direction that the object is moving scaled by how vertical the wave is. I can then apply a force found by multiplying this by some scalar defined per object (to simulate heavier or lighter objects, or objects that are more or less streamlined).
The best part about this model is, if you don't setup the wave right, an object will ride up the front of the wave and down the back side. This iteraction is observable in the real world, and gets incredibly realistic looking wave interactions with little work.
My first attempt at rendering waves was to create a plane in the world. Each vertex will be evaluated once a frame, sampling its position in the height field. We even get the surface normal from our movement calculations! This turned out to scale incredibly poorly, especially when running on a phone. One team member had a Nexus 5, so this wasn't going to fly.
After the normal optimization steps in Unity (using native arrays over List and switching to for loops over indices over foreach with an enumerator), I decided to change the geometry to minimize the number of vertices in the scene. I generate the vertices in viewport space, ensuring an even distribution across the screen. For each vertex, I use Unity's ViewportPointToRay function to generate a world space ray, then intersect it with the water plane. After this projection, I sample the height field to move the point up or down. This let me halve the number of vertices and maintain the same (or even improved) level of graphic fidelity.
In the main menu, it was decided that viewing the horizon was important. I'm sure you are worried that this method wouldn't hold up when I didn't have a ground plane to intersect. If the raycast misses, I simply projected the vertex to the far plane, and let the heightmap logic pull it down. Visually it actually worked pretty well (although waves appear broken if you touch too far back).
|Note that the water mesh in the editor is evenly spaced across the display, with the only protrusion being from the ripple.|
What is Missing
Due to the way I was performing mesh generation, the UV coordinates of the mesh were locked to screen space. Not only did this make a water texture stand still when the camera moved, but the perspective shearing was undone by the vertices being placed from viewport space. I opted remove the texture entirely, although I could've tied it to the world space position of the vertex in the x,z plane.
The water has just basic lighting on it. I would've liked to change the color based on the height of the wave and how "up" the surface normal was, which could've been done with a simple shader. Since we also computed the radius of the waves, we could've spread "foam" particles around the wave to simulate the waves breaking.
What Would I Change
I mentioned before a model where you'd simply move vertices up or down and let them normalize out to simulate waves. I really would've liked to experiment with this, it would've let waves crash around islands and let me render trails behind the ships without having to come up with a new equation to factor into the height field. Most importantly, it would've easily let me allow a user to drag their finger to generate waves.
Watching people play the game, everyone wants to drag their finger. I would've liked to support this which could've been done by either changing the water model (as mentioned above), dropping multiple "wave points" (which I capped at 16 for performance and code simplicity reasons), or generating a model where I track the start and end points of the touch. I could simulate the magnitude of the wave mathematically as a capsule, with the radius changing based on the time the point was alive.
The model used in the game for waves was incredibly simple and ran well on all the devices we had available. Despite not being incredibly accurate, it turned out very well. Check out our sourcecode and final APK form our Jam page: