We want to share how we accomplished our giant fish monster in our upcoming game Zenith Falls! This was a tricky task that required some inner-thinking and late nights standing at the drawing board. We got this inspiration from a really old game that we used to play that had an implementation for when swimming too far out in the ocean you would get eaten by a large fish.
A lot of games have no swim water zones where you drown, die, or lose health. With this script and setup you can create a couple of different systems, such as ours with one giant sea monster, possibly having multiple smaller fish like piranhas, etc, anything with jumping out of the water towards a dropping target, this is how we achieved our accuracy!
For our game, we would have a player drop from a tall tower. It’s a long drop sometimes as you can see below.
So we tried landing in the water, it was boring while we waited for the sea monster to eat us, on top of that the guy was so big we had to zoom out the camera just to see yourself be eaten and it still wasn’t looking the best.
Then the idea came to us, creating a fish that jumps out of the water while you fall to eat you :)
Here’s how we did it. We took the liberty of simplifying the scripts and uploading a package for anyone who wants to skip the tutorial. We included low poly fish as examples and set up a scene for the showcase. (Patreon link)
Starting off we set up the red trigger on Y-axis 0, this will be our water visual and center. We use this as the mirroring center. The green box is where we will call our spawn trigger, this will save us in optimization to only have fish spawned when we call them.
Create two scripts called “SpawnFishTrigger” & “FishControl”, inside of FishControl add “public GameObject target;” just so we can access it for now.
Open SpawnFishTrigger in your code editor.
We add a game object to hold as many fish as we want and a trigger tag to change what reacts with our spawn fish trigger.
Create the void SpawnFish and give it a reference to a gameObject that will be our player or target. We create an “int chosenFish” and give it a random number, 0 between “fish.length”, this will be our chosen spawn fish. (You can add multiple types of fish and have a random fish occur each time / we love randomness)
To ensure our fish is pointing up we give it an X rotation of -90, we access our fish control script and set our target that we prepared earlier.
Next, to call the spawn function, we create an OnTriggerEnter void with a collider reference, this will allow us to check the tag on our gameObject (we set our script tag to “Player”)
We now start working on the controller for the fish, open “FishControl”.
We create our stat variables such as moveSpeed, rotSpeed, curveRadius, boostOffset, eatDistance, destroyTimer
below that, we add our FX and reference boolean to let us know when we arrived at our target.
When our gameObject starts we want to prepare its home location near where the original spawn is at, so we first create a primitive default sphere, set the Y position to a random float and our curve radius will reach out forward by our chosen amount “curveRadius”.
Now we can add our mimic behavior in Update() which is a fairly straight forward line of code, we mimic the X & Z positions while inverting the Y position, to make sure we meet a little before the waterline, we add a “boostOffset” for our fish, so they leave the water before we enter it.
Our fish sets its rest position (sphere on the left), acknowledges the center level, and mimics the player behavior that summoned him + the boost offset.
Next, we add our slow look at functions, subtracting our target position by transform and slerping our rotation for smooth results.
Once we have our rotation and movement setup, we want to prepare our animations (if any) and our second step, falling back into the water. So to avoid errors we first check if we have a target, then we record our distance between ourselves and our target.
If our target Is within our range, call your animation and any other in range functions, else prepare it for starting step 2.
Inside of our “!reachedPoint” statement we invoke a new function “FallSpeed” after 0.2f, we change our target to our restPosition and turn our last target invisible to simulate being eaten. Turn our “reachedPoint” bool to true and let our fish know to start step 2. (Also start a destroy timer)
Our FallSpeed void is fairly simple, but this will give the illusion of gravity when diving back into the water.
Multiple players each spawning a fish, that turns to its rest position after reaching its target. Perfect for an online or multiplayer game world
The final step is adding the splash FX, we do this by adding two triggers to detect when a fish enters and exits the water. Be sure to add a rigid body set as kinematic and a collider set as trigger for these to work properly.
With some animations, tail physics, and tweaking you can get some interesting results, is here is our sea monster after adding a tail animator.
Thanks for reading our tutorial! Include your own shaders, fish, and animations than you can make your own creations come to life as well!
We included a scene for you to get started (splash prefabs, monster, and water are not included), and if you need water for your game or testing, here is a free GitHub resource to Crest water shader. (Crest Water Shader Download)