How to Create a Visual Footstep Trail for your Character

Hey, this is my first community tutorial on the developer forum. I want to share how you can create a visual footstep trail for your game on Roblox. This tutorial will also feature how you can create different footstep traces depending on what material/environment you’re standing on.

https://gyazo.com/7abdd7c19c211c705269b9cba2d385c8

Requirements of knowledge:

- Moderate to advanced CFrame knowledge
-Intermediate vector understanding
-Runservice
-Raycasting
-RemoteFunctions

Step 1.
Start by creating your visual footstep trace. Make sure it’s anchored and cancollide is set to false.

Step 2.
Create a Folder, insert your visual footstep inside the Folder, and place your folder in ReplicatedStorage.

Create a client script and insert it into the StarterPlayerscripts, and then create a Serverscript and insert it into the Serverscriptservice

Create a RemoteFunction and insert it into Replicatedstorage.

Tutorial

Step 3.
Open the client script and bind a Renderstepped to your Step Function. The purpose of this function is to detect character movement and the Distance between each footstep.

local function Step()

RunService:BindToRenderStep("Step",Enum.RenderPriority.Character.Value,Step) --Binding a renderstepped and setting the render priority to the Character in order to ensure it runs every character update.

Great! We have a loop! Now we need to detect character movement and calculate the distance moved to ensure footsteps spawn a certain number of studs apart.

Now how do we do this?
We can achieve movement detection by using the Humanoid MoveDirection property.

When moveDirection isn’t 0 on the Z and X axis. the character is essentially moving.

if Humanoid.MoveDirection.X ~= 0 or Humanoid.MoveDirection.Z ~= 0 then  

The Humanoid property
682c622fc787f211ad1876e71049a522

We also need to check the Distance between each movement. Now how do we do that? By simply using Magnitude. (A magnitude is essentially the Distance between two points in a 3D Space calculated by using the Pythagorean theorem.)

magnitude_im1 A visual representation of a Magnitude in 3D space.

2D space

This is how you calculate can calculate magnitude. By subtracting the positions of two parts we get the Direction, Then we simply type “.magnitude” for Roblox to calculate the Magnitude.

local Magnitude = (Part1.Position-Part2.Position).magnitude

Now we should have something like this. We’re now able to detect Humanoid movement and measure the Distance between each footstep.

