Part being offset from the ground doesn't navigate inclines smoothly

i have some arbitrary part that’s offset from the ground a certain distance, by having constant acceleration applied to it to cover the error in height under t time. it simply adds the const accel eq on top of workspace gravity. a vector force is being used here.

local aUp = g + 2 * (d - v.Y * t) / t^2

local max_accel = math.sqrt(2 * g * d)
local max_impulse = math.max((max_accel - v.Y) * 60, 0)
        
aUp = math.min(max_impulse, aUp)

that all works fine, but i’ve been struggling to figure out how it could smoothly travel up and down inclines without jumping off or going into them. i know why it happens, but i haven’t nailed down a solution myself. i’ve tried projecting the velocity onto the normal to get a perpendicular velocity and then extracting the components of the distance between that and the current velocity, which was the closest i’ve gotten, but to little avail.

local vdot = normal:Dot(v)
local vinto = normal * vdot
local vperp = v - vinto

local distance = vperp - v
local vspeed = distance / t

local aX, aZ = vspeed.X, vspeed.Z

aUp += math.min(0, vspeed.Y)

local aF = Vector3.new(0, aUp, 0)
        
force.Force = aF * m

this is going to be part of a custom character controller i’m working on. any help would be appreciated (the clip in the attachment only uses the aUp component)

still actively looking for solutions on this, there doesn’t seem to be many pertinent resources online to it

I think the issue is that your aUp variable always points up and you want it to change depending on the slope because obviously if you move up said slope the new position you are at is higher than you’d expect for a flat piece of terrain.

You need to factor in the normal of the surface you are ontop of. Right now in order to get the force applied per frame you are just plugging aUp into the Y coordinate. I’d try getting the velocity along the normal through doing normal:Dot(v), then instead of calculating aUp with v.Y use said velocity-along-normal and also sub out the d variable for a vector version of distance.

Finally when you’re applying the force apply normal * accel (previously aUp) * m and that should work I think.

(sorry for formatting, I am on a phone right now so I can’t do codeblocks :upside_down_face:)

applying the force along the normal on any non-flat surface only causes further instability in my case. the minimum force applied is always g, assuming there’s no error in distance/height.

this is the closest i’ve gotten, but it results in flinging when the y component of velocity is high enough

local aUp = g + 2 * (d - v.Y * t) / t^2
		
local max_accel = math.sqrt(2 * g * d)
local max_impulse = math.max((max_accel - v.Y) * 60, 0)
		
aUp = math.min(max_impulse, aUp)
		
local vcontrol = Vector3.new(v.X, 0, v.Z)
local vdot = normal:Dot(vcontrol)
local vinto = normal * vdot
-- the control velocity tangential to the slope
local vperp = vcontrol - vinto
		
local vchange = vperp - vcontrol
local vspeed = vdot / 2.5 * (vchange / t)
		
aUp += vspeed.Y
		
local vX = 0
local vZ = 0
		
local aF = Vector3.new(vX, aUp, vZ)

force.Force = aF * m

do you have any metrics on what “high enough” looks like? I’d also assume it would fling upwards along Y but correct me if that’s wrong.

nevermind! I think you are accidentally creating a quadratic.

When you calculate vspeed you are using vdot and vchange. vchange looks to be -vinto which is just vdot with a direction.

When you look at the components of vspeed it works out to be velocity^2/time. You are ultimately doing vdot * vdot to calculate the resulting size as the size of the vector vchange is just vdot.

Combine this with the fact that the clamp to max_impulse happens before you add vspeed.Y to aUp and you are adding an unclamped exponential to your upwards velocity.

good catch on vchange just being -vinto. unfortunately its still a no go even with your suggestions added

the goal of this is to simulate the unexposed force in the humanoid that keeps the rootpart off the ground with hipheight, but i just can’t nail it down no matter how close i get

I think you should try using some form of PD controller. The current solution you use is trying to correct in a single physics tick which inherently doesn’t work for non flat surfaces (every frame the Y would be jumping to the next position, this obviously causes jitter if the Y value it jumps too isn’t constant). I believe the built in HipHeight uses a feedback system similar to a PD controller, it might even be a stock PD controller I haven’t looked at it in a very long time.

