Issues with Moving “Train” Platforms that exceed Humanoid WalkSpeed

Preface

I’ve been experimenting with raycasting and CFraming recently in order to become more well-rounded, and I was interested in creating platforms that Characters could automatically move with. During this process, I stumbled across a topic that tyridge77 and badcc provided insight on, as they’ve implemented trains in the respective games they work on (The Wild West & Jailbreak).

Utilizing the information provided there, including placebo code from a couple of posts, I put together something that I was satisfied with until realizing that Characters were unable to leave the moving platform in the direction it’s moving in when exceeding the Humanoid’s WalkSpeed.


Video Examples

Video 1 Description: The first video demonstrates the moving platforms in my testing place at various speeds. The yellow, blue, and red platforms are the only parts contained in the raycast whitelist; the translucent parts that my Character walks on are not included.

Video 1 Notes: I understand that the Character is unable to leave the platform because of how physics take over when the raycast does not find any whitelisted part to teleport the Character above. When the platform is slower than the Character, the platform cannot keep up with the Character, and vice versa. However, I am unsure of how I can accurately compensate for this without accidentally teleporting the Character when it’s not necessary.


Video 2 Description: In the second video, I went into Jailbreak and The Wild West in order to run similar tests that I did in my place. While I’m unsure of the speed that the trains were moving at, it’s clear that both of the trains exceeded the WalkSpeed of the Humanoid when walking or sprinting.

Video 2 Notes: The behavior of Jailbreak’s raycasting is similar to what was demonstrated in my test place – this is especially clear at the very front of the train. However, I was baffled by The Wild West because it allowed the Character to smoothly exit the boundaries of the train in the direction it’s moving in, even though the train is moving faster than the Character is able to.


Code & Test Place

The code for the raycasting & teleporting of the Character along with the Tweening of the moving platforms can be found below. While I understand how the code works, take note that I am not adept with raycasting & CFraming. Because this is the first project I’ve worked on that revolves around these concepts, there are likely many nuances that I’ll be unaware of until I garner more experience.

Raycasting Test Place (3.20.2021).rbxl (27.7 KB)

Character Raycasting code

The code I have below was derived/based on this topic in regards to moving platforms.

(Extra lines of code were omitted from this, including the Raycast Visualization)

local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer


local lastObjectRaycasted -- Last part that was hit by a raycast
local lastObjectCFrame -- The part's CFrame at the time


local Function
local Function2


local Whitelist = {workspace.MovableItems.MovingParts}



Function = RunService.Heartbeat:Connect(function()
	
	local Character = player.Character or player.CharacterAdded:Wait()
	local HumanoidRootPart = player.Character.HumanoidRootPart
	
	
	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = Whitelist
	raycastParams.FilterType = Enum.RaycastFilterType.Whitelist
	
-- I plan on making this more efficient in the future
	local raycastParams2 = RaycastParams.new()
	raycastParams2.FilterDescendantsInstances = {lastObjectRaycasted}
	raycastParams2.FilterType = Enum.RaycastFilterType.Whitelist
	
-- Two separate RaycastResults in case a previous Instance is still stored
	local RaycastResult = workspace:Raycast(HumanoidRootPart.CFrame.Position, Vector3.new(0,-20,0), raycastParams)
	local lastObjectRaycastedRaycastResult = workspace:Raycast(HumanoidRootPart.CFrame.Position, Vector3.new(0,-20,0), raycastParams2)
	
	
	local updatedInfo

-- If Character is still above the "lastObjectRaycasted", then...
	if lastObjectRaycasted and lastObjectRaycastedRaycastResult then
		updatedInfo = lastObjectRaycastedRaycastResult -- Stores Raycast Result into updatedInfo
		
	else -- Otherwise, updatedInfo is set to the Raycast Result that detects any descendant of the "MovingParts" folder
		updatedInfo = RaycastResult
		lastObjectRaycasted = nil
	end
	
	
	if updatedInfo then -- If a raycast found a part in the whitelist...
		
		local raycastInstance = updatedInfo.Instance
		
		if not lastObjectCFrame then -- Prevents relativeDistance from erroring if no value is stored already
			lastObjectCFrame = raycastInstance.CFrame
		end
		
		
		local currentCFrame
		
		if lastObjectRaycasted and raycastInstance ~= lastObjectRaycasted then
			currentCFrame = lastObjectRaycasted.CFrame -- Updates currentCFrame to the last object that was stored if it doesn't align with the current raycastInstance
		else
			currentCFrame = raycastInstance.CFrame
		end
		
		
		local relativeDistance = currentCFrame * lastObjectCFrame:Inverse() -- Finds the relativeDistance from the CFrame of the current Instance & the previous one that was stored
		HumanoidRootPart.CFrame = relativeDistance * HumanoidRootPart.CFrame
		
		lastObjectRaycasted = raycastInstance
		lastObjectCFrame = raycastInstance.CFrame
		
	else -- If raycast didn't return a result, the lastObjectCFrame will be cleared
		lastObjectCFrame = nil
	end
	
	
	if not Function2 then
		Function2 = player.Character.Humanoid.Died:Connect(function()
			Function:Disconnect()
			Function2:Disconnect()
		end)
	end
	
end)

