Slope physics for linear Velocity

Hii

So Basically im making a skateboarding game an decided to use linear velocity for movement, because its steady and customizable. Now with this i ran into a problem!.

Adding gravity was easy - I just used Plane Mode

But currently i need to add slope physics so it accelerates on slopes, how would i do that?

I know it involves probably a lot of math, but i just need a way to simulate it. Currently it goes up and a down slopes, but at a constant speed. My goal is to make it accelerate on downward slopes, and slow down on upward slopes, when eventually it starts going in the opposite direction - falls back to the ground.

To sum it all up what im looking for is realistic slope simulation.

I am not sure where to start from. All i know is that it will probably involve normal vectors and stuff like that.

Any help is appreciated, thanks a lot for reading!

1 Like

Hello!

Have you tried enabling CustomPhysicalProperties on the Part and perhaps increasing the density? I’m thinking there’s a chance that there is a deceleration on upward slopes, and an acceleration on upward slopes, but that the mass is so low compared to the linear velocity that you can hardly tell there’s a force of gravity acting on it when going up the slope!

I just tried it and it didnt work sadly. In the documentation it says that it always applies constant velocity, which is what im trying to compensate for

I’m not proficient on the implementation, but I do know some maths
Firstly theres something called gravitational potential energy, essentially this is what tire you when you walk up a flight of stairs, Or throw a ball up. This energy increases with height and can be calculated using

m * g * h

where

  • m object’s mass
  • g gravitational acceleration (unit/s^2)
  • h height, from a reference point

Second is kinetic energy, essentially how fast an object is moving, calculated using

0.5 * m * v^2 
  • m object’s mass
  • v object speed (unit/s)

Now in normal circumstance, where you dont put energy into a system, energy should be conserved.
which mean for example, let say you are on a ramp that is 10 meters high, At first you are still on the edge of the ramp having only gravitational potential energy, as you roll down the ramp, some of these get converted to kinetic energy , but the total of Ek and Ep must be the same. Once you are at the bottom all your energy are now kinetic, so to find the final velocity one might do

v = sqrt(2 * g * h)

where h is the height of the ramp, or 10 meters in this case

Now as for your case, you might compute the total energy every interval or something, Once you start rolling down a ramp, save the total energy somewhere, and as height changes set the velocity to the velocity computed by the formula, and change the direction of the velocity to be in the direction you want and parallel to the ground at a point

Alright, i made this, it sort of works but its sort of buggy, sometimes going up slopes, instead of down(Also the velocity has its RelativeTo propertie set to “Attachment0”)

local part = script.Parent
local linearVelocity = part:FindFirstChildOfClass("LinearVelocity")
if not linearVelocity then
	warn("LinearVelocity not found on the part!")
	return
end

local attachment = linearVelocity.Parent.Attachment
local MAX_SPEED = 50
local rayLength = 5
local FRICTION = 4
local SLOPE_INFLUENCE = 6

local currentVelocity = Vector3.new()

local function getGroundNormal()
	local rayParams = RaycastParams.new()
	rayParams.FilterDescendantsInstances = {part}
	rayParams.FilterType = Enum.RaycastFilterType.Blacklist
	local result = workspace:Raycast(part.Position, Vector3.new(0, -rayLength, 0), rayParams)
	return result and result.Normal or nil
end

local function getDownhillVelocity(normal)
	local horizontal = Vector3.new(normal.X, 0, normal.Z)
	local hLen = horizontal.Magnitude
	if hLen < 1e-5 then return nil end
	horizontal = horizontal.Unit
	local downhill = -horizontal
	local verticalFactor = 1 - normal.Y
	return downhill * MAX_SPEED * verticalFactor
end

local function worldToPlaneVelocity(worldVel)
	local localVel = attachment.CFrame:VectorToObjectSpace(worldVel)
	return Vector2.new(localVel.X, localVel.Z)
end

game:GetService("RunService").Stepped:Connect(function(_, dt)
	local slopeNormal = getGroundNormal()

	if slopeNormal then
		local slideVel = getDownhillVelocity(slopeNormal)
		if slideVel then
			currentVelocity = currentVelocity + slideVel * SLOPE_INFLUENCE * dt
		end
	else
		currentVelocity = currentVelocity:Lerp(Vector3.new(0,0,0), FRICTION * dt)
	end

	linearVelocity.PlaneVelocity = worldToPlaneVelocity(currentVelocity)
end)

Yo I made something but it requires the part to be anchored, and does not use linear velocity as I find collision and gravity annoying.

