UE4: Using HISMs to generate an Asteroid Field
A tutorial on procedural object placement in Unreal Engine 4.
Written By: Christian Sparks
Founder of Smallheart Studios. Working on Woodbound.
This tutorial was written for users with beginning-intermediate familiarity with Blueprints scripting in Unreal Engine 4, and was created on version 4.16.
My friend and I have been toying around with an idea for a small game set in space, and I wanted to build out a demo scene to play around in. I needed a crapload of asteroids to fill up the empty space around me, but I didn’t want to drag them all in and sloppily throw them around using the default transform tools, so I set out to make a blueprint that would place them for me. Here’s what I came up with:
The actor uses a boolean “button” to procedurally spawn in a definable number of simple rock meshes in the world, with definable maximum spawn distance from the actor position, as well as values for space between rocks, and min/max uniform scaling of each instance. There’s another boolean “button” to clear the existing instances.
I used a series of Hierarchical Instanced Static Mesh components to handle the cataloging of the meshes (6 in my case, just for mesh variety, I crapped out some procedural rocks from Blender, you can find details on that process here) and with some fairly straightforward checks, the actor works quite well.
So the first thing that I did was to set up the “button” functionality using an editable boolean variable, called “Clear Rocks.” It’s default value is false, and it’s used (as seen in the image above) to simply reconstruct the script without calling any of the HISM creation functions, effectively “resetting” the actor if there’s already been any instances created.
From there, I added all of my HISM components that I added and named into an array variable for easier reference further along in the script.
Next up, we do a check to see if we’re creating any meshes by creating another boolean, “button,” using an editable false-by-default boolean variable called, “Spawn Rocks.
If Spawn Rocks is true, it’ll perform the button functionality, and then go through and fire the custom Spawn Rocks function I built(detailed below) using a For Loop node to control how many times that function is called. Once that Loop is complete, a debug message will post, letting you know it’s finished.
Custom Spawn Rocks Function (Part 1)
This function goes through a few steps to add instances to the HISM components, effectively spawning them into the world. The first step is to randomly pick from the array of HISM actors to decide which mesh is going to appear in the world. (Each HISM only holds one static mesh type, so multiple HISM components were added for mesh variety.) I used a simple Random Integer in Range node, with the min set to 0 (minimum value in the HISM array) and 5 (the max value in my HISM array, yours may be different depending on how many different mesh shapes you’re wanting to spawn.) Then I created a non-editable variable for easier storage of the random integer value, just so I don’t get my wires crossed later on, and so that I could more easily separate out the portions of this function for this tutorial.
From there, we need to create two more functions. Let’s start with Random Transform (Editable).
Random Transform (Editable)
I kind of worked backwards on this one: I added a return node with a Transform variable-output pin added. Then I dragged back off of that Transform pin and added a Make Transform node. This way I could expose the location, rotation & scale of the mesh instances.
For the location, I used an editable float variable called Max Spawn Distance from Origin, using that base value as the min input for a Random Float in Range node, and it’s negative counterpart (derived by multiplying the float by -1) as the max input.* I used this same approach 3 times, once for each input in a Make Vector node, which fires that location info into the Location input pin of my make transform node.
*Author note: I realized as I was writing this tutorial that I built out that function backwards, even though it works perfectly this way. Regardless, I’ll go back and correct this mistake for clarity when I’m back at my development PC.
For the rotation, I just used a Random Rotator node plugged into the Rotation input pin of the make transform node, with Roll checked as True so that rotation is applied on all axes.
For the scale, I used a Vector*Float node, scaling a 1x1x1 flat scale by a random float in a range, the range min and max of which are determined by editable Min Rock Scale and Max Rock Scale float variables. This scales the rocks uniformly along all axes.
Once your Make Transform pin is built with all the info it needs, you can drag the Output pin into the T input pin on the Return node of this function, and then check to make sure in the function attributes that this particular function is checked as, “Pure,” and we can save our work and move on to writing the last custom function that we’ll need for this actor, called “Rock-Rock Distance.”
The first thing that we’re going to do in this function is add a Return Node, and then add T In and T Out Transform variable pins to the Input and Return nodes, respectively, of this function.
The T In pin will be fed the T value from our Random Transform (Editable) function that we wrote, so drag that value in and create a non-editable transform variable out of it, and name it something like, “Comparative Transform.” The reason that we’re storing this into a temporary variable instead of just calling a random transform within this function, is because we need to create a random transform once, but then examine the values and weigh them against the values of the previously spawned instance, without generating a new transform every time we look for the information, as would happen if we nested the random transform function within this function. Doing this, within our order of operations in the Spawn Rocks function, will maintain the informational integrity of the transform info across our functions.
The next event delegation in this script is a branch node, but there’s a lot to define in what determines the yes/no value of the branch, so I’ll start from the left and work towards the right. The first thing that we want to do is get our HISM Array variable that we populated in the construction script. We need to use a Get (A Copy) node to get the previously used HISM component in our loop to find its most recent entry’s transform info. So in your Get (A Copy) node, in the index input, use a non-editable integer variable, and call it something like, “Last Used HISM.” This variable hasn’t actually been set yet, and will default to 0, which is fine, as the first spawn instance will use that 0 to look for a transform to compare against, see that there’s nothing there, and then go ahead with spawning, as it doesn’t have to worry about spawning too close to another instance.
In the case of instances spawning after the first actor, we need to get the transform of the last spawn instance to compare the location against, so we’ll drag off of the Output pin of our Get (A Copy) node, and create a Get Instance Transform node. Make sure that World Space is checked, as we’re comparing global locations, not relative. Since we need to determine which instance we’re comparing transform information to, we need to also create a Get Instance Count node from our Get (A Copy) node, which will return the max value (or most recent instance) of the given HISM component that was last active.
Now that we have which instance we’re going to weigh against for our differential spawn distance check, it’s time to extract the location of the instance from its transform, as well as the location of our most recent randomly generated transform, stored earlier in this function in the variable “Comparative Transform.” So we’re going to break the transforms, get the locations, subtract them from each other (Previous Instance Transform Location-Comparitive Transform Location) and get the Vector Length. Then we need to compare that length value to a Min Spawn Distance Between Instances editable float variable, and check to see if it’s greater than or equal to that definable value. If it is, the branch node that the return value for the ≥ node is feeding will move forward with spawning an instance, as it’s determined that the last spawn location is far enough away from the most recently generated spawn location. If it’s not, the branch will fire into a Set node for the # of Rocks to Spawn editable Integer variable, where the value is the # of Rocks to Spawn subtracted by 1, so that the script knows that it didn’t successfully spawn a mesh on this loop, and that it needs to go back and try again with another random transform.
Custom Spawn Rocks Function (Part 2)
Okay, now that we’ve built out our other custom functions, it’s time to hook everything up and test it out. You’ll want to use a Get (A Copy) node again to figure out which HISM component (or mesh shape) is going to be used, using the Spawn Rocks Random Integer value we stored a couple steps ago as the index value, and then from that Get node, we’ll create an Add Instance World Space node, using the T Out value from our Rock-Rock Distance function as the World Transform value of the instance being spawned. After that, we just need to set the Last Used HISM integer value, derived from the Spawn Rocks Random Integer value again, so that on the next loop, the script will know which HISM component to compare against for the distance check.
And that’s it! Save your work and test out the actor in your editor. Be careful how many instances you’re spawning, and note that on my machine, which runs an i5 4670k and a 980ti, anything after about 20k instances will take a couple seconds to load. Once the instances are loaded however, I still get a constant 120fps, owing to the HISM actor’s great batching and draw-call reduction.
I’ll re-add the images from this tutorial in order of how they should be completed (at least in my opinion, if you want to try it in a different order I don’t see any harm) so that you can review your functions and layouts and make sure that everything matches. If you’ve reviewed your scripts and something still isn’t working right, hit me up at email@example.com and let me know, and I’ll take a look at it with you.
Thanks for reading, and happy developing!