Moving Platform code

Explorer Hierarchy

-- This tweens every descendant that is a BasePart in the MovableItems folder

local TweenService = game:GetService("TweenService")

local MovingPlatformTween = TweenInfo.new(

	20,
	--[[
	
	20 seconds = 20 studs a second
	25 seconds = 16 studs a second
	400/15 seconds = 15 studs a second

	]]--
	
	
	Enum.EasingStyle.Linear,
	Enum.EasingDirection.In,
	5,
	true,
	20

)



for _,Item in ipairs(workspace.MovableItems:GetDescendants()) do
	
	if Item:IsA("BasePart") then
		
		local Goal = {
			
			CFrame = Item.CFrame + Vector3.new(0, 0, -400)
			
		}
		
		local MoveItem = TweenService:Create(Item, MovingPlatformTween, Goal)
		
		MoveItem:Play()
		
	end
end

I appreciate you taking the time to read this! :slight_smile:

10 Likes

Super cool research that have done on raycasting and cframe. I am pretty sure the ‘train’ element that was implemented in both jailbreak, and the wild west wasn’t a local script as all players see the same. I checked out the file and opened your game in my studios after looking at the scripts, I also learned a thing or two about raycasting, thanks.

1 Like

Take note that I did post this topic in #help-and-feedback:code-review, so even if you’ve learned something from this topic, remember that I’m looking for improvements/optimizations since the behavior that’s exhibited under certain circumstances is not ideal. I provided a disclaimer that this is the first project I’ve worked on that revolves around these concepts at the end of the post, which means that I am most definitely not utilizing the best practices for raycasting & CFraming quite yet.


Also, The Wild West updates the CFrame of each player on the client (as to reduce load on the server), but the position of other players on the train (from each client’s perspective) are given that information based on what the server found, as noted by tyridge’s post that I referenced:

1 Like

Me and @StrongBigeMan9 have discussed this in dms and we began brain storming what it could be, and after a while, we eventually got to a solution, but there were a few hiccups here and there


LookVector is terrible for this

At first, I thought “Maybe I could use LookVector for this”, and it worked…somewhat. What I did was use the LookVector to add an angle to the Raycast, since at first it was just going completely down. And it did fix the issue, but it wasn’t perfect. What happened was that it wouldn’t keep you on if the part moved and you’re at the edge or when you tried to go off it backwards via shift locking, looking away, and trying to get off, as the part would keep you on. Eventually, this was not ideal, and I quickly went back to the drawing board, and that was eventually when I discovered the fix for it


The Fix

After a while of trial and error, I remembered the MoveDirection Property of humanoids, which describes the XYZ unit of the humanoid of where it is moving. With this knowledge, I did this

  • Create a variable for the down vector of the Raycast which is the UpVector of the RootPart multiplied by -13
  • Create a variable For the move direction of the Humanoid multipled by 2.5 for what I used
  • Create a variable for the angle which is just DownVector subtracted by The X and Z of our multiplied MoveDirection
  • Make the direction of both Raycasts to the angle variable

