Megabite #17: Procedural Generation (part 1)

I had a big plan for this week's Megabite, but my demonstration wasn't quite ready in time.  Fortunately though, it was on this very same topic, and it gives us all something else to look forward to next friday.

In this article, I'm going to answer the question that I've gotten a few times regarding Stellar: "How did you create the blocks"? The blocks in question are the ones within the "Skirmish Mode", seen here:


Well, the blocks themselves were easy to create, but it's the tunneling system and colorization script that really started bringing things together a bit more.  It wasn't long ago that the colors were random and chaotic.

The script works at first in a similar manner to a roguelike - first you fill all the spaces, then you actually cut away the areas you want open.  In doing, you'll save yourself a massive headache.  So with a script like the one below, what we're doing is basically making a giant grid of thousands of tiny blocks:

for (var i =0; i < cuber; i++) {
 if (i > 0) {
 Status = "Generating " + (i).ToString() + " of " + cuber.ToString();
 }
 if (i == (cuber - 1)) {
 Status = "Manipulating Terrain Elements";
 }
 yield;
 transform.position.x = transform.position.x + cubeSize;
 for (var i2=0; i2 < cuber; i2++) {
 var eachCube = Instantiate (genCube, transform.position, Quaternion.identity);
 eachCube.renderer.material.color = Color(0.51,0.36,0.13,1);
 transform.position.z = transform.position.z + cubeSize;
 }
 transform.position.z = minZ;
 }

A few important things to note here:

  • I used "yield;" in the script so that the GUI could update and allow a user to not think that the game has frozen.  Generating a massive world can take a lot of time.
  • If you're going to try this, make sure your camera is not looking at the generation in progress.  Cull away the rendering until after all is finished and you'll save yourself much needed processing power.
  • I've rendered each cube as my standard "dirt" color.  The idea is to add in "veins" of slightly different colors to give the cubes a more natural look.
  • The cubes themselves are simply unscripted primitive cubes with a standard grey appearance by default.  They are very simple, and only contain a collider component.

Now, we want to set up a starting location for our tunneling system.  What we're doing here is moving a gameObject around the cubes and shooting out a physics ray to select cubes that lay in front of it.  A ray is a single line, so we're also going to add two additional lines to cut out a larger path.  In addition, we're going to do this in short bursts of 100 meters and have the tunnel weave a bit randomly.  When the script detects cubes, it destroys them and we have a straight line of demolition.  The code for the tunnel detector looks something like this:

function KillFwd() {
 var hits = (Physics.RaycastAll (transform.position, transform.forward, 100));
   for (hit in hits) {
    if (hit.transform.tag == "genBlock"){
    Destroy(hit.transform.gameObject);
    }
   }
}

The code that executes the above script is:

function KillVein(startHere : Vector3) {
 Status = "Creating Routes & Generating Frustration";
 yield;
 for (x=0; x < 4; x++) {
 transform.position = startHere;
 transform.rotation = Quaternion.identity;
 transform.Rotate(Vector3.up * (90 * (x+1)));
 for (i=0; i < 100; i++) {
 KillFwd();
 transform.Translate(Vector3.right * 10);
 KillFwd();
 transform.Translate(Vector3.left * 20);
 KillFwd();
 transform.Translate(Vector3.right * 10);
 var move = Random.Range(0,90);
 var rot = Random.Range(-60,60);
 transform.Translate(Vector3.forward * move);
 transform.Rotate(Vector3.up * rot);
 }
 }
}

We do this in a few places, and what we end up with is the leftover "islands" that create the world.  When I lay down the enemy spawn points and bases, I have some additional code that clears the area first, but that's pretty much the basics.

Colorization is done in a very similar way - we randomize more lines and instead of destroying them, this time we change the way they are displayed:

function StabColor(theColor : Color) {
 var hits = (Physics.RaycastAll (transform.position, transform.forward, 100));
 for (hit in hits) {
   if (hit.transform.tag == "genBlock"){
   hit.transform.renderer.material.color = theColor;
   }
 }
}

As you can see, it's a very similar script.  The script that runs this function is actually a bit more complex than the tunneling though:

function AddColor() {
 var colorHere : Vector3;
 for (var i = 0; i < 3; i++) {
 colorHere.x = Random.Range(minX, maxX);
 colorHere.y = 0;
 colorHere.z = Random.Range(minZ, maxZ);
 Colorize(colorHere, Color(0.66,0.34,0,1));
 }
 for (i = 0; i < 3; i++) {
 colorHere.x = Random.Range(minX, maxX);
 colorHere.y = 0;
 colorHere.z = Random.Range(minZ, maxZ);
 Colorize(colorHere, Color(0.60,0.40,0,1));
 }
 for (i= 0; i < 15; i++) {
 colorHere.x = Random.Range(minX, maxX);
 colorHere.y = 0;
 colorHere.z = Random.Range(minZ, maxZ);
 ColorBomb(colorHere, Color(0.71,0.29,0,1));
 }
}
function ColorBomb(startHere : Vector3, theColor : Color) {
 var range : int = Random.Range(15, 45);
 var blocks : Collider[] = Physics.OverlapSphere(startHere, 100);
 for (each in blocks) {
 if (each.transform.tag == "genBlock") {
 each.transform.renderer.material.color = theColor;
 }
 }
}
function Colorize(startHere : Vector3, theColor : Color) {
 //print(startHere + " " + theColor);
 Status = "Creating Routes & Generating Frustration";
 yield;
 for (x=0; x < 4; x++) {
 transform.position = startHere;
 transform.rotation = Quaternion.identity;
 transform.Rotate(Vector3.up * (90 * (x+1)));
 for (i=0; i < 100; i++) {
 StabColor(theColor);
 transform.Translate(Vector3.right * 10);
 StabColor(theColor);
 transform.Translate(Vector3.left * 20);
 //StabColor(theColor);
 transform.Translate(Vector3.right * 10);
 var move = Random.Range(0,90);
 var rot = Random.Range(-60,60);
 transform.Translate(Vector3.forward * move);
 transform.Rotate(Vector3.up * rot);
 }
 }
}

The "ColorBomb" function has another really nifty tool - Physics.OverlapSphere.  With this handy unity function, we're able to grab a "circle" of blocks and colorize them.  With this, I was able to add a bit more of a blotchy and natural look in addition to the veins of color.

I'm working on a separate project to show some of the integrated features of the Unity Terrain generator - hopefully it will be ready for next week's Megabite.  Until then, if you have any questions about what you see here, feel free to drop a comment below!