local Studs = 2
local function Step()
	if Humanoid.MoveDirection ~= Vector3.new(0,0,0) then--Checking whether we are moving
		local Magnitude = ((PreviousPosition or HumanoidRootPart.Position)-HumanoidRootPart.Position).magnitude --Getting the distance from the previous position
		if Magnitude >= Studs or PreviousPosition == nil then --Confirming whether we've moved 2 studs or not
			PreviousPosition = HumanoidRootPart.Position --Setting a new previous position(We're essentially setting a new previous position each 2 studs in order to check the distance from the last standing point
			SpawnFootstep() --Calling our  spawnFootstep function 
		end
	end
end
RunService:BindToRenderStep("Step",Enum.RenderPriority.Character.Value,Step) --Binding a renderstepped

Step 4.
To spawn our visual footstep trace we need a Position, an orientation, and what Material we’re currently standing on. Now our CFrame knowledge and Raycasting knowledge have become useful.

Now, we will perform a Raycast from the corresponding foot to detect the ground or surfaces. First, we need to create Raycast parameters to filter out certain objects, ensuring the Raycast only interacts with relevant surfaces.

local Rayparams = RaycastParams.new()
Rayparams.FilterDescendantsInstances = {Character,table.unpack(FootStepArray)} --Filtering out the character **and a table of our previous spawned visual footsteps(We'll come to that part soon)**
Rayparams.FilterType = Enum.RaycastFilterType.Exclude

Great!, now we can Raycast down from the characters foot.

How do we get the raycasting Direction? By essentially just subtracting two Vectors to find the Vector in between two points.

local Origin = Foot.Position+Vector3.new(0,0.1,0) --Setting the Origin of the raycast 0.1 studs above the foot
local Direction = (Foot.Position-Origin) --Getting the Direction by subtracting the positions (End Position- Start Position = Direction

Now we need to calculate our desired orientation of the surface we hit. To do this, we will determine the surface’s Normal and align it with the part.

A “normal” is a vector that is perpendicular to a surface, indicating the direction that the surface faces.

This image shows what a normal is.
NormalVector_700

This is how we get the ray Normal.

**ray.Normal**

To align the footstep trace with the character’s movement direction, we need to obtain the character’s lookvector, which indicates the direction the character is facing. This vector will ensure the footstep is properly aligned.
To calculate our desired CFrame of the surface we need to cross vectors to get a vector product.

How do we get an aligned vector relative to the characters lookvector and the normal of the hit surface? In order to achieve this we would need to cross these two vectors.

A brief explanation of Vector crossing.

This is a visual representation of Vector crossing
Vector cross product results in a new vector that is perpendicular to the plane formed by the original two vectors, with a direction determined by the right-hand rule.

In the image we’re crossing A and B in order to get C.
righthand

Ray.Normal is the UpVector that we hit The blue arrow shows the normal.

This is the visual representation of the characters lookvector

Great! Now we’ve got 2 vectors that we can cross to create the product vector.

 HumanoidRootPart.CFrame.LookVector:Cross(UpVector)

The red arrow is the crossed product, This will act as our RightVector for our matrix.


The job of the RightVector is to point the footstep trace accordingly to the character’s LookVector.

Okay, but why did we do all of this? The reason why we need these vectors is because we’re going to create a matrix from the part by providing a RightVector and an UpVector. A matrix will allow us to orient the footstep trace according to these Vectors.

A matrix takes 4 arguments. The first argument is the origin Position, which is self-explanatory.
The three other arguments are the RightVector, the UpVector and the LookVector **The last argument is optional.

CFrame.fromMatrix(Position,RightVector,UpVector,LookVector)

If you’ve no idea what a Matrix is, I would recommend this tutorial.

Now let’s create a matrix! By providing the ray’s position and the ForwardVector(RightVector) and the UpVector(ray.normal)

local UpVector = ray.Normal
local ForwardVector = HumanoidRootPart.CFrame.LookVector:Cross(UpVector)  --Crossing the LookVector and the UpVector(ray.normal)

local CF = CFrame.fromMatrix(ray.Position,ForwardVector,UpVector)  

return CF,ray.Material.Name or ray.Instance.Material.Name --Returning the CFrame and the Material

–This is a visual representation of a part adjusting to the blue and red arrow directions by rotating the part according to the directions.

Now we should have something like this.

local function GetSurfaceOrientation(Foot)
	local Character = Foot.Parent

	local Rayparams = RaycastParams.new()
	Rayparams.FilterDescendantsInstances = {Character,table.unpack(FootStepArray)}
	Rayparams.FilterType = Enum.RaycastFilterType.Exclude

	local Origin = Foot.Position+Vector3.new(0,0.1,0)
	local Direction = (Foot.Position-Origin)
	local ray = workspace:Raycast(Origin,Direction.Unit*3,Rayparams)

	if ray then --Making sure we've successfully raycasted
		local UpVector = ray.Normal


		local ForwardVector = HumanoidRootPart.CFrame.LookVector:Cross(ray.Normal) 

		local CF = CFrame.fromMatrix(ray.Position,ForwardVector,UpVector)  

		return CF,ray.Material.Name or ray.Instance.Material.Name
	end
end

Step 5.

Now we have the Material of what we’re standing on and our desired CFrame.
Let’s now assign footstep attributes to determine which foot should Raycast

if Character:GetAttribute(Footstep) == nil then Character:SetAttribute(Footstep,RightFoot) end--If there isn't a footstep attribute then create one|

Now, I am calling the RemoteFunction that I set up earlier. I will be sending the CFrame and our desired FootstepType. After the footstep is spawned on the server, I am returning the footstep to filter it out for upcoming raycasts. Additionally, I am clearing our FootStepArray each time a footstep is spawned by calling our ClearTable function to remove unnecessary variables from the array.

local FootstepArray = {}

local function SpawnFootstep()
	if Character:GetAttribute("Footstep") == nil then Character:SetAttribute("Footstep","RightFoot") end--If there isn't a footstep attribute then create one
	
	local Cframe,Material = GetSurfaceOrientation(Character:FindFirstChild(Character:GetAttribute("Footstep")))
	if Material == nil or Cframe == nil then return end --Making sure we got a valid result

	--Footstep
	local Footstep = FootstepFolder:FindFirstChild(Material.."Footstep") --Making sure we find the footstep

	if Footstep  == nil then return end --Checking whether we found a footstep 
	local Step = FootstepEvent:InvokeServer(Footstep,Cframe) --Passing the calculation and the cframe

	table.insert(FootStepArray,Step) --Inserting the visual footstep we've just created into our array in order to filter our the footsteps for future raycasts.
	ClearTable() --Clearing
	
--Settings the a new **FootAttribute** each time we walk
	local FootAttribute = Character:GetAttribute("Footstep")
	if FootAttribute == "RightFoot" then--Setting the every second footstep
		Character:SetAttribute("Footstep","LeftFoot")
	elseif FootAttribute == "LeftFoot" then
		Character:SetAttribute("Footstep","RightFoot")
	end
end

Now let’s take a look at our Serverscript.

<Serverscript
local FootstepEvent = game.ReplicatedStorage:WaitForChild("RemoteFunction")
local FootstepFolder = game.ReplicatedStorage:WaitForChild("Footsteps")

local Debris = game:GetService("Debris")

FootstepEvent.OnServerInvoke = (function(Player,Footstep,cf)
	local HumanoidRootPart = Player.Character.HumanoidRootPart
	if Footstep ~= nil then --Making sure our Footstep isn't nil
--Creating the footstep
		local FootstepClone = Footstep:Clone()
		FootstepClone.CanCollide = false
		FootstepClone.CFrame = cf
		FootstepClone.Anchored = true
		FootstepClone.Parent = game.Workspace
		Debris:AddItem(FootstepClone,5) --Removing it after 5 seconds
	end
end)

Our final result should look something like this:
https://gyazo.com/7abdd7c19c211c705269b9cba2d385c8

Here is the place file:
FootStepTracer.rbxl (65.9 KB)

As mentioned earlier, this is my first tutorial on the developer forum, so I appreciate all the feedback I can get. Please correct me if I made any errors.

I would appreciate it if you answered my poll :grinning:

Did you find this tutorial useful?

  • Yes
  • No

0 voters

Client script
local RunService = game:GetService("RunService")

--Player
local Players = game.Players
local Player = Players.LocalPlayer
local Character = Player.Character or Player.CharacterAdded:Wait()
local HumanoidRootPart = Character:WaitForChild("HumanoidRootPart")

local Humanoid = Character:WaitForChild("Humanoid")


local FootstepEvent = game.ReplicatedStorage:WaitForChild("RemoteFunction")

local FootstepFolder = game.ReplicatedStorage:WaitForChild("Footsteps") 


local Studs = 3

local PreviousPosition = nil

local FootStepArray = {}

local function ClearTable()
	for i,v in pairs(FootStepArray) do
		if v.Parent == nil then
			FootStepArray[i] = nil
		end
	end
end


local function GetSurfaceOrientation(Foot)
	local Character = Foot.Parent

	local Rayparams = RaycastParams.new()
	Rayparams.FilterDescendantsInstances = {Character,table.unpack(FootStepArray)}
	Rayparams.FilterType = Enum.RaycastFilterType.Exclude

	local Origin = Foot.Position+Vector3.new(0,0.1,0)
	local Direction = (Foot.Position-Origin)
	local ray = workspace:Raycast(Origin,Direction.Unit*3,Rayparams)

	if ray and ray.Position then
		local UpVector = ray.Normal
		local Point = (HumanoidRootPart.Position-ray.Position).Unit
		
		local RightVector = UpVector:Cross(Vector3.new(0,1,0)) 
		local ForwardVector = HumanoidRootPart.CFrame.LookVector:Cross(ray.Normal) 

		local CF = CFrame.fromMatrix(ray.Position,ForwardVector,UpVector)  

		return CF,ray.Material.Name or ray.Instance.Material.Name
	end
end


local function SpawnFootstep()
	if Character:GetAttribute("Footstep") == nil then Character:SetAttribute("Footstep","RightFoot") end--If there isn't a footstep attribute then create one
	
	local Cframe,Material = GetSurfaceOrientation(Character:FindFirstChild(Character:GetAttribute("Footstep")))
	if Material == nil or Cframe == nil then return end --Making sure we got a valid result

	--Footstep
	print(Material)
	local Footstep = FootstepFolder:FindFirstChild(Material.."Footstep") --Making sure we find the footstep

	if Footstep  == nil then return end --Checking whether we found a footstep 
	local Step = FootstepEvent:InvokeServer(Footstep,Cframe) --Passing the calculation and the cframe
	table.insert(FootStepArray,Step)
	ClearTable()
	
	local FootAttribute = Character:GetAttribute("Footstep")
	if FootAttribute == "RightFoot" then--Setting the every second footstep
		Character:SetAttribute("Footstep","LeftFoot")
	elseif FootAttribute == "LeftFoot" then
		Character:SetAttribute("Footstep","RightFoot")
	end
end


--This function is responsible for checking the distance between each step
local function Step()
	if Humanoid.MoveDirection.X ~= 0 or Humanoid.MoveDirection.Z ~= 0 then --Checking whether we are moving
		local Magnitude = ((PreviousPosition or HumanoidRootPart.Position)-HumanoidRootPart.Position).magnitude --Getting the distance from the previous position
		if Magnitude >= Studs or PreviousPosition == nil then --Confirming whether we've moved 2 studs or not
			PreviousPosition = HumanoidRootPart.Position
			SpawnFootstep()
		end
	end
end
RunService:BindToRenderStep("Step",Enum.RenderPriority.Character.Value,Step) --Binding a renderstepped

19 Likes

You go indepth with the calculations which is neat but… the security side of things is bad.
Nothing’s stopping an exploiter from simply running FootstepEvent:InvokeServer with any arguments they want also, sending CFrame data and then sending all the part data to each client isn’t very performant. I think the best way would be letting the client handle the footsteps for ALL players. It’d also be neat to incorporate footstep sounds to make them sync with the part’s appearing.

also, this is kinda odd as you could simply use a boolean for ex. Foot = false – left foot and Foot = true – right foot

7 Likes

How so not very performant? I am pretty sure RemoteFunctions are built in a way to handle multiple calls, so sending back the part data shouldn’t be a huge deal.

Wdym by " I think the best way would be letting the client handle the footsteps for ALL players. "

2 Likes

My focus here was to emphasize on calculation part.

1 Like

Yeah, I’m not sure why I didn’t use booleans lol.

1 Like

You wont need any networking making everything run smoothly for all players independant of their internet, and by not very performant i meant just this; you can have no networking and get a better result

2 Likes

Aha I see, however the goal here is to make the footsteps appear for other players also.

1 Like

Yes, thats the point, if the player does it for all other players then they will still see their footsteps. You dont need to do everything on the server for small things such as this

2 Likes

I am a bit confused here, if I make the footsteps spawn on the client it won’t appear for other players?

2 Likes

Then exploiters could manipulate the local script to create fake footsteps or other unwanted effects.

2 Likes

What he quite means is have the other clients handle the responsibility of visualizing footsteps for other players.

for example, your client handling the visual footsteps for other players within range.

Are you suggesting only spawning the footsteps for players in a certain distance to the character?

I’m suggesting to do the visual effects purely from the client, since you are able to get details of character positioning from other players in game

Well, as just mentioned, in that case exploiters might take advantage of that?

1 Like

The exploiter are not taking advantage of anything since it’s staying on the client, your client is tasked with visualizing the footsteps of other players and not replicating anything

1 Like

Well, they may visualize unwanted things then?

Thats… what you are doing.
If each player makes their own footsteps for other people there will be no problem

1 Like

They can do that with your code too. Except other’s would also see that

2 Likes

Aha I see, thanks for the feedback. I’ll make sure to fix that.

2 Likes

Hi, thanks for sharing all the info and other peoples replies…

Did u ever make the updates suggested above?