Saturday, August 13, 2016

Creating Dynamic 2D Water Effects in Unity_part1

In this tutorial, we're going to simulate a dynamic 2D body of water using simple physics. We will use a mixture of a line renderer, mesh renderers, triggers and particles to create our effect. The final result comes complete with waves and splashes, ready to add to your next game. A Unity (Unity3D) demo source is included, but you should be able to implement something similar using the same principles in any game engine.

End Result

Here's what we're going to end up with. You'll need the Unity browser plugin to try it out.


Setting Up Our Water Manager

In his tutorial, Michael Hoffman demonstrated how we can model the surface of water with a row of springs.

We're going to render the top of our water using one of Unity's line renderers, and use so many nodes that it appears as a continuous wave.



We'll have to keep track of the positions, velocities and accelerations of every node, though. To do that, we're going to use arrays. So at the top of our class we'll add these variables:
  1. float[] xpositions;
  2. float[] ypositions;
  3. float[] velocities;
  4. float[] accelerations;
  5. LineRenderer Body;
The LineRenderer will store all our nodes and outline our body of water. We still need the water itself, though; we'll create this with Meshes. We're going to need objects to hold these meshes too.
  1. GameObject[] meshobjects;
  2. Mesh[] meshes;
We're also going to need colliders so that things can interact with our water:
  1. GameObject[] colliders;
And we'll store all our constants as well:
  1. const float springconstant = 0.02f;
  2. const float damping = 0.04f;
  3. const float spread = 0.05f;
  4. const float z = -1f;
These constants are the same kind as Michael discussed, with the exception of z—this is our z-offset for our water. We're going to use -1 for this so that it gets displayed in front of our objects. (You might want to change this depending on what you want to appear in front and behind of it; you're going to have to use the z-coordinate to determine where sprites sit relative to it.)

Next, we're going to hold onto some values:
  1. float baseheight;
  2. float left;
  3. float bottom;
These are just the dimensions of the water.

We're going to need some public variables we can set in the editor, too. First, the particle system we're going to use for our splashes:
  1. public GameObject splash:
Next, the material we'll use for our line renderer (in case you want to reuse the script for acid, lava, chemicals, or anything else):
  1. public Material mat:
Plus, the kind of mesh we're going to use for the main body of water:
  1. public GameObject watermesh:
These are all going to be based on prefabs, which are all included in the source files.

We want a game object that can hold all of this data, act as a manager, and spawn our body of water ingame to specification. To do that, we'll write a function called SpawnWater().

