Voxel Farm Unreal 4 Material Harvesting Rates

Wed 16 August 2017

Today's Goal

Voxel Farm just released a new version of their Unreal plugin about two weeks ago, and it has shown major improvement over the last version. With much better code organization, and highly increased performance it is easier to work with the plugin than ever before.

Today, we are going to explore how to implement material based harvesting for voxels in Voxel Farm and Unreal 4. Our goal is that different voxel materials will have different harvesting speeds so that it takes longer to pick up 'heavy' materials like stone than it does to harvest 'lighter' materials like sand. Here is a video demonstrating varying harvest speeds for 2 different materials from my current development efforts on Cyboreal:

To accomplish this task, we need to be able to discover what voxel material is in our line of sight. We are going to do this by extending the VoxelFarmPlugin to add 2 new USTRUCT as well as a new blueprint callable function for the VoxelFarmWorldActor. This will allow us to retrieve the data held in VoxelFarm about line of sight that is not currently exposed in the plugin. Following that we will write some code for the FPS character template that makes use of our new functionality in order to implement material based harvest times for remove voxel calls.

Extending the Voxel Farm Plugin

Our first step will be to extend the VoxelFarmPlugin to expose more of the functionality of VoxelFarm. You can find the project structure for the VoxelFarmPlugin inside your Unreal project's 'Plugins' subdirectory. When building your code that modifies the plugin it is a good idea to run 'Clean Solution' prior to build as sometimes things left in the cache can cause problems. Also, keep in mind that when VoxelFarm updates their plugin we will have to bring this code forward with us manually. It amounts to 2 headers and a single function inserted in one of the existing classes so it won't be too difficult, but hopefully Voxel Farm will figure out how to expose a source controlled repo to us in the future.

Exposing VoxelHitInfo to Blueprints

VoxelFarm already comes with a class that allows you to get information about the end-point of a AVoxelFarmWorldActor::SetBlockLineOfSight call. This is the VoxelFarm::VoxelHitInfo struct that can be found in ClipmapView.h in the VoxelFarm includes. It contains data such as whether there was a valid collision, the world space position of the hit, the CellId for the cell that was hit, the voxel coordinate that was hit, and the material of the voxel.

Wrapping CellId

The first challenge we have is that we can't directly expose the VoxelFarm::CellId to Unreal, we need to create a new USTRUCT that will allow us to pass this data out to Blueprints in a more friendly manner. CellId is made up of 4 separate integers representing the Level of Detail, and X, Y, Z coordinates of the cell.

Let's create a new header file and add it to VoxelFarmPlugin/Source/VoxelFarmPlugin/Public:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#####VoxelFarmCellId.h
#pragma once
#include "Engine.h"
#include "VoxelFarmCellId.generated.h"

USTRUCT(BlueprintType)
struct  VOXELFARMPLUGIN_API FVoxelFarmCellId
{
    GENERATED_USTRUCT_BODY()

    UPROPERTY(BlueprintReadWrite, Category = "Voxel Farm")
        int Level;

    UPROPERTY(BlueprintReadWrite, Category = "Voxel Farm")
        int X;

    UPROPERTY(BlueprintReadWrite, Category = "Voxel Farm")
        int Y;

    UPROPERTY(BlueprintReadWrite, Category = "Voxel Farm")
        int Z;

    FORCEINLINE bool operator==(const FVoxelFarmCellId& other) const
    {
        return (Level == other.Level && X == other.X && Y == other.Y && Z == other.Z);
    }

    FORCEINLINE bool operator!=(const FVoxelFarmCellId& other) const {
        return (Level != other.Level || X != other.X || Y != other.Y || Z != other.Z);
    }

    FVoxelFarmCellId() {}
};

We have also implemented some equality functions so that we can easily test to see if 2 CellId are equal to each other. This will come in hand when we want to determine whether or not we are looking at the same voxel as last frame.

Wrapping VoxelHitInfo

