As a single developer without a team of artists to create hand craft level content for me, I am considering creating my RPG’s world from Math. In this tutorial, I will create a means of visualizing such a world to show just how flexible and easy to use it can be. We will be making heavy use of Perlin noise, but don’t worry, this post will actually be light on math.
It is actually quite easy to create infinite worlds with a great degree of variation, providing a nice boost to exploration and replay ability. On the other hand, if I go with something too complex I may need to move away from the text based interface. Decisions, decisions!
Create the Scene
- Begin by creating a new empty scene.
- Add a Quad. From the menu bar choose “GameObject->3D Object->Quad”
- Create a new material. From the project tab, choose “Create->Material”
- With the material selected, use the inspector to set the material’s shader to “Unlit/Texture”, but leave the texture unassigned.
- Assign the newly created material to the Mesh Renderer of the Quad.
- Select the “Main Camera” object in the “Hierarchy” panel.
- In the Inspector, set the camera component’s “Projection” type to “Orthographic” and the “Size” to 0.5.
- Move the camera to position: (0, 0, -10).
At this point, the Game window should show the Quad taking up the full height of the screen. There may be bars on the side if you are using a non-square aspect ratio, but it doesn’t matter, we just want a convenient canvas to render on and show us the result.
Rendering to a Texture
Create a new script called “PerlinVisualizer.cs” and attach it to the Quad in the scene. We may want to change the resolution of the texture, so we will create a public Vector2 for the texture size. I created a small default size of 32×32 because I am imagining each pixel as a “room” or at least “tile” of a world. We will also need properties to hold a Texture2D which we will create and draw on, and an array of Color so that we can paint the texture in a single pass.
public Vector2 textureSize = new Vector2(32, 32); Texture2D texture; Color[] pix;
The texture itself will be created and assigned to our material in OnEnable. At the same time we will create the placeholder Color array for our pixel information. The texture format is important, not all of them support painting. Setting the “filterMode” to “Point” causes the pixels to display crisply. If you don’t set it, the renderer will try to blend everything together. Since we create the texture we are responsible for destroying it, do that in OnDisable.
void OnEnable () { texture = new Texture2D((int)textureSize.x, (int)textureSize.y, TextureFormat.ARGB32, false); texture.filterMode = FilterMode.Point; renderer.material.mainTexture = texture; pix = new Color[texture.width * texture.height]; } void OnDisable () { Destroy(texture); texture = null; }
Now we can do a sample render just to make sure everything works. In the Start method, we will trigger a test render that fills every pixel on our canvas with a randomly generated color. Note that I assign the color to the pix array, then assign the array to the texture. You must call “Apply” on the texture before it uploads to the graphics card and therefore appears on screen.
void Start () { Draw(); } public void Draw () { for (int y = 0; y < texture.height; ++y) { for (int x = 0; x < texture.width; ++x) { int index = y * texture.width + x; pix[index] = new Color( UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value ); } } texture.SetPixels(pix); texture.Apply(); }
Save the script and run the scene. If everything is configured correctly you should see your Quad appear as if a rainbow vomited all over it.
Draw Perlin Noise
Next, we want to draw something a little more intentional. Create a new script called “Mapper.cs”. It will be the base class of the script drawing perlin noise. We might later want to implement different patterns or means of drawing initial values. It indicates that we will take an x and y value (driven by pixels of the image) and return a float. We will use that float as a multiplier on a color value to paint the pixel.
public abstract class Mapper : MonoBehaviour { public abstract float Map (int x, int y); }
Here is the concrete implementation of the Mapper for Perlin Noise.
public class PerlinMapper : Mapper { /// <summary> /// The offset is used to determine a base shifting of the perlin pattern /// and will be added to the sample position /// </summary> public Vector2 offset = Vector2.zero; /// <summary> /// The scale is used to shrink or grow the overal size of the pattern /// </summary> public Vector2 scale = new Vector2(0.1f, 0.1f); /// <summary> /// Takes an x and y position and returns a value from 0-1 with respect /// to its offset and scale properties /// </summary> public override float Map (int x, int y) { return Mathf.PerlinNoise((x + offset.x) * scale.x, (y + offset.y) * scale.y); } }
So you can see the results of this bit of code, let’s plug it in to draw the perlin noise to our texture instead of the rainbow garbage we had earlier. The bit of code we are doing right now is throw-away and is just to let you see the purpose of each piece before it is fully assembled into something that might otherwise be too large to understand.
Add the Perlin Mapper script to our canvas object (called “Quad” in the scene unless you renamed it). Add another temporary property to the PerlinVisualizer script:
PerlinMapper mapper;
At the end of the OnEnable method get the component and assign it to our property.
mapper = gameObject.GetComponent<PerlinMapper>();
One last step. Modify the line that assigned a random color to each pixel to the following:
// Change this line... // pix[index] = new Color( UnityEngine.Random.value, UnityEngine.Random.value, UnityEngine.Random.value ); // To this... pix[index] = Color.white * mapper.Map(x, y);
Run the scene again, and now you should see a pixelated version of something that looks kind of like Photoshop’s render clouds filter.
Experiment with the pattern
The values you pass along to Perlin Noise can be pretty sensitive, so rather than having to change a value and run the scene repeatedly, let’s add an editor script to allow us to manually trigger a render, or to simply make it render every frame. Before that, we will need to expose some new functionality.
Add another property, which indicates whether or not our script is rendering the canvas every frame or not:
public bool isPlaying { get; private set; }
These simple methods handle starting, stopping and performing the draw loop:
public void Play () { if (isPlaying == false) { isPlaying = true; StartCoroutine("DrawLoop"); } } public void Stop () { if (isPlaying) { StopCoroutine("DrawLoop"); isPlaying = false; } } IEnumerator DrawLoop () { while (true) { yield return null; Draw (); } }
Now we can add a new editor script. Note that it needs to be placed in a folder called “Editor” in order to work properly. I named the script “PerlinVisualizerInspector.cs”. Editor scripts must include the “UnityEditor” namespace and should inherit from the “Editor” class. Marking the class as “CustomEditor” is what allows it to target our PerlinVisualizer component and insert new controls into the inspector.
using UnityEngine; using UnityEditor; [CustomEditor (typeof(PerlinVisualizer))] public class PerlinVisualizerInspector : Editor { protected PerlinVisualizer perlinVisualizer { get { return (PerlinVisualizer)target; } } public override void OnInspectorGUI () { DrawDefaultInspector (); if (GUILayout.Button("Draw")) perlinVisualizer.Draw(); if (perlinVisualizer.isPlaying && GUILayout.Button("Stop")) perlinVisualizer.Stop(); if (!perlinVisualizer.isPlaying && GUILayout.Button("Play")) perlinVisualizer.Play(); } }
Now run the scene. Modify some values on the Perlin Mapper, such as the offset position. Click the “Draw” button in the Perlin Visualizers inspector and the texture should update. If our calculations get too complex, or we display it on too large of a texture, we may want to leave play mode off and trigger renders manually with the Draw button like this.
Click “Play” and now drag your mouse across the Offset and you should see the texture scroll across the canvas. Since our texture is small and our system simple, we can get away with leaving it in play mode for now.
Make the pattern look like a Level
As it is, the Perlin noise looks like it could be nice for a textured ground or perhaps a nice height map for a heavily subdivided ground mesh. It can be tweaked for many other purposes, such as drawing out a walkable level. I am imagining something where we only see white or black pixels, where the white pixels represent traversable world and the black pixels are non-traversable. They could become mountain ranges or oceans surrounding an island. It doesn’t really matter, but you get the idea for now.
Create an abstract base class called Node. It will be used to modify the parameters we get from instances of the Mapper class in some way.
public abstract class Node : MonoBehaviour { public abstract float Calculate (float input); }
The first concrete implementation of the Node class I will call the “BarNode”. It will take a value and if it passes a “bar” value, will return 1, otherwise it will return 0. This will create the white or black extremes as I mentioned earlier.
public class BarNode : Node { /// <summary> /// Determines the value at which values are clamped /// to either 0 or 1 /// </summary> public float value = 0.5f; /// <summary> /// Values less than or equal to the input will be /// output as zero, one otherwise /// </summary> public override float Calculate (float input) { return (input <= value) ? 0f : 1f; } }
To see the effect of the Node on a Mapper, add this component to our Canvas object (the same one with the mapper and visualizer). Add another temporary property for it to our PerlinVisualizer script:
Node node;
Connect the Property at the end of the OnEnable method:
node = gameObject.GetComponent<BarNode>();
And run the mapper’s value through the node in the “Draw” method:
pix[index] = Color.white * node.Calculate( mapper.Map(x, y) );
Run the scene now and you can see that there are very defined regions of walkable vs non-walkable area. We could definitely create some sort of interesting maze like level out of this.
Enable Play mode on the Visualizer script and modify the value of the BarNode script. Lower values create pockets of black, which I might imagine as random boulders scattered over an open field, or if used as a mask, pockets of areas that will become forests or swamps etc. Higher values create pockets of white, which might be islands in an ocean. Use your imagination here.
Create a new script called “BandNode”. It will also clamp values to either 0 or 1, but will do so based on values within a value range which will be defined by a Vector2:
public class BandNode : Node { /// <summary> /// The range of values that return 1; /// </summary> public Vector2 range = new Vector2(0.4f, 0.6f); /// <summary> /// Input values >= range.x and values <= range.y output 1 /// </summary> public override float Calculate (float input) { return (input >= range.x && input <= range.y) ? 1f : 0f; } }
Add this script to the Canvas, and in the OnEnable method, get a reference to the BandNode instead of the BarNode.
// Change this... // node = gameObject.GetComponent<BarNode>(); // To this... node = gameObject.GetComponent<BandNode>();
Run the scene and you will have an even more maze like system of passageways than we saw before. The passages are narrow and winding.
Layer the Complexity
Now I want to be able to stack mappers and nodes and combine their functions to create even more interesting patterns and possibilities. Create a new script called “RenderLayer.cs”:
public class RenderLayer : MonoBehaviour { public Color color = Color.white; public Mapper mapper; public Node[] filters; public Color Render (int x, int y) { float value = mapper.Map(x, y); if (filters != null) { for (int i = 0; i < filters.Length; ++i) { value = filters[i].Calculate(value); } } return color * value; } }
Now I will remove all the temporary code I added to the Visualizer script and implement its final version. Here is the full class for clarity sake:
public class PerlinVisualizer : MonoBehaviour { public RenderLayer[] layers; public Vector2 textureSize = new Vector2(32, 32); public bool isPlaying { get; private set; } Texture2D texture; Color[] pix; void OnEnable () { texture = new Texture2D((int)textureSize.x, (int)textureSize.y, TextureFormat.ARGB32, false); texture.filterMode = FilterMode.Point; renderer.material.mainTexture = texture; pix = new Color[texture.width * texture.height]; } void OnDisable () { Destroy(texture); texture = null; } void Start () { Draw (); } public void Play () { if (isPlaying == false) { isPlaying = true; StartCoroutine("DrawLoop"); } } public void Stop () { if (isPlaying) { StopCoroutine("DrawLoop"); isPlaying = false; } } public void Draw () { for (int y = 0; y < texture.height; ++y) { for (int x = 0; x < texture.width; ++x) { Color c = Color.black; for (int i = 0; i < layers.Length; ++i) { c += layers[i].Render(x, y); } pix[ y * texture.width + x ] = c; } } texture.SetPixels(pix); texture.Apply(); } IEnumerator DrawLoop () { while (true) { yield return null; Draw (); } } }
I removed the mapper and node components from the canvas. Then I created a new GameObject and parented it to the canvas. I named the object “Layer 0”. Add the RenderLayer script to this new object. Drag the object onto the Visualizer script’s “Layers” property so that it has an array size of 1 and the element is auto assigned.
Add another GameObject called “Perlin Mapper” and attach the “PerlinMapper” script to it. Parent this object to the “Layer 0” GameObject (note that none of the parenting has any effect on anything, it is simply for organization sake). Drag the “Perlin Mapper” object into the “Mapper” field of the Render Layer’s inspector. At this point you could play the scene and see the soft “Cloud” like filter from earlier.
Add another GameObject called “Bar Node” and attach the “BarNode” script to it. Parent this object to the “Layer 0” GameObject and drag the object onto the Layer’s “Filters” property so that it has an array size of 1 and the element is auto assigned. I will set the value of the Bar Node to 0.6 so that it creates little islands or pockets of white.
Create another layer like we did before. I made mine a separate hierarchy so that each layer is a sibling child of the Quad canvas object. Like before, the mapper will be a Perlin Mapper. To make things more interesting I used a different value for the scale of the noise (0.15, 0.15). Also we will add some variation by using a BandNode for the filter. Don’t forget to add the new layer to the Visualizer script’s Layers array or you won’t see it added to the picture. Run the scene and you will see a nice mixture of winding passages and larger more open areas.
Hopefully by now you see the potential of this system. Feel free to experiment with what I have already provided and try changing colors or adding more nodes into the mix:
The invert node will take white pixels and make them black and vice-versa.
public class InvertNode : Node { public override float Calculate (float input) { return 1f - input; } }
The multiply node will reduce the effect of a layer much like an alpha channel. values of 0 turn a layer off, and values of 1 are fully visible and unmodified.
public class MultiplyNode : Node { public float value; public override float Calculate (float input) { return input * value; } }
Enjoy!