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.
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
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.)
A visual representation of a Magnitude in 3D space.
2D spaceThis 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.
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.
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
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