[TUTORIAL]: Procedurally placed, cut-able trees in UE4
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.
So if you click generate trees, a loop will execute however many times your Max Trees To Spawn value is set to. You can see we have some weird looking reroute nodes plugging into the Set TempSpawnVec node; I'll get to these in a moment, but they are there due to the way the ForLoop node defines an execution, and the lack of custom event calling in the construction script.
Here's where things get a little yucky. This section is almost identical to its sibling in the event graph, with the noteable addition of these back-looping event delegations from the branch nodes. The reason these are in the constructino script is due to the way the ForLoop node defines an execution: when an execution fires, regardless of additional parameters that you set along your execution string, as soon as the execution hits its end, whether that be at the successful spawn of the tree, or the detection of an overlap, ForLoop will treat that as an execution and move on to its next one. In the event graph, we don't use for loop because we want to control the frequency of the trace for performance reasons. In the construction script we can't do this, so we rely on ForLoop and compromise that there'll be a brief hitch while the trees are spawned (depending on your spawn amount and rig) but this doesn't come as a development consequence because the player will never experience it. However, we still need to get around ForLoop's logic issue, so in the event that an overlap is detected or a relative 0,0,0 spawn is detected, we simply need to reroute our execution back to the Set TempSpawnVec node so that the trace can try again until a suitable location is randomly generated. To do this, I just drag off of the True pin for the OV Count Branch, and search for a reroute node. I then drag off of that reroute node and generate one more, then connect it to the Set TempSpawnVec node. This will allow you to more easily arrange the reroute nodes without having them confuse which direction you're delegating your execution in. You can then run the same steps out of the False pin on your relative 0,0,0 branch check to arrange the reroute back to the TempSpawnVec step. From the relative 0,0,0 True pin, you can execute the same steps as in the event graph to complete the construction script. Okay, so we can spawn in the tree mesh instances, but now we need to be able to cut them down. In order to do so, we need to create one more actor, as well as a function in your First Person Character blueprint. The actor is really simple: go into Content>TreeTutorial>Blueprints, and create another Blueprint Actor, and name it BP_TreeTop. Open it up, and add a Static Mesh Component, using your SM_TreeTop asset as the mesh. Under the mesh properties, tick Simulate Physics to True, and then compile & save and close the actor out. Easiest part of the system done right there, so let's move on to the FirstPersonCharacter function.
Open up your FirstPersonCharacter blueprint, and create a new function, naming it something like TreeTrace.
The first section of the function is a very typical camera forward vector trace. Change the float value in the Vector*Float node to increase or decrease the trace distance. (1500 by default is a decent reach distance.)
From our trace break hit result, we first need to cast to our BP_InteractiveTrees actor. If the cast succeeds, it means that the trace detected a hit on of the the tree instances spawned in that actor. Dragging out from As BP Interactive Trees, we can create an Add Instance Worldspace node, but instead of adding another tree, this time we're going to add a Trunk instance, serving as the trunk left behind when the tree is cut. Dragging off of As BP Interactive Trees again, we can get the transform of the Tree instance that we hit, using the Hit Item value from our break hit result to figure out which instance we hit. We can then feed that World Transform into our Add Instance World Space to figure out where, at what rotation and at what scale to place our Trunk.
After we've added our trunk, we need to add the top. To do this, we use a SpawnActorFromClass node, setting the class to BP_TreeTop actor we made just a moment ago, using the same transform that we used for the trunk spawning. After we've spawned our physics-simulating tree top, which will just fall down because that's all it's programmed to do, we can remove the instance that we hit in the first place, so that the instanced static mesh that served as the visual indicator of the tree can be fully replaced by the static trunk left behind, and the falling top, giving off the illusion that it was just one tree that was chopped and is now falling over. The reason that we remove the instance last in this string is because we need its transform info for the Trunk & Top spawning nodes, and removing it before they've had a chance to grab its transform info returns null transforms and you won't see your trunk or your top. And since the execution happens so quickly, it doesn't affect the physics simulation of the top. After you've set up your TreeTrace function in your character BP, you can simply call it using a key press or input event, and if you're aiming at a tree and within reach, that key press/input event will cause the tree to be cut and fall down:
And that's how it's done! I hope you've found this tutorial helpful, if you have any questions or suggestions, please leave a comment below. If you enjoyed the tutorial, please consider sharing it around so that other people can use it as well.