local lastpos = script.Parent.Position
st=tick()
local function createBeam(posA, posB, parent)
	-- Create Attachments
	local attachmentA = Instance.new("Attachment")
	local attachmentB = Instance.new("Attachment")

	-- Set positions 

	attachmentA.Position = posA
	attachmentB.Position = posB

	-- Parent attachments to a part (or create one)

	-- Parent attachments to the dummy part
	attachmentA.Parent = workspace.Terrain
	attachmentB.Parent = workspace.Terrain

	-- Create the Beam
	local beam = Instance.new("Beam")
	beam.Attachment0 = attachmentA
	beam.Attachment1 = attachmentB

	-- Beam properties (customize as needed)
	beam.Color = ColorSequence.new(Color3.fromRGB(255, 255, 255)) -- Red beam
	beam.Width0 = 5
	beam.Width1 = 5
	beam.Texture = "rbxassetid://16336479573"
	beam.Transparency = NumberSequence.new(0,0)
	beam.LightEmission = 1
	beam.Parent = workspace.Terrain
	beam.FaceCamera = true
	game.TweenService:Create(beam,TweenInfo.new(0.5),{["Width0"]=0,["Width1"]=0}):Play()
	game.Debris:AddItem(beam,0.5)
	game.Debris:AddItem(attachmentA,0.5)
	game.Debris:AddItem(attachmentB,0.5)
	game.Debris:AddItem(part,100)
end
local function createPart(posA)
	local part = Instance.new("Part")
	part.Anchored = true
	part.CanCollide = false
	part.Size = Vector3.new(4,4,4)
	part.Position = posA
	part.Parent = workspace
end
function lethalityMetric(radius, velocity, r_max, v_max)
	print(radius,velocity)
	-- Normalize size and velocity
	local sizeFactor = (radius^2) / (r_max^2)
	local velocityFactor = velocity / v_max

	-- Compute Lethality Score
	local LS = sizeFactor * velocityFactor

	-- Determine result
	if LS >= 1 then
		return LS * 100
	else
		local damagePercent = LS * 100  -- Interpolates from 0% to 100%
		return damagePercent
	end
end
--print("WORKED")
local pos = nil
local aa1 = Instance.new("Attachment")
local aa2 = Instance.new("Attachment")
aa1.Parent = script.Parent
aa2.Parent = script.Parent
local trail = Instance.new("Trail")
trail.FaceCamera = true

--trail.Lifetime = 1
--trail.Parent = script.Parent
--trail.Attachment1 = aa1
--trail.Attachment0 = aa2
--trail.LightInfluence = 0
--trail.LightEmission=1
--trail.Texture = "rbxassetid://16336479573"
--trail.TextureMode = Enum.TextureMode.Static
--trail.TextureLength = script.Parent.Size.X
--trail.Transparency = NumberSequence.new(0,0)
local Rparam = RaycastParams.new()
Rparam.FilterType = Enum.RaycastFilterType.Include
Rparam.FilterDescendantsInstances = {workspace.Map}
local list = {}
for _, v in pairs(game.Players:GetChildren()) do
	if v.Character  then
		table.insert(list,v.Character)
	end
end
table.insert(list,workspace.Map)
Rparam.FilterDescendantsInstances = list

