Moving player along with parts without constraints

Hey everybod.

Recently, I stumped upon a problem - the player didn’t move with the part. :frowning:


And I absolutely HATE working with constraints… :rage: :sob:

So, I tried to make a system which would prevent that. it took just 2 hours it is easy tbh
And I want to share it with you!

1. How is it supposed to work?

Well, first things first, what are we even supposed to write in our script? How the heck is it going to work? Let's break it down into smaller steps.
  1. Get the part that the player is standing on.
    We can simply use raycast in order to do that.
  2. Is the part that we are standing on moving?
    By creating a variable for old position of the part, we can easily check that.
  3. Snap the player back on the part.
    :PivotTo() will do the thing.

Well, now we know what we are supposed to do. Let’s actually make this.

2. Scripting

Planning this was the easiest thing - now, let's script it. First of all, we will raycast to check what part we are standing on. (We will also make an indicator of the ray origin and destination) In ```StarterPlayer``` -> ```StarterCharacterScripts``` create a ```LocalScript``` and write this code:
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local char:Model = script.Parent
local player = Players.LocalPlayer
local rootPart:BasePart = char:WaitForChild("HumanoidRootPart")

if char:WaitForChild("Humanoid").RigType == Enum.HumanoidRigType.R15 then
	legHeight = char:WaitForChild("LeftUpperLeg").Size.Y
	legHeight += char:WaitForChild("LeftLowerLeg").Size.Y
	legHeight += char:WaitForChild("LeftFoot").Size.Y
else
	legHeight = char:WaitForChild("LeftLeg").Size.Y
end

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = {char}
params.IgnoreWater = true

--// these are for testing where the ray goes
local originPart = Instance.new("Part")
originPart.Transparency = 0.5
originPart.CanCollide = false
originPart.Anchored = true
originPart.Color = Color3.new(1, 0, 0)
originPart.Size = Vector3.new(1.5, 1.5, 1.5)
originPart.Parent = char

local directionPart = Instance.new("Part")
directionPart.Transparency = 0.5
directionPart.CanCollide = false
directionPart.Anchored = true
directionPart.Color = Color3.new(0, 1, 0)
directionPart.Size = Vector3.new(1.5, 1.5, 1.5)
directionPart.Parent = char

RunService.Heartbeat:Connect(function(dt)
	local origin = rootPart.Position
	originPart.Position = origin
	local destination = origin - (Vector3.new(0, legHeight + (rootPart.Size.Y / 2), 0))
	directionPart.Position = destination
	local result = workspace:Raycast(origin, destination-origin, params)
	
	if result then
		print(result)
	end
end)

This is what we should get!


Two parts representing the origin and destination and some stuff in console. We can see the instance, where did the raycast collide and blah blah blah…

Next, let’s try to detect if the part is moving or not. For that, we will make a variable oldPosition to represent the old position of the previousResult. (We will also check if the part CanCollide or not)

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local char:Model = script.Parent
local player = Players.LocalPlayer
local rootPart:BasePart = char:WaitForChild("HumanoidRootPart")

if char:WaitForChild("Humanoid").RigType == Enum.HumanoidRigType.R15 then
	legHeight = char:WaitForChild("LeftUpperLeg").Size.Y
	legHeight += char:WaitForChild("LeftLowerLeg").Size.Y
	legHeight += char:WaitForChild("LeftFoot").Size.Y
else
	legHeight = char:WaitForChild("LeftLeg").Size.Y
end

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = {char}
params.IgnoreWater = true

--// these are for testing where the ray goes
local originPart = Instance.new("Part")
originPart.Transparency = 0.5
originPart.CanCollide = false
originPart.Anchored = true
originPart.Color = Color3.new(1, 0, 0)
originPart.Size = Vector3.new(1.5, 1.5, 1.5)
originPart.Parent = char

local directionPart = Instance.new("Part")
directionPart.Transparency = 0.5
directionPart.CanCollide = false
directionPart.Anchored = true
directionPart.Color = Color3.new(0, 1, 0)
directionPart.Size = Vector3.new(1.5, 1.5, 1.5)
directionPart.Parent = char