When I suggested calculating using the normal that would just be a sort of patch to this issue the more I think about it. It was trying to compensate for the change in d along a slope but realistically it wouldn’t work with varied terrain

Basically try correcting relative to the error instead of all at once in a single frame. You can make your PD controller pretty aggressive just try to prevent it from overshooting and don’t try to correct in a single update.

I’m hoping the quadratic issue I pointed out solved the Y flinging because otherwise I’m not sure what caused it. In my mind if the flinging issue is fixed and the only issue is it’s jittery and unstable that is probably caused by trying to solve the distance 100% correctly in a single impulse

t is equal to 1/20th of a second, or 1 / 20. it being a constant that’s less than dt is what doesn’t cause idle jitter on flat surfaces. same case for multiplying by 60 instead of dividing by dt here

local max_impulse = math.max((max_accel - v.Y) * 60, 0)

if vcontrol is equal to horizontal velocity, then multiplying vspeed by some constant and adding the resulting y component to aUp is what leads to my most stable behavior yet. the original reference i used when i first started this had control velocity equal to Vector3.new(v.X, 0, v.Y) and it didn’t quite work correctly, but its around 8 years old at this point. its an old repo that serves as a replacement to humanoids written entirely in luau. it might’ve worked as intended at its time though
https://github.com/LPGhatguy/luanoid/blob/master/src/core/CharacterState/Walking.lua

The bottom line here is that to get the optimal balance of smoothness and responsiveness, you need to insure that your control system is operating close to being critically damped. If your per-frame corrections are too strong, you end up with an underdamped system that can oscillate (jitter) and even blow up (oscillations going quickly to huge forces and flinging you). If you make small corrections, your system will be overdamped and it will be stable and jitter-free, but it will be noticeably slow at correcting abrupt changes in target height, which will feel laggy, sluggish, unresponsive, etc.

This is challenging to do if you’re chosing to control things with a force computed per-frame in Lua, rather than using the right built-in constraint type to maintain the desired offset. Roblox gives you a whole set of physics constraints that already have tunable PD controllers that run at the true and full physics step rate.

You can come pretty close to the behavior of built-in constraints if you make use of the dt provided to you by the RunService event. If you pick an arbitrary fixed timestep, you’re basically forced to clamp your per-frame corrections and make your system very overdamped, so that it won’t oscillate or blow up (fling you) in even the worst case of the game having a very-long physics step. This will prevent flinging, but at the cost of the system’s response time being very slow to correct position.

Your options are basically to implement a proper PD or PID controller in Lua, to control the VectorForce and tune it to be near to critical damping, or you can use a different physics contraint that will let you only have to consider the target position/height and let the constraint’s PD controller handle all the force computation and application. This second approach is more likely to give the best results, but if you’re whole objective is to see if you can do this all with forces in Lua, it’s possible, it’s just a much harder problem to build and tune a PD controller from scratch.

update: i implemented a physics substep algorithm straight from dthecoolest’s physics character controller, and while it’s gotten a closer desired result, it’s still not quite there. i’ve looked at the provided place file up and down, but i cannot spot any differences between it and my own, besides the fact that i’m not using other vectorforce instances for XZ velocity, rather writing to ALV directly. that’s very likely what it comes down to though, and i may just opt for that instead

what i’ve done:

local ground_height = result.Position.Y
local target_height = ground_height + OFFSET
		
local d = target_height - current_height
local aUp = GetConstAcceleration(d, v.Y, t)
		
local fUp = aUp * m
		
local function Substep(f, vY, step, height)
	local netForce = f - weight
			
	local vYNew = ToYVelocity(netForce, m, vY, step)
	local dYNew = ToYDisplacement(netForce, m, vY, step)
			
	local fNew = GetConstAcceleration(target_height - (height + dYNew), vYNew, step) * m
			
	local avgV = (vY + vYNew) / 2
	local avgF = (fNew + f) / 2
	local avgH = height + dYNew / 2
			
	return avgF, avgV, avgH
end
		
local iForce = fUp
local iYVelocity = v.Y
local iHeight = current_height
		
local n = 3
local step = dt / n
		
for _ = 1, n - 1 do
	iForce, iYVelocity, iHeight = Substep(iForce, iYVelocity, iHeight, step)
end
		
fUp = iForce
		
force.Force = Vector3.new(0, fUp, 0)