Now that we have a CellId container, we are ready to wrap the actual VoxelHitInfo struct into a similar USTRUCT. Don't forget to add the api specifier 'VOXELFARMPLUGIN_API' to any new structures you introduce to the plugin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#####VoxelFarmHitInfo.h
#pragma once
#include "VoxelFarmCellId.h"
#include "Engine.h"
#include "VoxelFarmHitInfo.generated.h"

USTRUCT(BlueprintType)
struct  VOXELFARMPLUGIN_API FVoxelFarmHitInfo
{
    GENERATED_USTRUCT_BODY()

    UPROPERTY(BlueprintReadWrite, Category = "Voxel Farm")
        FVoxelFarmCellId CellId;

    UPROPERTY(BlueprintReadWrite, Category = "Voxel Farm")
        bool Success;

    UPROPERTY(BlueprintReadWrite, Category = "Voxel Farm")
        int Material;

    UPROPERTY(BlueprintReadWrite, Category = "Voxel Farm")
        FVector WorldPos;

    UPROPERTY(BlueprintReadWrite, Category = "Voxel Farm")
        FVector VoxelPos;

    FORCEINLINE bool operator==(const FVoxelFarmHitInfo& other) const
    {
        return (CellId == other.CellId && Success == other.Success && VoxelPos == other.VoxelPos);
    }

    FORCEINLINE bool operator!=(const FVoxelFarmHitInfo& other) const
    {
        return (CellId != other.CellId || Success != other.Success || VoxelPos != other.VoxelPos);
    }

    FVoxelFarmHitInfo(){
        # Let's initialize Success to False and Materail to 0 so that
        # it doesn't appear like a successful hit query on accident.
        Success = false;
        Material = 0;
    }
};

Note that for our equality operators, we avoid comparing WorldPos when deciding whether 2 hit infos are equal. This is because the WorldPos will vary based on the precise path of the ray trace; however, at least in my case, I am only concerned about whether or not we are hitting the same Voxel.

Extending AVoxelFarmWorldActor

Ok, now we want to make use of our new struct to expose the hit info in a way that we can use outside of the plugin itself. Open up VoxelFarmWorldActor.h and find the section where StampBlock and the other public API functions are declared and add a new function. Don't forget to include the headers we just created:

1
2
3
4
5
#####Add this function to VoxelFarmWorldActor.h

#I usually add in the section where stamp block is
UFUNCTION(BlueprintCallable, Category = "VoxelFarm")
    FVoxelFarmHitInfo InfoAtLineOfSight();

Now that we have declared our function, let's go ahead and create the body of the function. Switch over to VoxelFarmWorldActor.cpp and once again I'd suggest finding the area of the file where StampBlock and all the other public functions are declared. All this function will do for the most part is check to make sure VoxelFarm is initialized, and that the hit info we got is valid, and then copy the data from the VoxelFarm:VoxelHitInfo struct to our new FVoxelFarmHitInfo struct that has been properly exposed for use outside of the plugin.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#####Body for the function (add to VoxelFarmWorldActor.cpp)

