Splat-mapping Abstraction in Unity

Hello all! I’m posting today with a brief follow up on my post about terrain texture detection in Unity. If you haven’t checked that out, it will definitely be informing this post! I have been working further on getting this system to work with my current PlayerMovement system, and have come to a neat solution of abstraction which can be branched to communicate anywhere in your game what the current terrain texture is beneath the player.

Here’s what this solution solves:

Our player’s movement is effected constantly by what type of ground the player is on. So for example, braking power & amount of friction (2 separate variables in the PlayerMovement). Since we need these values to be dynamic, a layer of abstraction is helpful in allowing these values to be continuously modified by factors such as the terrain.

Our designers have not finalized the variables effecting movement at large, this solution allows for testing of multiple combinations of these movement variables as extreme ease to the design team. This will be used for AB testing in our playtests down the road.

The solution:

It’s very simple, but has helped a lot with working this data into my PlayerMovement in a clean and clutter free way. All I have done is created a new Scriptable Object type, MovementVariables, and moved all of the “designer variables” into this scriptable object. Additionally, I have created a simple class, TerrainType, which stores all of the variables that are dynamic and dependent upon the terrain the player is on. I’ve made this class serializable, and within my MovementVariables I have a public array of TerrainType that allows the designers to set each terrain type uniquely for each variant of the MovementVariable type.

MovementVariables has a public function, SetToTerrain(), that takes in an int representing the terrain texture the player is currently on (remember this is stored in an int by the Alpha map). Upon taking that int, MovementVariables will set the necessary variables to match those of the corresponding TerrainType in the local array. So, for example, terrainTypes[0] is created by the designer to have float frictionLevel = 1, and float brakingPower = 3. Once SetToTerrain() takes in an int 0, then MovementVariables will set the frictionLevel and brakingPower according to whatever is in terrainTypes[0].

From here, all that is necessary for setup (besides setting the designer variables) is to create a reference to a MovementVariables in both the PlayerMovement and the TerrainTextureGetter. The former will simply read the scripted values from this Scriptable Object, and the latter will pass the int representing the texture into SetToTerrain().

… and that’s it! It’s a super simple solution but really has helped me in passing these terrain settings into my movement, in making my PlayerMovement less beefy, and in aiding the design team with finalizing their movement variables… Here’s some code

[System.Serializable]
public class TerrainType
{
   public string terrainTypeName;
   [Range(0f, 100f)]public float stridePushPower;
   [Range(0f, 15f)] public float brakingPower;
   [Range(0f, 5f)] public float slidingFrictionPower;
   [Range(0f, 10f)] public float turningSpeed;
}

This is the TerrainType class which is key in allowing this abstraction to work. These are the variables in PlayerMovement which we want to be altered by the terrain type beneath the player.

//Variables set by terrain
[ShowOnly]public float turningSpeedMod;
[ShowOnly]public float slidingFrictionPower;
[ShowOnly]public float brakingPower;
[ShowOnly]public float stridePushPower;

public void SetToTerrain(int terrainNumber)
{
    if (terrainNumber == 0 && terrainVariableSets[0] != null)
    {
        turningSpeedMod = terrainVariableSets[0].turningSpeed;
        slidingFrictionPower = terrainVariableSets[0].slidingFrictionPower;
        brakingPower = terrainVariableSets[0].brakingPower;
        stridePushPower = terrainVariableSets[0].stridePushPower;
    }
    else if (terrainNumber == 1 && terrainVariableSets[1] != null)
    {
        turningSpeedMod = terrainVariableSets[1].turningSpeed;
        slidingFrictionPower = terrainVariableSets[1].slidingFrictionPower;
        brakingPower = terrainVariableSets[1].brakingPower;
        stridePushPower = terrainVariableSets[1].stridePushPower;
    } 
    else if (terrainNumber == 2 && terrainVariableSets[2] != null)
    {
        turningSpeedMod = terrainVariableSets[2].turningSpeed;
        slidingFrictionPower = terrainVariableSets[2].slidingFrictionPower;
        brakingPower = terrainVariableSets[2].brakingPower;
        stridePushPower = terrainVariableSets[2].stridePushPower;
    } 
    else if (terrainNumber == 3 && terrainVariableSets[3] != null)
    {
        turningSpeedMod = terrainVariableSets[3].turningSpeed;
        slidingFrictionPower = terrainVariableSets[3].slidingFrictionPower;
        brakingPower = terrainVariableSets[3].brakingPower;
        stridePushPower = terrainVariableSets[3].stridePushPower;
    }
    else if (terrainNumber > terrainVariableSets.Length - 1)Debug.LogError("Unsupported Terrain Type. Add to MovementVariablePackage");
}

This chunk of code above is located within my MovementVariables Scriptable Object. The top variables are what are being referenced within PlayerMovement, but they are being set below. SetToTerrain() is called every time a terrain texture change is detected. An important note is the [ShowOnly] editor attribute was written and made public by Stack Overflow user Lev-Lukomskyi. Huge thanks for that, as it keeps the designers from touching things they shouldnt ;). I’m just kidding… I hope that this post was helpful for anyone who needed a follow up from my last post about terrain texture detection! Until next time!

Detecting Textures Across Multiple Terrains In Unity

Hello all, I hope that as you read this, all is well in your world. The last week has been a truly fitting capstone on the crazy year that has been 2020, especially for U.S. citizens. I want to take your mind off the madness for a moment and talk about something I worked on over the last few days.