This worked for every situation @StrongBigeMan9 would face, the only issue would be trying to jump off the part in the same direction as it is going, but that’s not an issue with this as it’s because the part is moving faster than your WalkSpeed


Conclusion

Once I discovered this, I immediately messaged him with my findings and it worked for him and I even thought him something new since he has never heard of MoveDirection haha, but of course it’s fine he didn’t know, we can’t expect anyone to be 100% knowledgeable in everything!

4 Likes

Extra Info!

Oh and just to clarify for anyone else that comes across this thread, I tested it at one of the same speeds as shown in the video (20 “WalkSpeed” for the platform) and it allowed my Character to leave the platform in the direction it was moving in (when it originally wouldn’t at all) – it’s just that the player’s Character was unable to remain on the adjacent part due to the speed the platforms were going at (which is completely understandable). If the part is much closer/the platforms aren’t moving as fast, the Character may be able to swap smoothly.

To see a video example with the new changes, click here or scroll down to the (New) Video Example section. Or, for even more fun, click the smiley face to be automatically brought to that section :slight_smile:


Code Changes


Example Place & Updated Code

Raycasting Test Place (Fixed Version!).rbxl (27.5 KB)

Click here for the updated code in its entirety
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")
local player = Players.LocalPlayer


local lastObjectRaycasted
local lastObjectCFrame


local Function
local Function2


local Whitelist = {workspace.MovableItems.MovingParts}



Function = RunService.Heartbeat:Connect(function()
	
	local Character = player.Character or player.CharacterAdded:Wait()
	local Humanoid = Character.Humanoid
	local HumanoidRootPart = Character.HumanoidRootPart
	
	
	local raycastParams = RaycastParams.new()
	raycastParams.FilterDescendantsInstances = Whitelist
	raycastParams.FilterType = Enum.RaycastFilterType.Whitelist
	
	
	local raycastParams2 = RaycastParams.new()
	raycastParams2.FilterDescendantsInstances = {lastObjectRaycasted}
	raycastParams2.FilterType = Enum.RaycastFilterType.Whitelist
	
	
	local downVector = HumanoidRootPart.CFrame.UpVector * -13
	local moveDirection = Humanoid.MoveDirection * 2.5
	local angle = downVector - Vector3.new(moveDirection.X, 0, moveDirection.Z)
	
	local RaycastResult = workspace:Raycast(HumanoidRootPart.CFrame.Position, angle, raycastParams)
	local lastObjectRaycastedRaycastResult = workspace:Raycast(HumanoidRootPart.CFrame.Position, angle, raycastParams2)
	
	
	local updatedInfo
	
	if lastObjectRaycasted and lastObjectRaycastedRaycastResult then
		updatedInfo = lastObjectRaycastedRaycastResult
		
	else
		updatedInfo = RaycastResult
		lastObjectRaycasted = nil
	end
	
	
	if updatedInfo then
		
		
		local distance = (HumanoidRootPart.CFrame.Position - updatedInfo.Position).Magnitude
		local visualizer = Instance.new("Part")
		visualizer.Anchored = true
		visualizer.Transparency = 0.5
		visualizer.BrickColor = BrickColor.new("New Yeller")
		visualizer.CanCollide = false
		visualizer.Size = Vector3.new(0.1, 0.1, distance)
		visualizer.CFrame = CFrame.lookAt(HumanoidRootPart.CFrame.Position, updatedInfo.Position) * CFrame.new(0, 0, -distance/2)
		visualizer.Parent = workspace
		
		
		local raycastInstance = updatedInfo.Instance
		
		if not lastObjectCFrame then
			lastObjectCFrame = raycastInstance.CFrame
		end
		
		
		local currentCFrame
		
		if lastObjectRaycasted and raycastInstance ~= lastObjectRaycasted then
			currentCFrame = lastObjectRaycasted.CFrame
		else
			currentCFrame = raycastInstance.CFrame
		end
		
		
		local relativeDistance = currentCFrame * lastObjectCFrame:Inverse()
		HumanoidRootPart.CFrame = relativeDistance * HumanoidRootPart.CFrame
		
		lastObjectRaycasted = raycastInstance
		lastObjectCFrame = raycastInstance.CFrame
		
	else
		lastObjectCFrame = nil
	end
	
	
	if not Function2 then
		Function2 = player.Character.Humanoid.Died:Connect(function()
			Function:Disconnect()
			Function2:Disconnect()
		end)
	end
	
end)

