Unity Terrain Footsteps Feature Image

Terrain Footsteps in Unity (how to detect different textures)

In Articles by John6 Comments

Recently, I’ve been trying to find the best way to build a footsteps system in Unity. At first I though it would be simple, after all detecting a Game Object’s surface material is as easy as adding a tag, right? but what if you can’t use tags, what if you’re actually using terrain for some or all of the walkable surfaces in your game?

Well that’s exactly the problem I ran into while working on my Sun Temple video series.

Terrain objects, like any other object, can only have one tag but they will often use multiple different surface textures that each require different sounds. So, when you’re using terrain in Unity, how can you detect the terrain texture that the player is walking on?

In Unity Terrain textures are stored as Alpha Maps or “Splat Maps”, which are arrays of values that hold the mix of each different texture at every point on the Terrain.  It’s possible to detect what mix of textures the player is standing on at any given moment by converting the player’s current transform position to a coordinate on the alpha map image. This can then be used to check the Alpha Map data at that position. This is ideal for determining what footstep sound should play. In this post I’ll show you how to detect what surface material the player is standing on step by step and how you can use that data to trigger footstep sounds.

Below I’ll explain how this works and show you the method I used to actually do this. If, however you just want a working script that you can copy and paste, skip to the bottom of this post for my finished example.

How to detect terrain textures at a specific world position

In this example I’m going to show you how I used terrain alpha maps to check for certain textures when the player is walking on them. I used the first person character controller but you can use this with any type of player or object as it only needs a transform position to work.

1. Create the script and variables

Before I do anything I’m going to need to create a new script on the player object, I’m going to call it CheckTerrainTexture and then add a few variables:

public class CheckTerrainTexture : MonoBehaviour {

	public Transform playerTransform;
	public Terrain t;

	public int posX;
	public int posZ;
	public float[] textureValues;

}

What I’ve done here is added a transform for the player, a game object variable for the terrain, which I’ve called t to keep my lines a bit shorter, and two integer variables posX and PosZ which I’ll use to hold the alpha map coordinate once I’ve converted it.

I’ve also created a float array call textureValues which I’ll use to hold the texture values that I get (more on that later).

2. Convert the player’s position to an alpha map coordinate

Next, I need to work out where on the alpha map I’m going to check for textures by converting the world position of the player to a coordinate on the alpha map. To do this, I’m going to create a new function called ConvertPosition that takes one parameter, a Vector3, which I’ll use to hold the player’s position.

The way to do this is to first subtract the terrain’s world position from the player’s world position. This removes any offset from the centre of the scene and the start of the terrain. For example the position at the bottom left corner of the terrain, which is where it starts, would now be 0,0.

Next I’m going to measure how far along the x and z axises the position of the player is by dividing the, now offset, position by the total size of the terrain (in world units).

Essentially this returns a position on the terrain that’s represented by a percentage along the x and z axises. For example if the Player was standing in the exact centre of the terrain, the value would be 0.5 along the x axis and 0.5 along the z. If they were standing top right, it’d be 1,1 or somewhere in the top left, maybe 0.24,0.89. You get the idea.

Unity Terrain Map Example

Dividing the relative position of the player by height and width of the terrain gives you a percentage that can be used for the Alpha Map.

Finally I’m going to convert the percentage position value to an alpha map coordinate. To do this, all I need to do is multiply the total width and height of the alpha map image in pixels by the percentage position I just worked out. Then I need to cast it as an integer, which will round it to an exact pixel.

void ConvertPosition(Vector3 playerPosition)
{
  Vector3 terrainPosition = playerPosition - t.transform.position;

  Vector3 mapPosition = new Vector3 
  (terrainPosition.x / t.terrainData.size.x, 0, 
  terrainPosition.z / t.terrainData.size.z);

  float xCoord = mapPosition.x * t.terrainData.alphamapWidth;
  float zCoord = mapPosition.z * t.terrainData.alphamapHeight;

  posX = (int)xCoord;
  posZ = (int)zCoord;
}