FVoxelFarmHitInfo AVoxelFarmWorldActor::InfoAtLineOfSight()
{
    FVoxelFarmHitInfo hitInfo = FVoxelFarmHitInfo();
    FVoxelFarmCellId cellId = FVoxelFarmCellId();
    #If nothing is initialized yet, we have a failed hit.
    if (!initialized ||
        !vfWorld ||
        !vfWorld->clipmapView ||
        vfCellFactory->firstScene)
    {
        hitInfo.Success = false;
        return hitInfo;
    }
    VoxelFarm::VoxelHitInfo hitInfoInternal = vfWorld->clipmapView->hitInfo;
    #If the hit is not valid, we also have a failed hit.
    if (!hitInfoInternal.valid) {
        hitInfo.Success = false;
        return hitInfo;
    }
    #When a line of sight trace is inaccurate I've seen it return an invalid
    #material with id -842150451. Let's treat this case like an invalid hit.
    if (hitInfoInternal.material == -842150451) {
        hitInfo.Success = false;
        return hitInfo;
    }
    hitInfo.Success = true;
    hitInfo.WorldPos.X = hitInfoInternal.position[0];
    hitInfo.WorldPos.Y = hitInfoInternal.position[1];
    hitInfo.WorldPos.Z = hitInfoInternal.position[2];
    hitInfo.VoxelPos.X = hitInfoInternal.voxel[0];
    hitInfo.VoxelPos.Y = hitInfoInternal.voxel[1];
    hitInfo.VoxelPos.Z = hitInfoInternal.voxel[2];
    hitInfo.Material = hitInfoInternal.material;
    VoxelFarm::unpackCellId(hitInfoInternal.cell, cellId.Level, cellId.X, cellId.Y, cellId.Z);
    hitInfo.CellId = cellId;
    return hitInfo;
}

The only thing of note here is the VoxelFarm::unpackCellId function, this is how we take a CellId and get the 4 ints we want out of it. There is also a VoxelFarm::packCellId to go the other way if you need to extend the plugin going in the other direction for your use case.

Hooray now you should be able to compile your project and once you restart Unreal Engine you should be able to find our new functions and structs in the Blueprint editor.

Implementing Timed Harvesting on Our Character

Now we are ready to make use of our new code. I started with the C++ FPS template so that is what this tutorial is going to assume you are working with. We need to solve a few problems in order to set this up properly. At the moment, Voxel Farm shows you how to instantly add or remove voxels. What we want to do is assign a 'hardness' to each of our voxel material types, and then when the player is harvesting they subtract from the hardness for that material until it reaches 0 at which point we invoke the voxel removal.

Building a Material Map

Step one of our process will be to create a class to represent our materials in game. This will be where we store the additional data about materials we need for our game. Make sure to replace the CYBOREAL_API specifier with your own games name in all of the following code samples. Go ahead and add a new header and cpp file for our VoxelMat class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#pragma once
class CYBOREAL_API VoxelMat
{
public:
    int matId;
    float hardness;
    FText name;
    VoxelMat(int matIdIn, float hardnessIn, FText nameIn);
    ~VoxelMat();
};

At the moment, I only have 3 data points I'm keeping track of for my VoxelMat, the matId which is the integer that this material corresponds to from VoxelFarm, the name of the material type for use later, and the hardness variable previously mentioned.

The cpp file for this is currently just a stub, I plan on adding more in the future so I have gone ahead and added it instead of just doing everything in the header.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#include "VoxelMat.h"
#include "Cyboreal.h"
VoxelMat::VoxelMat(int matIdIn, float hardnessIn, FText nameIn)
{
    matId = matIdIn;
    hardness = hardnessIn;
    name = nameIn;
}

VoxelMat::~VoxelMat()
{

}

With our VoxelMat we should now create a way to create, keep track of, and access them from anywhere in the application so that we can find out about our materials wherever we need to. I have chosen to use a simple singleton. Here is our VoxelMats.h, we will store all our materials in a TMap with keys of the integer representing that material from VoxelFarm.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#pragma once
#include "VoxelMat.h"
#include <map>
/**
 * 
 */
class CYBOREAL_API VoxelMats
{
private:
    /* Here will be the instance stored. */
    static VoxelMats* instance;

    /* Private constructor to prevent instancing. */
    VoxelMats();
    ~VoxelMats();

public:
    /* Static access method. */
    static VoxelMats* getInstance();

    UPROPERTY()
        TMap<int, VoxelMat*> materials;

};

Here is the implementation:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define LOCTEXT_NAMESPACE "Cyboreal" 
#include "VoxelMats.h"
#include "Cyboreal.h"
#include <map>
#include <string>

VoxelMats* VoxelMats::instance = 0;