Explanation Section

The changes to the code revolved around these lines – more specifically, the calculation involved with the direction of the raycast:

local RaycastResult = workspace:Raycast(HumanoidRootPart.CFrame.Position, Vector3.new(0,-20,0), raycastParams)
local lastObjectRaycastedRaycastResult = workspace:Raycast(HumanoidRootPart.CFrame.Position, Vector3.new(0,-20,0), raycastParams2)

Instead of casting it directly downward, @EmbatTheHybrid incorporated the Humanoid’s MoveDirection as to determine the direction that the Character is moving toward. This ensures that the Character is not “locked” from leaving the platform in a particular direction and that they do not fall off when jumping in place near the edge of the platform (which was an issue when LookVector was used on an individual axis).

local downVector = HumanoidRootPart.CFrame.UpVector * -13 -- Vertical aspect / 13 studs downward from HumanoidRootPart
local moveDirection = Humanoid.MoveDirection * 2.5 -- (Edit: This is multiplying the MoveDirection and not adding! I mixed up multiplying CFrames together vs a Vector3 value and a single number) 
local angle = downVector - Vector3.new(moveDirection.X,0,moveDirection.Z) -- Subtracts the moveDirection so that the raycast ends up casting behind the Character

Note: Because the Humanoid’s MoveDirection is being referenced, ensure that a variable has been created for the Humanoid at the start of the function (above/below the HumanoidRootPart variable).

The direction that the ray is cast would then need to be adjusted inside of the RaycastResult variables from a static Vector3.new(0, -20, 0) to using the angle that was calculated above.

local RaycastResult = workspace:Raycast(HumanoidRootPart.CFrame.Position, angle, raycastParams)
local lastObjectRaycastedRaycastResult = workspace:Raycast(HumanoidRootPart.CFrame.Position, angle, raycastParams2)

For those of you coming across this post, keep in mind that that the value which is multiplied by the MoveDirection in addition to the negative offset of the UpVector may need to be adjusted, however, the current values of -13 and 2.5 worked well for my use case.


(New) Video Example


Raycast Visualization

Just in case anyone is curious about how the raycast visualization was achieved, I based it off of the following post by incapaz:

Here’s how it was incorporated into the code:

if updatedInfo then -- For reference
		
		----[[
		
		local distance = (HumanoidRootPart.CFrame.Position - updatedInfo.Position).Magnitude
		local visualizer = Instance.new("Part")
		visualizer.Anchored = true
		visualizer.Transparency = 0.5
		visualizer.BrickColor = BrickColor.new("New Yeller")
		visualizer.CanCollide = false
		visualizer.Size = Vector3.new(0.1, 0.1, distance)
		visualizer.CFrame = CFrame.lookAt(HumanoidRootPart.CFrame.Position, updatedInfo.Position) * CFrame.new(0, 0, -distance/2)
		visualizer.Parent = workspace
		

        Debris:AddItem(visualizer, # of seconds until it disappears)
        -- Optional to have this; make sure to define Debris at top of LocalScript


		--]]--
		
		
		local raycastInstance = updatedInfo.Instance -- For reference

Conclusion

@EmbatTheHybrid, thank you very much once again for the extraordinary help with this! I really appreciate the time and support :smiley:

4 Likes

Anytime! I thank you for coming to me for help, it means a lot to me! Also, a bit of a correction

This actually multiplies 2.5 with all the axis in MoveDirection

So if it was (0.5,0,-1)

It would become

(1.25,0,-2.5)

Least, that’s what I believe haha

3 Likes