script.Parent.Touched:Connect(function(hit)
	if hit:FindFirstAncestor("Map") and not script.Tween.Value then
		print("HIT")

		local raycastres = workspace:Spherecast(script.Parent.Position,script.Parent.Size.X/2,(script.Parent.Position- lastpos).Unit*1023,Rparam)
		if raycastres then
			script.Parent.Anchored = true
			script.Parent.Position = raycastres.Position+ Vector3.new(0,0,0)
			--task.wait(10)
			game.ReplicatedStorage.remote.breakr:FireAllClients(20000,script.Parent,raycastres.Position,raycastres.Normal,false)
			script.Parent:Destroy()
			script:Destroy()
		end
	end
end)
while script.Parent.Parent ~= nil and tick()-st<5 do
	pos = script.Parent.Position
	--print("WORKING")
	--workspace:sh
	--print((script.Parent.Position-lastpos))
	--clne.CFrame = CFrame.lookAlong(lastpos,script.Parent.AssemblyLinearVelocity)
	--local spc = script.Parent:Clone()
	--spc.Bullet:Destroy()
	--spc.Anchored = true
	--spc.CanCollide = false
	--spc.CanQuery = false
	--spc.CanTouch = false
	--spc.Size = Vector3.new(10,10,10)
	--spc.Name = "spc"
	--spc.Parent = workspace
	--createBeam(lastpos,lastpos+ (script.Parent.Position-lastpos))
	--aa1.Position = script.Parent.AssemblyLinearVelocity.Unit*Vector3.new(1,0,1):Cross(Vector3.new(0,1,0))*5*script.Parent.Size.X/2
	--aa2.Position = -script.Parent.AssemblyLinearVelocity.Unit*Vector3.new(1,0,1):Cross(Vector3.new(0,1,0))*5*script.Parent.Size.X/2
	local raycastres = workspace:Spherecast(lastpos,script.Parent.Size.X/2,(script.Parent.Position-lastpos),Rparam)
	createBeam(lastpos,script.Parent.Position,workspace.Terrain)
	if raycastres then
		--print(raycastres.Instance.Name)
		local hum = raycastres.Instance.Parent:FindFirstChildOfClass("Humanoid") or raycastres.Instance.Parent.Parent:FindFirstChildOfClass("Humanoid")
		if hum then
			if raycastres.Instance.Name == "Head" or raycastres.Instance:FindFirstAncestor("Head") then
				if lethalityMetric(script.Parent.Size.X*0.3048/2,script.Parent.AssemblyLinearVelocity.Magnitude*0.3048,0.02,1500) > 10 then
					
					game.ServerScriptService.Events.Fight.damageDealth:Fire(script.Value.Value,hum.Health)
					hum.Health =0
				else
					game.ServerScriptService.Events.Fight.damageDealth:Fire(script.Value.Value,lethalityMetric(script.Parent.Size.X*0.3048/2,script.Parent.AssemblyLinearVelocity.Magnitude*0.3048,0.02,1500)*2)
					hum.Health = hum.Health - lethalityMetric(script.Parent.Size.X*0.3048/2,script.Parent.AssemblyLinearVelocity.Magnitude*0.3048,0.02,1500)*2
				end
			else
				game.ServerScriptService.Events.Fight.damageDealth:Fire(script.Value.Value,lethalityMetric(script.Parent.Size.X*0.3048/2,script.Parent.AssemblyLinearVelocity.Magnitude*0.3048,0.02,1500))
				hum.Health = hum.Health - lethalityMetric(script.Parent.Size.X*0.3048/2,script.Parent.AssemblyLinearVelocity.Magnitude*0.3048,0.02,1500)
			end
		elseif raycastres.Instance:FindFirstAncestor("Map") then
			if (script.Parent.AssemblyLinearVelocity.Magnitude) then
				--createPart(raycastres.Position)
				--print("PENETRATW")
				script.Parent.Anchored = true
				script.Parent.Size = Vector3.new(10,10,10)
				--script.Parent.Color = Color3.new(1,0,0)
				script.Parent.Name = "BOB"
				script.Parent.Position = raycastres.Position+ Vector3.new(0,10,0)
				script.Parent.AssemblyLinearVelocity = Vector3.zero
				--task.wait(10)
				game.ReplicatedStorage.remote.breakr:FireAllClients(20000,script.Parent,raycastres.Position,raycastres.Normal,false)
				script.Parent:Destroy()
				break
			end
		end
	end
	
	lastpos = script.Parent.Position
	game["Run Service"].Heartbeat:Wait()
end

bob.rbxm (8.7 KB)

Yeah thats what im trying to achieve, but is there a way to do with linear velocity, because I need collisions, thanks a lot for the example

Well the one I sent earlier (bob.rbxm) does do collision, At least to the ground, Collision with other object would be just some raycast/shapecast. Heres the code, You can pick out some functions, and use them to calculate velocity, the speed or mag of velocity is just the function bob(y)

local part = script.Parent

local linearVelocity = part:FindFirstChildOfClass("LinearVelocity")

if not linearVelocity then

	warn("LinearVelocity not found on the part!")

	return

end

task.wait(3)

local attachment = linearVelocity.Parent.Attachment

local MAX_SPEED = 50

local rayLength = 10

local uk = 0.5

local SLOPE_INFLUENCE = 6



local currentVelocity = Vector3.new()



local function getGroundNormal()

	local rayParams = RaycastParams.new()

	rayParams.FilterDescendantsInstances = {part}

	rayParams.FilterType = Enum.RaycastFilterType.Blacklist

	local result = workspace:Raycast(part.Position+ Vector3.new(0,2,0), Vector3.new(0, -rayLength, 0), rayParams)

	if result then

		return result.Position,result.Normal

	else

		return nil

	end

end



local function getDownhillVelocity(normal)

	local horizontal = Vector3.new(normal.X, 0, normal.Z).Unit

	return horizontal