local lastResult:RaycastResult = nil
local oldPosition = nil
RunService.Heartbeat:Connect(function(dt)
	local origin = rootPart.Position
	originPart.Position = origin
	local destination = origin - (Vector3.new(0, legHeight + (rootPart.Size.Y / 2), 0))
	directionPart.Position = destination
	local result = workspace:Raycast(origin, destination-origin, params)
	
	if lastResult ~= nil and result == nil then
		if lastResult.Instance:IsA("BasePart") then
			if lastResult.Instance.Position ~= oldPosition and lastResult.Instance.CanCollide then
				print("player slipped of the platform")
			end
		end
	elseif lastResult and result then
		if lastResult.Instance:IsA("BasePart") and result.Instance:IsA("BasePart") then
			if oldPosition ~= lastResult.Instance.Position and lastResult.Instance == result.Instance  and result.Instance.CanCollide then
				print("platform is moving")
			end
		end
	end	
	
	lastResult = result
	if result then
		if result.Instance:IsA("BasePart") then
			oldPosition = result.Instance.Position
		else
			oldPosition = nil
		end
	else
		oldPosition = nil
	end
end)


Cool.

But, here comes the hardest part - we have to snap the player to the platform.
No worries. I already did this for you.

local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local char:Model = script.Parent
local player = Players.LocalPlayer
local rootPart:BasePart = char:WaitForChild("HumanoidRootPart")

if char:WaitForChild("Humanoid").RigType == Enum.HumanoidRigType.R15 then
	legHeight = char:WaitForChild("LeftUpperLeg").Size.Y
	legHeight += char:WaitForChild("LeftLowerLeg").Size.Y
	legHeight += char:WaitForChild("LeftFoot").Size.Y
else
	legHeight = char:WaitForChild("LeftLeg").Size.Y
end

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = {char}
params.IgnoreWater = true

--// these are for testing where the ray goes
local originPart = Instance.new("Part")
originPart.Transparency = 0.5
originPart.CanCollide = false
originPart.Anchored = true
originPart.Color = Color3.new(1, 0, 0)
originPart.Size = Vector3.new(1.5, 1.5, 1.5)
originPart.Parent = char

local directionPart = Instance.new("Part")
directionPart.Transparency = 0.5
directionPart.CanCollide = false
directionPart.Anchored = true
directionPart.Color = Color3.new(0, 1, 0)
directionPart.Size = Vector3.new(1.5, 1.5, 1.5)
directionPart.Parent = char

local lastResult:RaycastResult = nil
local oldPosition = nil
local relativePosition = nil
RunService.Heartbeat:Connect(function(dt)
	local origin = rootPart.Position
	originPart.Position = origin
	local destination = origin - (Vector3.new(0, legHeight + (rootPart.Size.Y / 2), 0))
	directionPart.Position = destination
	local result = workspace:Raycast(origin, destination-origin, params)
	
	if lastResult ~= nil and result == nil then
		if lastResult.Instance:IsA("BasePart") then
			if lastResult.Instance.Position ~= oldPosition and lastResult.Instance.CanCollide then
				char:PivotTo(
					CFrame.new(
						lastResult.Instance.Position + relativePosition
					) *
						CFrame.Angles(
							math.rad(rootPart.Rotation.X),
							math.rad(rootPart.Rotation.Y),
							math.rad(rootPart.Rotation.Z)
						)
				)
			end
		end
	elseif lastResult and result then
		if lastResult.Instance:IsA("BasePart") and result.Instance:IsA("BasePart") then
			if oldPosition ~= lastResult.Instance.Position and lastResult.Instance == result.Instance and result.Instance.CanCollide then
				char:PivotTo(
					CFrame.new(
						lastResult.Instance.Position + relativePosition
					) *
						CFrame.Angles(
							math.rad(rootPart.Rotation.X),
							math.rad(rootPart.Rotation.Y),
							math.rad(rootPart.Rotation.Z)
						)
				)
			end
		end
	end	
	
	if result then
		if result.Instance:IsA("BasePart") then
			relativePosition = rootPart.Position - result.Instance.Position
		end
	end
	
	lastResult = result
	if result then
		if result.Instance:IsA("BasePart") then
			oldPosition = result.Instance.Position
		else
			oldPosition = nil
		end
	else
		oldPosition = nil
	end
end)

