Hey, thanks for sharing this.
Yesterday I was kinda bored so I turned this into a little module so I can use it on players/npc’s (it’s by no means perfect, but it gets the job done), so here it is in case it helps anyone:
Preview:
File:
GlintStep.rbxl (97.0 KB)
Code Preview:
--!strict
--[[
@class GlintStep
Utility class that handles character foot planting.
@constructor new
Creates a new GlintStep instance for a given character.
@param Character Model - The character model.
@param Active boolean? - Whether foot planting should start active.
@return Glint - The new GlintStep instance.
@field Active boolean
Indicates whether foot planting is currently active.
@method Toggle
Activates or deactivates foot planting.
@param Active boolean - The new state of foot planting.
@return nil
@method Destroy
Cleans up the instance and removes all associated connections.
@return nil
--]]
----- Services -----
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")
----- Variables -----
local Code = ReplicatedStorage:WaitForChild("Code")
local Shared = Code.Shared
local Resources = Shared.Resources
local Types = require(script.Parent.Types)
local Trove = require(Resources.RbxUtil.Trove)
local MAX_SLOPE_ANGLE = math.rad(45) --Maximum angle the foot planting will apply to.
local TARGET_TRESHOLD_STANDING = 0.1 --Distance between the current point and the previous required before an update.
local TARGET_TRESHOLD_MOVING = 0.05
local SMOOTH_TIME_STANDING = 0.01 --Transition time to be applied to the footplanting as it aligns to the floor.
local SMOOTH_TIME_MOVING = 0.001
local WEIGHT_STANDING = 0.75 --Weight between 0 and 1 to be applied to the footplanting as it aligns to the floor.
local WEIGHT_MOVING = 0.45
local HEIGHT_STANDING_MULTIPLIER = 1 --Maximum distance the footplanting raycast will reach when aligning to the floor based on the character's height.
local HEIGHT_MOVING_MULTIPLIER = 0.4
----- Module -----
local GlintStep = {}
GlintStep.__index = GlintStep
--- Types ---
export type Glint = {
Active: boolean,
Toggle: (self: Glint, Active: boolean) -> (),
Destroy: (self: Glint) -> ()
}
type InternalGlint = Glint & {
Character: Types.Character,
Trove: Trove.Trove,
RayParams: RaycastParams,
LeftIKControl: IKControl,
RightIKControl: IKControl,
LastTargetPositions: {
Left: Vector3,
Right: Vector3
}
}
--- Methods ---
-- Constructor --
function GlintStep.new(Character: Types.Character, Active: boolean?): Glint
assert(Character, "Character passed is nil.")
local self: InternalGlint = {} :: any
--Properties
self.Character = Character
self.Trove = Trove.new()
self.Active = false
self.LastTargetPositions = {
Left = Vector3.zero,
Right = Vector3.zero
}
--Ray Params
self.RayParams = RaycastParams.new()
self.RayParams.FilterDescendantsInstances = {self.Character :: any}
self.RayParams.FilterType = Enum.RaycastFilterType.Exclude
--IK Controls
self.LeftIKControl = CreateIKControl(Character, "Left")
self.RightIKControl = CreateIKControl(Character, "Right")
--Handle cleanup
self.Trove:Add(self.Character:GetPropertyChangedSignal("Parent"):Connect(function(): ()
if not Character.Parent then
GlintStep.Destroy(self)
end
end) :: RBXScriptConnection)
--Toggle if requested
if Active then
GlintStep.Toggle(self, Active)
end
return setmetatable(self, GlintStep) :: any
end
-- Toggle --
function GlintStep.Toggle(self: InternalGlint, Active: boolean): ()
if Active ~= self.Active then
self.Active = Active
--Toggle
if Active then
--Update IKControl
local function UpdateIKControl(Side: "Left" | "Right"): ()
local IKControl: IKControl = self[Side.."IKControl"]
local Hip: Motor6D = self.Character[Side.."UpperLeg"][Side.."Hip"]
local Knee: Motor6D = self.Character[Side.."LowerLeg"][Side.."Knee"]
local Ankle: Motor6D = self.Character[Side.."Foot"][Side.."Ankle"]
local HipJointWorldCFrame = assert(Hip.Part0).CFrame * Hip.C0
local KneePart1CF = HipJointWorldCFrame * Hip.Transform * Hip.C1:Inverse()
local FootPart1CF = KneePart1CF * Knee.C0 * Knee.Transform * Knee.C1:Inverse()
local Reverse = FootPart1CF * Ankle.C0 * Ankle.Transform * Ankle.C1:Inverse()
local GoalTargetWorldCF = Reverse * assert(IKControl.EndEffector :: Attachment).CFrame
local Moving = self.Character.Humanoid.MoveDirection.Magnitude > 0
local Height = (0.5 * self.Character.HumanoidRootPart.Size.Y) + self.Character.Humanoid.HipHeight
local Treshold = Moving and TARGET_TRESHOLD_MOVING or TARGET_TRESHOLD_STANDING
local RayDistance = Moving and (Height / HEIGHT_MOVING_MULTIPLIER) or (Height / HEIGHT_STANDING_MULTIPLIER)
IKControl.Weight = Moving and WEIGHT_MOVING or WEIGHT_STANDING
IKControl.SmoothTime = Moving and SMOOTH_TIME_MOVING or SMOOTH_TIME_STANDING
--Raycast
local Origin = HipJointWorldCFrame.Position + Vector3.new(0, self.Character[Side.."Foot"].Size, 0)
local RayDist = Moving and self.Character.Humanoid.HipHeight or self.Character.Humanoid.HipHeight * 3
local Direction = (GoalTargetWorldCF.Position - Origin).Unit * RayDist
local Result = workspace:Raycast(Origin, Direction, self.RayParams)
if Result then
local SlopeAngle = math.acos(Result.Normal:Dot(Vector3.yAxis))
local LastPosition: Vector3 = (self.LastTargetPositions :: any)[Side]
if (SlopeAngle <= MAX_SLOPE_ANGLE) and ((Result.Position - LastPosition).Magnitude > Treshold) then
(self.LastTargetPositions :: any)[Side] = Result.Position
local RightVector = self.Character.HumanoidRootPart.CFrame.LookVector:Cross(Result.Normal).Unit
local ForwardVector = Result.Normal:Cross(RightVector).Unit
local GoalTargetWorldCF = CFrame.fromMatrix(Result.Position, RightVector, Result.Normal, ForwardVector)
assert(IKControl.Target :: Attachment).WorldCFrame = GoalTargetWorldCF
end
IKControl.Enabled = true
else
IKControl.Enabled = false
end
end
--Run
self.Trove:Add(RunService.PreRender:Connect(function(dt: number)
if self.Character.Humanoid.Health > 0 then
UpdateIKControl("Left")
UpdateIKControl("Right")
end
end))
else --Cleanup
self.Trove:Clean()
end
end
end
-- Destroy --
function GlintStep.Destroy(self: InternalGlint): ()
self:Toggle(false)
task.defer(function()
self.Trove:Destroy()
setmetatable(self, nil)
table.clear(self :: {any})
table.freeze(self :: {any})
end)
end
--- Internal Methods ---
-- CreateIKControl --
function CreateIKControl(Character: Types.Character, Side: ("Left" | "Right")): IKControl
local IKTarget = Instance.new("Attachment")
IKTarget.Name = Side.."IKTarget"
IKTarget.WorldCFrame = Character[Side.."Hand"].CFrame + Vector3.new(0, -0.75, 0)
IKTarget.Parent = Character.HumanoidRootPart
local IKControl = Instance.new("IKControl")
IKControl.Name = Side.."IKControl"
IKControl.Type = Enum.IKControlType.Transform
IKControl.EndEffector = Character[Side.."Foot"][Side.."FootAttachment"]
IKControl.Target = IKTarget
IKControl.ChainRoot = Character[Side.."UpperLeg"][Side.."HipRigAttachment"]
IKControl.Parent = Character.Humanoid
return IKControl
end
return {
new = GlintStep.new :: (Character: Model, Active: boolean?) -> Glint
}
You can probs get better results by tweaking the values and the raycast, but this was alright enough for me.
Edit: I’ve also added an npc example so you can test the module on npc’s (as you can see it looks kind of rough on some rigs atm, so def play around with the weight value during movement and such if you plan to use this):
Edit 2: I’ve played around with this a tiny bit more and I think I improved it slightly?
- It now uses the Avatar Joint Upgrade (in fact, try it with IsKinematic set to false, it should still work).
- Shapecasts are used for surface detection.
- IKConstraint’s weight is now lerped so the thing is less stuttery.
- I’ve added a “foot lock” so the foot doesn’t move around when you idle, dance and such (not perfect, it uses a torso/foot threshold, but it usually works).
It doesn’t seem to beheave the same between the player and the example rig I previously made, but the joint upgrade isn’t applied to general rigs yet, so it could just be that I applied the constraints to mine wrong, you’ll have to play around with your avatars and the values to figure what fits the best.
Preview:
Code:
GlintStep 1.1.rbxl (70.5 KB)