3. Retrieve the texture mix at that position

Now that I’ve converted the player’s position to a point on the alpha map, I can now check to see what’s there by using the terrain’s alpha maps.

I’m going to do this in another function, called CheckTexture.

Create a multi dimensional array to store the data

To store the alpha map data I need to use a 3d float array, which is like a normal array just with 3 dimensions.

In scripting, a multi dimensional array looks like this:

private float[] normalArray;
private float[,] 2dArray;
private float[,,] 3dArray;

The 3 dimensions of the alpha map array relate to the x position, z position and the texture that each splat map belongs to.

Each float value in the array is a texture mix, which is a value between 0 and 1, that corresponds to how much of the texture is applied at that position. I can use this to find out what mix of surfaces the player is standing on.

Use Get Alphamaps to get the data

To get the alpha map data I need to use the Get Alphamaps function. This is a function of terrain Data on the terrain class and takes four parameters:

  • x offset
  • z offset
  • sample size width
  • sample size height

In this example I’m going to pass in the x and z coordinates that I previously calculated for the offset but I’m going to set the sample size width and height to 1.

It’s important to remember that you’re taking a sample of data, not all of it. When I first tried this I got mixed up trying to pass in my  x and z coordinates along with the full width and height of the texture.

So if you, like me, keep seeing the error texture rectangle is out of bounds, then you may have made the same mistake.

Store each texture mix value

Now that I know how to retrieve the data, I need to store it in my textureValues array.

Before I do that though I’m going to set the textureValues array size to four in the inspector, as I already know that the terrain I’m using has four textures.

To check how many textures are being used on the terrain, and what order they’re in, open up the paint tab in the terrain options to view all the textures in use.

Terrain Texture Options (Unity)

Click on the paint tab in the Terrain options to find out what order the textures are in.

The textures are numbered, starting from 0, from left to right, you’ll need these indexes to tell each texture apart in the alpha map.

For each value, simply store whatever texture mix is at the array position.

Here’s what it looks like all together:

void CheckTexture()
  {
    float[,,] aMap = t.terrainData.GetAlphamaps (posX, posZ, 1, 1);
    
    textureValues[0] = aMap[0,0,0];
    textureValues[1] = aMap[0,0,1];
    textureValues[2] = aMap[0,0,2];
    textureValues[3] = aMap[0,0,3];
}

Because I only took a 1 pixel sample, I don’t need to pass in x and z coordinates for the offset (because there’s only one value present for each texture). The only value I need to change is the third value, which changes which texture I’m checking. This is the value that matches up to the texture index in the terrain options.

4. Call the functions when you need terrain footsteps

Lastly, with everything in place I’m going to add a public function to call CheckTexture and ConvertPosition whenever I need it.

void Update()
  {
    // For better performance, move this out of update 
    // and only call it when you need a footstep.
    GetTerrainTexture();
  }

public void GetTerrainTexture()
  {
    ConvertPosition(playerTransform.position);
    CheckTexture();
  }

I’m using a public function because a more efficient way to use this method is by only calling it when you need it. By making it public I can have another script call GetTerrainTexture whenever a footstep sound is needed. For now though I’m just calling it in update

Finished Script Example

Here’s the finished working script that I used for you to copy and paste.

Please note that you’ll need to adjust for the number of textures in the textureValues array to match what’s on your terrain.