VoxelMats::VoxelMats()
{
    materials.Add(0, new VoxelMat(0, 0, LOCTEXT("Air", "Air")));
    materials.Add(5, new VoxelMat(5, 100, LOCTEXT("Water", "Water")));
    materials.Add(4, new VoxelMat(4, 50, LOCTEXT("Grass 1", "Grass 1")));
    materials.Add(1, new VoxelMat(1, 200, LOCTEXT("Stone", "Stone")));
    materials.Add(3, new VoxelMat(3, 100, LOCTEXT("Sandstone", "Sandstone")));
    materials.Add(2, new VoxelMat(2, 25, LOCTEXT("Sand", "Sand")));
}

VoxelMats* VoxelMats::getInstance()
{
    if (instance == 0)
    {
        instance = new VoxelMats();
    }

    return instance;
}

VoxelMats::~VoxelMats()
{
}

#undef LOCTEXT_NAMESPACE

At the moment I am just declaring my materials in the constructor, it is possible you'd want to make this more complex in the future (perhaps loading from a file), make sure that the integers you use are the ones you pull from Voxel Studio or your materials will end up funky, and things won't work right if you pass a material identifier that doesn't exist to voxel farm.

Modifying character to store line of sight details

We will start off by setting up the character to receive line of sight information. First off all, add a new UPROPERTY to your character class.

1
2
3
4
#add to your ACharacter.h file in the public section

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = VoxelHitInfo)
    FVoxelFarmHitInfo currentHitInfo;

Now if you try to compile you should get an error that it can't find FVoxelFarmHitInfo. This is because we need to tell Unreal that you now depend on the VoxelFarmPlugin. Open your project.build.cs, you should find it in Source/ProjectName in your Unreal Project directory. Add 'VoxelFarmPlugin' to PublicDependencyModuleNames.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
using UnrealBuildTool;

public class Cyboreal : ModuleRules
{
    public Cyboreal(ReadOnlyTargetRules Target) : base(Target)
    {
        PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;

        PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", 
            "InputCore", "HeadMountedDisplay", "VoxelFarmPlugin" });
    }
}

It should look something like this. Now you should be able to compile your project, restart Unreal, and modify your Level Blueprint's onTick to look like this:

OnTick Blueprint

Introduce a Harvesting Event

I want to be able to setup our harvesting calls in Blueprint as currently defined in the Voxel Farm unreal tutorials, but generate the call from C++. We are going to introduce a new event that can be called from C++ and bound to in Blueprint so we can have our cake and eat it too.

At the top of our character header, let's introduce a new event:

1
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FNotifySetBlock, int, Mat);

We give it a single argument of an integer that corresponds to the material identifier of the material we want to stamp (0 for 'air' which is remove voxel, anything else for a material) Then further down, in the public section where we added our currentHitInfo property add:

1
2
UPROPERTY(BlueprintAssignable, Category = VoxelEditing)
    FNotifySetBlock OnSetBlock;

Go ahead and also add an editDistance property as we will use it to set the maximum distance at which voxels can be modified.

1
2
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = VoxelEditing)
    float editDistance;

Once we compile this we can now setup our Blueprint.

OnTick Blueprint

We can now make use of our event and our currentHitInfo data to perform a voxel edit from C++ where we also check to make sure the collision is within an appropriate distance:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void ACyborealCharacter::OnFire2()
{
    if (currentHitInfo.Success && currentHitInfo.Material != 0 && 
        FVector::DistSquared(currentHitInfo.WorldPos, FVector(0., 0., 0.)) 
        < editDistance * editDistance) {
        if (OnSetBlock.IsBound()) {
            OnSetBlock.Broadcast(2);
        }
    }
}

Notice that we check the distance between WorldPos and (0., 0., 0.), this is because your character is always the center of the World when we set the ClipmapView to be centered on our character. If you did something else with the Clipmap focus you would have to measure distance from a different point.

In this case I'm making it so that we will place down a '2' material when you click the mouse button.