As you can read about in my October Update, I’ve been making a skiing game! It’s been awesome so far, and really enjoyable. Something that is crucial to the design of our game is different textures on the terrain beneath the player having differing effects on gameplay. For example, we have a texture that represents “heavy snow”, which will bring down the speed at which the player treks through the snow, as well as an “ice” texture, which would instead speed the player up at a much higher rate, and slow down at a much slower rate. The process itself is not complicated, it just involves getting the terrainData of terrain below the player, and using the player’s relative position to the terrain to calculate what the texture is that is present at that point on of the terrain, and in what strength. While these calculations are somewhat daunting (involving the 3D float array used to represent an alpha map), they’re actually not too complex when broken down.

Where my specific need for this process differs from a lot of what I’ve seen online is that our Unity scene involves upwards of 20 separate terrain objects (and therefore over 20 individual TerrainData data type). The solution here for me was to setup a function to work in tandem with the terrain splat mapping calculations above. This function takes the player’s position, and compares it to the center point of the terrains stored in the array Terrain.activeTerrains. The terrain returned is then interpolated on to determine what textures is beneath the player. Here’s the code return the closest terrain to the player! Just remember I use Scriptable Objects to store live values (such as player posisiton), hence the need for me to specify “.value” to get the Vector3.

Terrain GetCurrentTerrain()
{
    //Array of all terrains
    Terrain[] totalTerrains = Terrain.activeTerrains;

    //Checks on length
    if (totalTerrains.Length == 0) return null;
    else if (totalTerrains.Length == 1) return totalTerrains[0];
    
    //closest terrain, Initialized with totalTerrains[0]
    Terrain closestTerrain = totalTerrains[0];
    
    //Center of terrain at totalTerrains[0]
    Vector3 terrainCenter = new Vector3(closestTerrain.transform.position.x + closestTerrain.terrainData.size.x / 2, playerPos.value.y, closestTerrain.transform.position.z + closestTerrain.terrainData.size.z / 2);
   
    //will be closest distance between player a terrain. Initialized with totalTerrains[0]
    float closestDistance = Vector3.Distance(terrainCenter, playerPos.value);
    

    //Iterate through list of all terrains
    for (int rep = 1; rep < totalTerrains.Length; rep++)
    {
        //currently selected terrain
        Terrain terrain = totalTerrains[rep];
        terrainCenter = new Vector3(terrain.transform.position.x + terrain.terrainData.size.x / 2, playerPos.value.y, terrain.transform.position.z + terrain.terrainData.size.z / 2);

        //Check on distance compared to closest terrain
        float d = Vector3.Distance(terrainCenter, playerPos.value);
        if (d < closestDistance)
        {
            closestDistance = d;
            closestTerrain = totalTerrains[rep];
        }
    }
    //Returns the closest terrain
    return closestTerrain;
}

So now that we have the closest terrain to our player, we need to convert the player’s position in the game to their position on the specific alpha map. This process looks something like this, noting that currentTerrain has just been set to whatever is returned by GetCurrentTerrain():

void GetPlayerTerrainPosition()
{
//Player position relative to terrain
Vector3 playerTerrainPosition = playerPos.value - currentTerrain.transform.position;

//Player position on alphamap of terrain using offset
Vector3 alphamapPosition = new Vector3 (playerTerrainPosition.x / currentTerrain.terrainData.size.x, 0, playerTerrainPosition.z / currentTerrain.terrainData.size.z);

//Properly scales players x and z
float xCoord = alphamapPosition.x * currentTerrain.terrainData.alphamapWidth;
float zCoord = alphamapPosition.z * currentTerrain.terrainData.alphamapHeight;

//Casts as int and sets
xPos = (int)xCoord;
zPos = (int)zCoord;
}

We get out of this call with now our 2 xPos and zPos fields set to the player’s coordinates on the terrain. All that’s left is to take these coordinates and get the alpha map at the player’s position, and determine which terrain texture is applied at that location. One important note is how alpha maps store references to textures. the alpha map is a 3d array where the third value refers to the texture being checked for. For example alphaMap[0,0,0] will return the strength of the texture in slot 0 of the terrain texture layer. alphaMap[0,0,1] will return the strength of texture in slot 1. Hence, splatmapping! We are able to interpolate on various combinations of strength of textures, not just simply player is or isnt on ice. Instead, we can say 30% ice, 70% regular snow, and have our movement variables adjust to that specific combination…. I’m getting off track, but just know this:

textureValues[] is an array of floats representing the strength of each texture at the specified x & z pos. The length of this array is simply set to the number of textures in our terrain layers.

here, rep is used to tie the corresponding spot in textureValues to the value of the texture in that slot of the 3d array

SetPlayerMovementVariables() is currently where we are interpolating on the data gathered here, but essentially the value is clamped from 0 to 1, representing how much of the splatmap at that point is of the texture in the corresponding spot in aMap, and from there we are setting values in our PlayerMovement script. Take a look!

//gets the float (clamped between 0 and 1) of how much of each texture is present at the x & z coord
void CheckTextureBelowPlayer()
{
//Will store the alpha map of the current terrain
float[,,] aMap;

//Uses x position and z position to set aMap to correct alpha map
aMap = currentTerrain.terrainData.GetAlphamaps(xPos, zPos, 1, 1);

//textureValues stores the current stength of the texture stored in the corresponding slot in the alpha Map
for (int rep = 0; rep < textureValues.Length; rep++ )
{
//stores stength of values at that point
textureValues[rep] = aMap[0, 0, rep];
}


//Iterates through to check if any values are greater than 0
for(int rep = 0; rep < textureValues.Length; rep++ )
{
//If terrain is present, sets player movement values
if(textureValues[rep] > 0)
{
SetPlayerMovementVariables(rep, textureValues[rep]);
}

}
}

This flow of operations effectively allows my team’s PlayerMovement script to iterate as usual, but be fed different live values decided by the terrain type. So far this works really well for me, but if I find more to change and tweak, I absolutely will update it here! I hope this helps anyone who is setting out to do this themselves, much like I was a few days ago! Hope you enjoy the code!