An obvious staple in countless games, player impact on the world is a huge focus for gameplay in Woodbound. Back when The Forest first came out and players discovered the ability to literally deforest the entire island, one tree at a time, I found myself deeply fascinated and impressed that an indie team could accomplish such a massive feat. Little did I know, the ability to set up a system like this is well within reach for even solo developers. I recently conceptualized and tested how this system could work in UE4, and I'm thrilled to have it in Woodbound, and to show you how you can add it to your game as well. Here are the source files, if you're interested.
The first thing that you'll need is your tree mesh. In the image above, I've just used the Sapling plugin in Blender to generate a basic fir tree mesh, which I've added a crude set of trunk roots to. (This tutorial is more about the scripting and less about the modeling, lol.) Once you have your tree mesh, there are a few steps we'll need to follow in order to get the pieces we need:
After you've exported your tree mesh, duplicate it and set your duplicate aside, just as a quick backup. Locate a section along the base of the tree, just above where the trunk would begin, and add a 3x loop subdivide, squashing the loops down and placing them at around stomach height for your character on the Z axis. Once you've scaled and straightened out your loops, select the middle loop, and delete the vertices. From there, grab the remaining loop vertices, one loop set at a time, and fill in the empty space left using the F key. From there, you can select the top of the mesh in edit mode, and press P to separate the top from the bottom into separate objects, which you can then export as your trunk and tree top, which will serve as your visual objects after your tree has been, "cut."
Okay cool, we have our meshes. Now let's get to the fun part; the scripting. I created this project in UE4 4.16. The first step is to create a new project using the First Person Character template. In your project folder, create a folder and name it TreeTutorial. In that folder, make three more folders; Blueprints, Materials, and Meshes. Your meshes and their materials should go in the appropriate folders. In your Blueprints folder, create a Blueprint Actor, and name it something like BP_InteractiveTrees. The editor tree generation and the run-time generation are set up a bit different from each other, but since this is intended for runtime use in a packaged project, let's start with the event graph.
First thing's first, let's add some components. We'll need two Instanced Static Mesh Components, (or Hierarchical Instanced Static Mesh Components if you're using meshes with LODs) one for our full tree mesh, and one for our trunk. The tree top mesh will come into play later in this tutorial, in a separate actor.
Next, we need some variables:
Once you have your variables, we have a couple functions that we need to set up. First up is a controlled random location function, which picks a random x & y distance from the actor location, as well as a random Z location in the world within min & max height values (max is set here, min is set in the trace behavior coming up):
Then we need a function that checks for placement overlaps:
This function will take the hit location from the trace behavior we're about to set up, and compare it to every successful tree placement location (stored in our Comparitive Vectors array) to see if it's far enough away using our MinDistanceBetweenInstances value.
Now that we have our variables & our functions, let's get into the BeginPlay string that'll help us set the stage for the looping spawn behavior.
***WARNINGS AHEAD, READ THEM CLOSELY OR RISK CRASHING YOUR PROJECT***
So in this section, on begin play this actor (if present in your level) will check to see if you've allowed for runtime tree generation using the appropriately named boolean value. If you've marked it as true, it will set a looping timer, which will call the Custom Event, "EventRuntimeTrees" to fire when it reaches 0.
***VERY VERY VERY IMPORTANT, DON'T MAKE YOUR TIME VALUE ON SET TIMER BY EVENT ANY LOWER THAN 0.0001. ANY LOWER AND YOUR PROJECT MIGHT CRASH.***
Now that I've gotten that VERY important warning out of the way, I have one other VERY important warning:
***VERY VERY VERY IMPORTANT: UNLESS YOU HAVE A MONSTROUS RIG, I HIGHLY ADVISE AGAINST SPAWNING MORE THAN 50K INSTANCES USING THIS SYSTEM. WITH AN I5 4670K, SPAWNING IN MORE THAN 50K UNITS AT RUNTIME CAUSES SERIOUS SLOWDOWN, WITH FRAMERATES DIPPING DOWN TO 5-10FPS AT 100K UNITS.***
The first thing that EventRuntimeTrees will do is check if you've reached the maximum amount of instances spawned. If you haven't, it'll continue it's trace loop, but if you have, it'll stop the loop that's delegating the executions, effectively shutting down the spawn loop. So if you were to spawn more than 50K units, WHICH I HIGHLY ADVISE AGAINST, your framerate would jump back up to whatever it would be if you had just painted the same amount of instances in using the regular foliage painter.
This section dictates the trace behavior. It takes the random xy location from our function we made earlier, and then traces straight down from the worldspace Z location (SpawnMaxHeight) derived from the same function, to the same XY location at the worldspace Z location below (SpawnMinHeight) and returns the hit location from the landscape along that trace in the break hit result up ahead. The reason that we use this trace instead of picking a random XYZ location is so that we can still have a random XY location, but align the tree to the landscape on its Z location. If we didn't derive our Z location from a landscape trace, we'd have no way to line the trees up with the landscape.
Okay, now that we've run our trace, and (if you've set up your min & max heights to encompass your landscape at any/all of the applicable XY locations) we've returned a hit from it, we can use the location info (along with some branch checks for applicability) to determine a) whether or not to place a tree, and b) where to place it if it can be placed.
Immediately following the line trace, you'll see that we're setting our Temp OV Count to 0. We're doing this for the overlap check function, which will add up 1 integer at a time in the event that any of the comparitive vectors are within the mininum distance between instances value. After checking for overlaps, the branch node ahead will check if the Temp OV Count is more than 0, and if it is, it indicates that the current trace hit location is too close to another location we've stored (storage logic is in the next step) and so we don't want to place a tree at that location. We want to reset the Temp OV Count to 0 on every trace, because we want a clean slate when running the overlap check and the follow-up branch check.
So following the Set Temp OV Count, we have a for loop node, which delegates how many times the Overlap Checks function will run, per-trace. It gets its length from the integer value of the length of the Comparitive Vectors array, so as more trees are spawned, more vectors are stored in that array, and the Overlap Check will account for all of the vectors stored.
When the loop completes, it'll check to see if that Temp OVCount is above 0 as discussed above. If it's not, it means that none of the previous spawn locations are too close to the new one, so we can run the next check, which is making sure that the hit location isn't at this actor's relative 0,0,0 location. I added this step because without it, multiple trees would dogpile at relative 0,0,0, and this was the easiest fix for it.
Down below, we have a Make Transform node, which will be used to generate the location, rotation and scale of the tree that gets spawned. The yaw is randomized here, but you can simply unplug the Random Float in Range node from the Z value in the Make Rotator if you want them to all have the same yaw. The scale just takes a random float in a range dictated by our Min and Max Instance Scale values, and uses that resulting float to multiply the uniform 1x1x1 scale value to set the scale for the tree to be spawned.
This last section controls the actual mesh instance spawning and the comparitive vector array storage. If the trace location hasn't overlapped a previous one and it doesn't hit relative 0,0,0, then an instance will be added to the Tree ISM/HISM in worldspace using the transform information we generated in the last step. After the tree is spawned, the location info from that transform we generated (which is the same as the Location value in our break hit result) is added as a vector instance in our comparitive vectors array for use in the overlap check function the next time the trace is run, if it is run.
Okay, so we've set up our runtime tree spawning behavior, but testing our spawner by launching a PIE or Simulate session can get annoying, so I'm going to show you how to translate this system over to the construction script for debugging, so that you can just hit one button and spawn in the trees. After I show you how the construction script works, we can get to cutting these trees down.
Much of the construction script behaves the same way as the event graph, with the exception of the loop behavior & initiation.
The first branch check in the construction script serves as a, "button," that you can click on and generate your trees. This works by checking to see if the Generate Trees boolean value is true (false by default) and if it is, it'll execute the spawn loop sequence, while also setting itself back to false, giving it a pseudo button-like behavior. If Generate Trees is false, it'll then check for Clear Trees, which does the same button-function, but instead reconstructs the blueprint without running the spawn loop behavior, effectively clearing the instances from your tree ISM/HISM.