Don't forget to add the binding to the PlayerInputComponent, by default this is the SetupPlayerInputComponent function, you also need to declare your OnFire2 function in the character header and add a 'Fire2' input to your project settings:

1
2
PlayerInputComponent->BindAction("Fire2", IE_Pressed, this,
                                 &ACyborealCharacter::OnFire2);

Add Voxel Removal Logic

Finally, we are going to setup the character so that removing voxels occurs over time. The first step is to change a mouse click from directly calling OnSetBlock to setting our character into a harvesting mode.

Let's add a few more variables to our character header. We will add a second FVoxelFarmHitInfo so that we can track when we change which voxel we are looking at. (this is where all those equality methods we defined on our structs will come in handy) We will also need a harvestRate that determines how quickly we subtract hardness from a material, a bool to keep track of when we are in harvesting mode, and a harvestingProgress that keeps track of what amount of hardness we have actually removed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public:

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = VoxelHitInfo)
    FVoxelFarmHitInfo lastHitInfo;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = VoxelEditing)
    float harvestRate;

protected:

    bool harvesting;

    float harvestingProgress;

You will also need a Fire and FireRelease function to handle putting us in and removing us from harvesting mode, add these to the header as well.

1
2
3
4
5
protected:

    void OnFire();

    void OnFireRelease();

These functions will be very simple, just setting harvesting when you click the button, and unsetting when we release:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void ACyborealCharacter::OnFireRelease()
{
    UE_LOG(LogTemp, Warning, TEXT("harvesting stop"));
    harvesting = false;
}

void ACyborealCharacter::OnFire()
{
    UE_LOG(LogTemp, Warning, TEXT("harvesting start"));
    harvesting = true;
}

You can add some log calls so that you know what mode you're in since we won't have an animation to tell us at this point.

Ok now bind those to actions as well:

1
2
3
4
PlayerInputComponent->BindAction("Fire", IE_Pressed, this, 
                                 &ACyborealCharacter::OnFire);
PlayerInputComponent->BindAction("Fire", IE_Released, this,
                                 &ACyborealCharacter::OnFireRelease);

Alright, now let's add the final logic. We need to override the Tick function to perform our harvesting calculations.

1
virtual void Tick(float DeltaSeconds) override;

Now we can write our function, add it to the character cpp file. Just like before we need to first check if we have a valid hit, and if it is within the editDistance. Next up, we check to see if we are looking at the same voxel, if not, we set lastHitInfo and reset harvestingProgress.

We then increment the harvestingProgress by the harvestRate scaled by time passed. We then use our VoxelMats singleton to get the hardness for the material we are looking at, compare that to our progress and if we are above it we call our harvest event with an argument of '0' for air voxels. Harvesting progress will automatically set back to 0 since this will implicitly produce a new voxel we are looking at once the update has happened.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void ACyborealCharacter::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    if (harvesting && currentHitInfo.Success && currentHitInfo.Material != 0 
        && FVector::DistSquared(currentHitInfo.WorldPos, 
            FVector(0., 0., 0.)) < editDistance * editDistance)
    {
        if (currentHitInfo != lastHitInfo) {
            lastHitInfo = currentHitInfo;
            harvestingProgress = 0.0f;
            UE_LOG(LogTemp, Warning, TEXT("We have switched voxels"));
        }
        harvestingProgress += DeltaTime * harvestRate;
        UE_LOG(LogTemp, Warning, TEXT("harvesting above limit %f"), harvestingProgress);
        VoxelMats* voxelMats = VoxelMats::getInstance();
        float hardness = voxelMats->materials[currentHitInfo.Material]->hardness;
        if (harvestingProgress > hardness) {
            if (OnSetBlock.IsBound()) {
                OnSetBlock.Broadcast(0);
            }
            UE_LOG(LogTemp, Warning, TEXT("harvesting above limit"));
        }
    }
}

There you go; now compile and you should be harvesting voxels at varying rates based on material! Thanks for reading.

blogroll