Procedural Generation
Tuesday, August 25, 2015
3:10 PM
In recent years procedural generation has become something of a trend for games, particularly those on the indie scene. It makes sense--procedural generation allows games to be different for every play through, meaning that smaller scale or simpler games can have significantly more replay value than if everything was hard coded in. Procedural generation is definitely nothing new (it is, after all, exactly what's going on with all of those games with random item pickups), but more eyes have been turning to the technique lately because of its use in level design. Getting a different weapon than your friend is all well and good, but experiencing a completely different environment from them? That's certainly something to take notice of.
Procedural generation can be complex. Extremely so. Mathematician thesis level complex. But, the good news is that it can also be really, really easy. At its base level, it's an awful lot like pulling a card from a hat, and slapping it down at a random position on a table. There are two main components that go into base level procedural generation: A list of stuff, and a place to put it. Everything else expands from that.
To illustrate the point, we'll be going over three types of procedural generation, each one building on the next.
Making an item generator
Creating an Array
Before we start making things appear in our level, we first need to create that list of stuff that our generator can choose from. When using JavaScript in Unity, this is accomplished using an Array. An array is a special variable that allows you to store multiple items in it, rather than just the standard single item. Unity uses arrays a lot, so you've probably seen one without realizing what it is. In the inspector, an array looks like this:
You can see the title of the variable, but rather than a single slot where you can plug in a game object or type in a value, it has a drop down arrow. When you roll it down, you'll see a space to type in Size. This value is to define how many objects you want to put in the array.
From the scripting side, it's pretty easy to set up.
Create a JavaScript and call it itemGenerator.
We declare arrays almost exactly like we do any other type of variable. The important bit that sets it apart happens after we declare the variable type.
var items : GameObject[];
The open and closed brackets [] let Unity know that the variable is an array. Declare the item array and then save your script and go back into Unity.
Create a new game object and call it itemManager. Attach the itemGenerator script to it, and you should see your freshly created array!
Before we go back into the script and actually do something with that array, we need to do some set up in Unity. First, let's make our itemManager more visible in scene. Click on the colored cube next to the name of the object to see a selection of labels we can assign. Choose one of the pill shaped labels, and your game empty should become immediately visible in scene.
Next we need to prepare items to put in the array. For the sake of ease, we'll create a series of prefabs made with Unity primitives. Create a cube, sphere, capsule, and cylinder. We'll have our manager drop an item to the ground for us, so assign each item a rigidbody and then prefab them.
Load your prefabs into the item array.
We're set on the Unity side, so let's go back into MonoDevelop to work on our script.
For the sake of simplicity, let's use an OnTriggerEnter function to have our generator drop a random item from our array when our player enters it.
The key to "making stuff appear" in Unity is utilizing Instantiate. When you call Instantiate, you'll need to provide three pieces of information: The object to instantiate, where to spawn it, and at what kind of rotation. For a regular item, this line would look something like this:
Instantiate (exampleVariable, transform.position, transform.rotation);
(transform.position and transform.rotation tells Unity to spawn the exampleVariable wherever the object that has the script on it exists in the scene)
Now, because we're calling a random item from an array, our script will be a little bit different. We need to tell Unity that we want it to pick a random item out of the array and then spawn it like anything else. To do that, in place of the variable alone, we'll have to type:
exampleVariable[Random.Range(0, exampleVariable.Length)]
Random.Range tells Unity to pick a number at random between two numbers that we declare. Because we might not know how long our array will be (we can always add or subtract items), we tell Unity to pick a number between 0, and however long the array is. We achieve this with .Length, which counts the number of items in the array for us.
All said and done, our script will look something like this:
var items : GameObject[];
function OnTriggerEnter ( other : Collider) {
Instantiate (items[Random.Range(0,items.Length)], transform.position, transform.rotation);
}
Save your script and return to Unity. Before we can actually test this out, we need our generator to have a trigger! Add a box collider to your itemManager, and offset it so it's in a place where our player can walk through. Don't forget to set it as isTrigger!
Add a character controller to your level and have your player walk through the trigger (you might need to place an object in the scene as a guide to where the trigger is, or just look in the scene view!). Once the player touches the trigger, a random item from the array will drop into the scene from the manager's origin. Walk in and out of the trigger to add more items. Tetris anyone?
The scenario for this sort of setup could be customized almost infinitely. You could place the item manager in a treasure chest; the trigger could be changed to a float which can be tied to collecting objects, so a random spawn will only occur after the player has picked up enough things; the spawner could move so you could rain random items down from the sky from all over the level; the items spawned could be ai enemies; the list goes on…
Generating Scenery
Single item spawning is all well and good, but what about procedurally generating levels? Obviously, you'll need to spawn more than one item at a time to make this happen.
The funny thing about this is that there's really only one major change between single item generation and whole scene generation: The for loop. A for loop allows you to run a specific chunk of code over and over again until a specific condition is met (for example, if a bool is set to true), or for a certain amount of times. It looks something like this:
for (var i : int = 0; i < ourNumber; i++ )
It might look confusing at first, but really, it's pretty straight forward!
The first part of the loop (var I : int = 0) is declaring a special variable that will be used exclusively in the loop as a type of counter. Just like any other variable, you can name it anything that you like, but it's easiest to just let it be something like 'i'. We have declared this variable as 0, which is where it will start when our code begins to run.
The next part is kind of like an if statement. The loop is checking our i variable to see what its current value is against ourNumber, which is a custom variable we're using this instance instead of a number (you can hard code a number in--for example, 20--but it limits how customizable your code is. By adding in an integer variable, we can customize how many times our loop will run directly from the inspector!). So long as ourNumber is bigger than the i variable, we move on to the next part of the loop.
The final part of the loop probably looks familiar. The ++ is the code for adding one, so the line reads as +1 to i.
So in plain English, for (var i : int = 0; i < ourNumber; i++ ) translates to; for however long our variable i is less than our custom ourNumber variable, add one to i and run whatever code is inside my brackets.
Let's get some scripting going for this. Create a new JavaScript and call it generateScene. Create an array variable and call it scenery. This part is exactly the same setup as the item generator above! Next, let's create a variable so we can customize how many items get generated.
var scenery : GameObject[];
var sceneNumber : int;
For the sake of this example, let's have the scenery auto generate as soon as the game begins. To achieve this, we'll place our for loop in the Start function.
function Start () {
for (var i : int = 0; i < sceneNumber; i ++) {
var position: Vector3 = Vector3(Random.Range(-10, 10), 0, Random.Range(-10, 10));
Instantiate(scenery[Random.Range(0,scenery.Length)], position, Quaternion.identity);
}
}
Note that rather than having the objects spawn at the origin point of the object that this script will be applied to, we're using a Vector3 declared as a variable. The variable is for the sake of ease--having to declare a vector3 in the instantiate would be insanely cluttered. You're probably already familiar with them, but Vector3s are simply an X,Y,Z position in space. Just like we're having Unity randomly pick an object out of our array with Random.Range, so to are we having Unity pick a random position for our object's spawn. When declaring a Vector3, the three values are in the order X, Y, and Z, so the line above is getting a random number between -10 and 10 for our X, our Y is at 0, and our Z will also be a random number between -10 and 10. It'd be nice to be able to customize those values in the inspector so we can generate scenery for different sized levels, so let's add some variables in for that. Here's the revised code:
var scenery : GameObject[];
var sceneNumber : int;
var minRange : int;
var maxRange : int;
var height : int;
function Start () {
for (var i : int = 0; i < sceneNumber; i++) {
var position: Vector3 = Vector3(Random.Range(minRange, maxRange), height, Random.Range(minRange, maxRange));
Instantiate(scenery[Random.Range(0,scenery.Length)], position, Quaternion.identity);
}
}
As a final side note, Quaternion.identity simply tells Unity not to add any special rotations to the objects!
Save your script and go back into Unity. Just as we did with our single item generator, let's create a game empty and call it sceneGenerator. Give it a label so it's visible in scene and apply the new script to it. Fill your array with the poly primitives, and set your limits for where the objects can spawn.
I set the height to 2 as our prefabs have rigidbodies on them, and I didn't want them to accidentally generate halfway through our ground and go flying off.
Press play and check out your scene…
Obviously with poly primitives it's not amazing, but imagine those as bushes, trees, and rocks. Suddenly you have yourself a forest scene, and all you had to do was hit play.
Procedurally Generating Levels with Tilesets
While the procedural generation setup we used above goes a long way toward helping us create naturalistic environments, it isn't without problems. Generating prefabs at random points means that it's possible to spawn items in other items, or create an environment that accidentally traps the player. While the random method is wonderful for creating chaos, it doesn’t do much in the way of generating order--for example, a maze.
The great secret of procedural generation in games is that in most instances, it isn't completely relied upon. Careful level designers still utilize pre-made parts, or hubs, from which procedural elements radiate. These hubs could be boss rooms, or player starts, or areas where quests are given. Procedural generation often works best when it's approached in the same way as modular storytelling: Make sure that your main themes are present at the right time, and let the rest build dynamically as it will.
Tilesets require a little bit more planning than the other types of procedural generation we discussed, and requires a little bit more complexity from a scripting point of view, though not by much. First, let's discuss the prefabs.
Tiles are essentially chunks of your level. How big or small or how complex is entirely up to you. It can be an entire chunk of a cave with multiple pathways, a room in a house, or even an arrangement of furniture. What's important is that when put together, these tiles form your level. Because you're leaving how these tiles are arranged up to the whim of the computer, you need to make sure that they work together in multiple different configurations. Again, how you make this work is entirely up to your level's needs. For the sake of this exercise, we'll work with tilesets that have openings on each side, so the player can freely navigate regardless of the order they're placed.
Here's a very simplistic set of "maze" tiles. Each piece has an exit on all four walls that are perfectly aligned. It wont make the most exciting maze without any dead ends, but perhaps for this game it's a matter of keeping away from an enemy or something. We're going to build a grid of spawn points, so it's very important that each of our tiles are exactly the same size to avoid overlapping or gaps.
As I just mentioned, the other part of the equation when it comes to working with tilesets is where the tiles will spawn. In this exercise, we'll be building a spawnpoint grid that spans the desired length and width of our level, but there are other methods as well. In order to keep things more dynamic and allow for dead ends, some designers prefer to place their spawn points directly in the prefabs themselves, while other designers run checks to see if a path is blocked. The great thing about tilesets is that you can make completely dynamic systems from either the tile design standpoint or from the scripting standpoint. Just choose the path that you're most comfortable with!
Let's go ahead and build the spawnpoint grid. We'll do this with game empties. Create an empty and name it spawnPoint, then give it a label. We also need to identify these points via tag, so create a new tag called spawn, and then add it to your spawnPoint. We want our tiles to spawn right up against each other, for this example, we need our spawnPoints 10 units away from each other in Z, and 12 units away from each other in X. Create a 5x5 grid of spawns.
Now that we have all of the elements in Unity, it's time to start thinking about the script.
If we were to apply the logic of the previous examples to this exercise, we'd realize that in order to spawn a tile at each point, we'd have to assign a script to each of the spawnPoint objects, and that would be a huge pain and could potentially slow down our game during the first frame, creating an odd popping effect. A more elegant solution would be to create a second array to store all of our spawn points, and then integrate that into a for loop!
Because we tagged all of our spawn points, we can have our script search for them and automatically add them to an array. The problem we'd be immediately facing however, is that the FindGameObjectWithTag feature is for just that…Finding game objects. We however want to find the spawnPoint's transform, which is not stored with the game object itself…Quite the conundrum. What we would normally have to do would be to first collect the game objects, then look through the array and grab each game object's transform, and finally add that information to a spawnPoint array. That's a lot of excess code. Fortunately, there's a shortcut!
We're going to import an extra set of rules that we can use to shorten the process. This will be in the form of importing System.Linq.
import System.Linq;
var tileSet : GameObject[];
var spawnPoints : Transform[];
function Awake () {
spawnPoints = GameObject.FindGameObjectsWithTag ("spawn").Select(function(go) {return go.transform;}).ToArray();
}
Importing is more commonly utilized in C#, but System.Linq works in JavaScript as well. It's basically an extra collection of preset rules and tools (like FindGameObjectsWithTag, for example) that we can utilize to make our coding faster. All that converting that we would normally have to do is condensed down into one line.
We do this all via the Awake function. The first part of the call is finding all of our game objects that are tagged as spawns, so our spawnPoints. Next, we select each of these items and find out what their transforms are, and finally, we record that information in our array.
Save your script and create a new game empty. Label it and name it tileManager. Apply the script. Push play, and you should see your array full of transforms!
While we're here, go ahead and load the tiles into the tile set array, then return to the script. Just like the previous examples, we'll instantiate in our start function. Rather than relying on a specific number in our for loop, we'll have Unity look at how many objects are in our spawnPoints array and use that number for how many times to loop. When we don't tell Unity to randomly pick an object from an array in a loop, it will start at the top and work its way down in order. Our final script:
import System.Linq;
var tileSet : GameObject[];
var spawnPoints : Transform[];
function Awake () {
spawnPoints = GameObject.FindGameObjectsWithTag ("spawn").Select(function(go) {return go.transform;}).ToArray();
}
function Start () {
for (var i : int = 0; i < spawnPoints.Length; i++) {
Instantiate(tileSet[Random.Range(0,tileSet.Length)],spawnPoints[i].position, transform.rotation);
}
}
Save, and return to Unity. Press play, and you should see your tiles generate at each spawn point!
You can already probably see a lot of potential in a system like this. Another path would be to randomize what spawn points get objects for more naturalistic environments. You can combine a grid system with the scenery system for a mix of chaos and orderly designs, and so on.
In Closing
Procedural generation, at its core, is just a list of objects, and places to put them. The nature of those lists are infinitely customizable, and the places to put them can be dynamic, or pre-set, or some combination of both. When building your own system, remember to start with a plan and implement each feature one at a time to avoid getting confused. The last example is pretty much the exact same as the first, with some extra bells and whistles built on top! Using procedural generation can add some really awesome variety to your games--now get out there and see what you can make!