end



local function worldToPlaneVelocity(worldVel)

	local localVel = attachment.CFrame:VectorToObjectSpace(worldVel)

	return Vector2.new(localVel.X, localVel.Z)

end

local tot = script.Parent.Position.Y

local function bob(h)

	return math.sqrt(198*(tot-h))

end

local look = script.Parent.CFrame.RightVector

local function createBeam(posA, posB, parent)

	-- Create Attachments

	local attachmentA = Instance.new("Attachment")

	local attachmentB = Instance.new("Attachment")



	-- Set positions 



	attachmentA.Position = posA

	attachmentB.Position = posB



	-- Parent attachments to a part (or create one)



	-- Parent attachments to the dummy part

	attachmentA.Parent = workspace.Terrain

	attachmentB.Parent = workspace.Terrain



	-- Create the Beam

	local beam = Instance.new("Beam")

	beam.Attachment0 = attachmentA

	beam.Attachment1 = attachmentB



	-- Beam properties (customize as needed)

	beam.Color = ColorSequence.new(Color3.fromRGB(255, 255, 255)) -- Red beam

	beam.Width0 = 0.1

	beam.Width1 = 0.1

	beam.Texture = "rbxassetid://16336479573"

	beam.Transparency = NumberSequence.new(0,0)

	beam.LightEmission = 1

	beam.Parent = workspace.Terrain

	beam.FaceCamera = true

end

lst = Vector3.zero
function movePointToDistance(pointA, pointB, surfaceNormal, desiredDistance)
	-- Ensure the normal vector has a length of 1, which is crucial for dot product calculations.
	local unitNormal = surfaceNormal.Unit

	-- 1. Calculate the vector from the surface up to your point.
	local vectorBToA = pointA - pointB

	-- 2. Find the current shortest distance from the surface to your point.
	-- This is done by projecting vectorBToA onto the surface's normal vector.
	local currentDistance = vectorBToA:Dot(unitNormal)

	-- 3. Determine the total displacement needed.
	-- This is the difference between your target distance and the current distance.
	local displacementScale = desiredDistance - currentDistance

	-- 4. Calculate the movement vector by scaling the normal by the displacement amount.
	local movementVector = unitNormal * displacementScale

	-- 5. Apply the movement vector to the original point to get the final position.
	local newPosition = pointA + movementVector

	return newPosition
end
game:GetService("RunService").Stepped:Connect(function(_, dt)

	dt = dt

	local hit,slopeNormal = getGroundNormal()

	if lst == Vector3.zero then

		lst = Vector3.new(1,0,0)

	end

	if slopeNormal then

		script.Parent.CFrame = CFrame.lookAt(movePointToDistance(script.Parent.Position,hit,slopeNormal,0.5),movePointToDistance(script.Parent.Position,hit,slopeNormal,0.5) + script.Parent.CFrame.LookVector,slopeNormal)

		script.Parent.AlignOrientation.PrimaryAxis = slopeNormal

		script.Parent.Color = Color3.new(1, 0, 0.0156863)

		createBeam(script.Parent.Position,script.Parent.Position + slopeNormal,workspace.Terrain)
		if script.Parent.Position.Y >= tot then
			tot = script.Parent.Position.Y
			lst = getDownhillVelocity(slopeNormal)
			currentVelocity = lst*0.01
		else
			currentVelocity = lst*bob(script.Parent.Position.Y)
		end
		if currentVelocity == 0 then
			currentVelocity = lst*0.01
			print(currentVelocity)
		end
		--lst = getDownhillVelocity(slopeNormal)

		--print(lst)
		--print(currentVelocity)

		--00print(bob(script.Parent.Position.Y))

	else

		script.Parent.AlignOrientation.PrimaryAxis = script.Parent.Attachment.WorldAxis

		script.Parent.Color = Color3.new(1,1,1)

		currentVelocity = currentVelocity + Vector3.new(0,-198,0)*dt

	end
	print(currentVelocity)
	script.Parent.Position = script.Parent.Position + currentVelocity*dt

	--linearVelocity.VectorVelocity = currentVelocity

	--task.wait(1)

	--print(bob(tot-script.Parent.Position.Y)/script.Parent.AssemblyLinearVelocity.Magnitude)

end)

The current velocity is the velocity and you could maybe plug it in to the linear velocity.
bob.rbxm (8.7 KB)

1 Like

Alright yeah i did some changes and made it work with linear velocity! Thanks a lot for the help I really appreciate it

1 Like