This function will take inputs of the left side, the width, the top, and the bottom of the body of water.
  1. public void SpawnWater(float Left, float Width, float Top, float Bottom)
  2. {
(Though this seems inconsistent, it acts in the interest of quick level design when building from left to right).

Creating the Nodes

Now we're going to find out how many nodes we need:
  1. int edgecount = Mathf.RoundToInt(Width) * 5;
  2. int nodecount = edgecount + 1;
We're going to use five per unit width, to give us smooth motion that isn't too demanding. (You can vary this to balance efficiency against smoothness.) This gives us all our lines, then we need the + 1 for the extra node on the end.

The first thing we're going to do is render our body of water with the LineRenderer component:
  1. Body = gameObject.AddComponent<LineRenderer>();
  2. Body.material = mat;
  3. Body.material.renderQueue = 1000;
  4. Body.SetVertexCount(nodecount);
  5. Body.SetWidth(0.1f, 0.1f);
What we've also done here is select our material, and set it to render above the water by choosing its position in the render queue. We've set the correct number of nodes, and set the width of the line to 0.1.

You can vary this depending on how thick you want your line. You may have noticed that SetWidth() takes two parameters; these are the width at the start and the end of the line. We want that width to be constant.

Now that we've made our nodes, we'll initialise all our top variables:
  1. xpositions = new float[nodecount];
  2. ypositions = new float[nodecount];
  3. velocities = new float[nodecount];
  4. accelerations = new float[nodecount];
  5.  
  6. meshobjects = new GameObject[edgecount];
  7. meshes = new Mesh[edgecount];
  8. colliders = new GameObject[edgecount];
  9.  
  10. baseheight = Top;
  11. bottom = Bottom;
  12. left = Left;
So now we have all our arrays, and we're holding on to our data.

Now to actually set the values of our arrays. We'll start with the nodes:
  1. for (int i = 0; i < nodecount; i++)
  2. {
  3.     ypositions[i] = Top;
  4.     xpositions[i] = Left + Width * i / edgecount;
  5.     accelerations[i] = 0;
  6.     velocities[i] = 0;
  7.     Body.SetPosition(i, new Vector3(xpositions[i], ypositions[i], z));
  8. }
Here, we set all the y-positions to be at the top of the water, and then incrementally add all the nodes side by side. Our velocities and accelerations are zero initially, as the water is still.

We finish the loop by setting each node in our LineRenderer (Body) to their correct position.

Creating the Meshes

Here's where it gets tricky.

We have our line, but we don't have the water itself. And the way we can make this is using Meshes. We'll start off by creating these:
  1. for (int i = 0; i < edgecount; i++)
  2. {
  3.     meshes[i] = new Mesh();
Now, Meshes store a bunch of variables. The first variable is pretty simple: it contains all the vertices (or corners).

The diagram shows what we want our mesh segments to look like. For the first segment, the vertices are highlighted. We want four in total.
  1. Vector3[] Vertices = new Vector3[4];
  2. Vertices[0] = new Vector3(xpositions[i], ypositions[i], z);
  3. Vertices[1] = new Vector3(xpositions[i + 1], ypositions[i + 1], z);
  4. Vertices[2] = new Vector3(xpositions[i], bottom, z);
  5. Vertices[3] = new Vector3(xpositions[i+1], bottom, z);
Now, as you can see here, vertex 0 is the top-left, 1 is the top-right,2 is the bottom-left, and is the top-right. We'll need to remember that for later.

The second property that meshes need is UVs. Meshes have textures, and the UVs choose which part of the textures we want to grab. In this case, we just want the top-left, top-right, bottom-left, and bottom-right corners of our texture.
  1. Vector2[] UVs = new Vector2[4];
  2. UVs[0] = new Vector2(0, 1);
  3. UVs[1] = new Vector2(1, 1);
  4. UVs[2] = new Vector2(0, 0);
  5. UVs[3] = new Vector2(1, 0);
Now we need those numbers from before again. Meshes are made up of triangles, and we know that any quadrilateral can be made of two triangles, so now we need to tell the mesh how it should draw those triangles.


Look at the corners with the node order labelled. Triangle A connects nodes 01 and 3; Triangle B connects nodes 32 and 0. Therefore, we want to make an array that contains six integers, reflecting exactly that:
  1. int[] tris = new int[6] { 0, 1, 3, 3, 2, 0 };
This creates our quadrilateral. Now we set the mesh values.
  1. meshes[i].vertices = Vertices;
  2. meshes[i].uv = UVs;
  3. meshes[i].triangles = tris;
Now, we have our meshes, but we don't have Game Objects to render them in the scene. So we're going to create them from our watermesh prefab which contains a Mesh Renderer and Mesh Filter.
  1. meshobjects[i] = Instantiate(watermesh,Vector3.zero,Quaternion.identity) as GameObject;
  2. meshobjects[i].GetComponent<MeshFilter>().mesh = meshes[i];
  3. meshobjects[i].transform.parent = transform;
We set the mesh, and we set it to be the child of the water manager, to tidy things up.
Written by Alex Rose

If you found this post interesting, follow and support us.
Suggest for you:

Make VR Games in Unity with C# - Cardboard, Gear VR, Oculus

Learn to Code by Making Games - The Complete Unity Developer

Make a Multiplayer Shooter in Unity

Unity 5 Host Your Game Server Online like a PRO

Start Learning Unity3d by Making 5 Games from Scratch

No comments:

Post a Comment