My example uses four textures, so I set the textureValues array size to four and updated textureValues four times in the CheckTexture function.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CheckTerrainTexture : MonoBehaviour {

public Transform playerTransform;
public Terrain t;

public int posX;
public int posZ;
public float[] textureValues;

void Start () 
  {
    t = Terrain.activeTerrain;
    playerTransform = gameObject.transform;
  }

void Update()
  {
    // For better performance, move this out of update 
    // and only call it when you need a footstep.
    GetTerrainTexture();
  }

public void GetTerrainTexture()
  {
    ConvertPosition(playerTransform.position);
    CheckTexture();
  }

void ConvertPosition(Vector3 playerPosition)
  {
    Vector3 terrainPosition = playerPosition - t.transform.position;

    Vector3 mapPosition = new Vector3
    (terrainPosition.x / t.terrainData.size.x, 0,
    terrainPosition.z / t.terrainData.size.z);

    float xCoord = mapPosition.x * t.terrainData.alphamapWidth;
    float zCoord = mapPosition.z * t.terrainData.alphamapHeight;

    posX = (int)xCoord;
    posZ = (int)zCoord;
  }

void CheckTexture()
  {
    float[,,] aMap = t.terrainData.GetAlphamaps (posX, posZ, 1, 1);
    textureValues[0] = aMap[0,0,0];
    textureValues[1] = aMap[0,0,1];
    textureValues[2] = aMap[0,0,2];
    textureValues[3] = aMap[0,0,3];
  }
}

How to use use terrain texture data to trigger different footstep sounds

What’s great about this method is that, unlike a tag, it doesn’t just return a true or false value. It’s a mix value, between 0 and 1 that can be used for more natural sounding footstep sounds.

To create natural landscapes, textures on terrain objects are often blended together. There’s often no hard division between one surface and another. You might be standing on 80% stone and 20% sand, or 100% grass, or 50/50 mud and rocks.

It’s possible to blend footstep sound effects in the same way for a really effective footsteps system.

Painting Terrain Textures in Unity

Terrain textures are often smoothly blended together. Footstep sounds can be too!

Here’s how to do it…

  • Get the terrain alpha map values by using the method above.
  • If any texture mix value is more than 0, play a footstep sound from an array of clips for that material.
  • Trigger it with Play One Shot, not Play (which takes a volume scale parameter).
  • Pass in the texture mix value for the Volume Scale parameter of Play One Shot to blend the sound with any others that are playing.

This will smoothly blend the footstep clips together depending on what the player’s standing on. Because it uses a volume scale, you will still be able to adjust the total volume of all the footsteps on the Audio Source if you need to.

Here’s an example of how it looks in scripting:

public AudioSource source;

public AudioClip[] stoneClips;
public AudioClip[] dirtClips;
public AudioClip[] sandClips;
public AudioClip[] grassClips;

AudioClip previousClip;

public void PlayFootstep()
{   
  GetTerrainTexture();

  if (textureValues[0] > 0)
  {
    source.PlayOneShot(GetClip(stoneClips), textureValues[0]);
  }
  if (textureValues[1] > 0)
  {
    source.PlayOneShot(GetClip(dirtClips), textureValues[1]);
  }
  if (textureValues[2] > 0)
  {
    source.PlayOneShot(GetClip(dirtClips), textureValues[2]);
  }
  if (textureValues[3] > 0)
  {
    source.PlayOneShot(GetClip(dirtClips), textureValues[3]);
  }
}

AudioClip GetClip(AudioClip[] clipArray)
  {
    int attempts = 3;
    AudioClip selectedClip = 
    clipArray[Random.Range(0, clipArray.Length - 1)];

    while (selectedClip == previousClip && attempts > 0)
      {
        selectedClip = 
        clipArray[Random.Range(0, clipArray.Length - 1)];
        
        attempts--;
      }

    previousClip = selectedClip;
    return selectedClip;

  }

Now I want to know how you’re using terrain in your game. Do you have your own unique challenges when it comes to detecting surface types? Does this system work for your game? or maybe you’re using alpha maps in some other weird and wonderful way.

Whatever you’re up to, let me know by leaving a comment below.

Comments

  1. Avatar

    I was looking for a way to detect textures within a terrain for my footstep system. I’m happy that I found your blog post. Thank you for taking the time to explain your solution and share your code.

    Cheers!

  2. Avatar

    This was incredibly useful, thanks for taking the time to post this – it has helped me build a footstep system to recognise both tagged objects and terrain textures using FMOD.

Leave a Comment