I will try to explain what it does:
When we are standing on a platform, it will write the relativePosition - which is player’s position relative to the platform. Then, when we detect that the player slipped off or the platform is moving and snap the player back.
Just comment the parts out and…


3. Some clarification

Finished script
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")

local char:Model = script.Parent
local player = Players.LocalPlayer
local rootPart:BasePart = char:WaitForChild("HumanoidRootPart")

if char:WaitForChild("Humanoid").RigType == Enum.HumanoidRigType.R15 then
	legHeight = char:WaitForChild("LeftUpperLeg").Size.Y
	legHeight += char:WaitForChild("LeftLowerLeg").Size.Y
	legHeight += char:WaitForChild("LeftFoot").Size.Y
else
	legHeight = char:WaitForChild("LeftLeg").Size.Y
end

local params = RaycastParams.new()
params.FilterType = Enum.RaycastFilterType.Exclude
params.FilterDescendantsInstances = {char}
params.IgnoreWater = true

--// these are for testing where the ray goes
--local originPart = Instance.new("Part")
--originPart.Transparency = 0.5
--originPart.CanCollide = false
--originPart.Anchored = true
--originPart.Color = Color3.new(1, 0, 0)
--originPart.Size = Vector3.new(1.5, 1.5, 1.5)
--originPart.Parent = char

--local directionPart = Instance.new("Part")
--directionPart.Transparency = 0.5
--directionPart.CanCollide = false
--directionPart.Anchored = true
--directionPart.Color = Color3.new(0, 1, 0)
--directionPart.Size = Vector3.new(1.5, 1.5, 1.5)
--directionPart.Parent = char

local lastResult:RaycastResult = nil
local oldPosition = nil
local relativePosition = nil
RunService.Heartbeat:Connect(function(dt)
	local origin = rootPart.Position
	--originPart.Position = origin
	local destination = origin - (Vector3.new(0, legHeight + (rootPart.Size.Y / 2), 0))
	--directionPart.Position = destination
	local result = workspace:Raycast(origin, destination-origin, params)
	
	if lastResult ~= nil and result == nil then
		if lastResult.Instance:IsA("BasePart") then
			if lastResult.Instance.Position ~= oldPosition and lastResult.Instance.CanCollide then
				char:PivotTo(
					CFrame.new(
						lastResult.Instance.Position + relativePosition
					) *
						CFrame.Angles(
							math.rad(rootPart.Rotation.X),
							math.rad(rootPart.Rotation.Y),
							math.rad(rootPart.Rotation.Z)
						)
				)
			end
		end
	elseif lastResult and result then
		if lastResult.Instance:IsA("BasePart") and result.Instance:IsA("BasePart") then
			if oldPosition ~= lastResult.Instance.Position and lastResult.Instance == result.Instance and result.Instance.CanCollide then
				char:PivotTo(
					CFrame.new(
						lastResult.Instance.Position + relativePosition
					) *
						CFrame.Angles(
							math.rad(rootPart.Rotation.X),
							math.rad(rootPart.Rotation.Y),
							math.rad(rootPart.Rotation.Z)
						)
				)
			end
		end
	end	
	
	if result then
		if result.Instance:IsA("BasePart") then
			relativePosition = rootPart.Position - result.Instance.Position
		end
	end
	
	lastResult = result
	if result then
		if result.Instance:IsA("BasePart") then
			oldPosition = result.Instance.Position
		else
			oldPosition = nil
		end
	else
		oldPosition = nil
	end
end)
Why not use constraints?

You can. But, I am not too familiar with them.
Plus there is sometimes no way to implement some things with constraints rather than just changing the position of a part.

Why local script?
  • Not jittery
  • Less chance of player falling from the platform
  • High ping will not interrupt the script
Author notes

If I made any grammar mistakes, I’d be happy if you tell me.
If you can improve da script, I’d also be happy :)

Edits

Jul. 30 2023 - Fixed some spelling
Jul. 30 2023 (2) - Reworked script for parts that cannot collide

I hope this helped you. Have a good day.

here is a picture of dani with a gucci banana on his head showing an enlarged thumbs up

thank you jeremy. very cool

5 Likes