Raycast coroutine running during renderstep

Hello! I’m debugging a system for grinding on rails that uses raycasts under the character every frame to check if the character should be turning while grinding. However, the raycasts are not accurate because they aren’t fired frequently enough.

Here’s a visualization of how it should work. See how the raycast hit the intersection of these two rails? If the raycast hits a rail under the character that isn’t the character’s current rail, the script then switches the character’s current rail to that new rail.

However, because there is a slight delay between raycasts, the character can fall off a rail even though it’s connected to another rail.


In this instance, the last raycast was fired too late and so the character fell off.

I tried to fix this by running the logic that raycasts under the player while grinding as a coroutine during the main function, however it leads to the same result, and the ray visualizations still being the same distance. My logic behind this was that if the raycasting was a coroutine, the distance between raycasts would be shorter leading to no falling off. Is it because the entire rail grinding function is still being run every frame?

I also tried changing the Y size of the intersecting rails so that the ray would hit, but that also doesn’t work.

Here is my script.

RailGrinding
-- ⤹ Functions ⤸ --
local function GetRailRelative(rail: BasePart)
	local axis = rail.CFrame:VectorToObjectSpace(rail.Position);
	local player_axis = rail.CFrame:VectorToObjectSpace(HumanoidRootPart.Position);
	local offset = player_axis - axis;
	
	-- ⤹ Return ⤸ --
	return offset, player_axis;
end

local function railCheck(add)
	local CastVisual = CastVisuals.new()
	-- ⤹ Check For A Rail ⤸ --
	local down_part = workspace:Raycast(HumanoidRootPart.Position + (add * rail_speed), (rail.CFrame - rail.CFrame.p).UpVector * -10, params);
	CastVisual:Raycast(HumanoidRootPart.Position + (add * rail_speed), (rail.CFrame - rail.CFrame.p).UpVector * -10, params);

	if down_part and down_part.Instance and down_part.Instance.Name == "Rail" then
		-- ⤹ If Not Our Current Rail ⤸ --
		if down_part.Instance ~= rail then
			print("Changing rails")
			rail = down_part.Instance;
			local offset = GetRailRelative(down_part.Instance);
			cur_pos = offset.Z
		end
	else
		-- ⤹ Fall Of The Rail ⤸ --
		on_rail = false;
		GrindBindable:Fire(on_rail)
		HumanoidRootPart.AssemblyLinearVelocity = add * rail_speed * config.rbx_amplif;
		humanoid:ChangeState(Enum.HumanoidStateType.Freefall)
	end

	local forward_upward_part = workspace:Raycast(HumanoidRootPart.Position + (add*rail_speed), HumanoidRootPart.CFrame.LookVector * 4, params)
	if forward_upward_part and forward_upward_part.Instance and forward_upward_part.Instance.Name == "Rail" then 
		if forward_upward_part.Instance ~= rail then
			rail = forward_upward_part.Instance;
			local offset = GetRailRelative(forward_upward_part.Instance);
			cur_pos = offset.Z
		end
	end
end


run_service:BindToRenderStep("__RailUpdate.core", Enum.RenderPriority.Input.Value, function(delta : number)
	-- ⤹ Get The Now ⤸ --
	local now = tick()

	--Framerate count 
	fps_count += 1

	-- ⤹ Update ⤸ --
	local framerate = 1 / config.framerate

	if now >= (next_tick + (framerate * 10)) then
		next_tick = now
	end

	if now >= next_recycle and now >= (next_tick + (framerate * recycle_frames)) then
		next_tick = now
		next_recycle = now + recycle_time
	end
	
	-- ⤹ If We Should Update ⤸ --
	while now >= next_tick do
		-- ⤹ Tickrate Count ⤸ --
		tps_count += 1
		
		-- ⤹ Rail Debounce Timer ⤸ --
		rail_debounce = math.max(0, rail_debounce-1);

		-- ⤹ Do Physics ⤸ --
		if on_rail then
			if event_fire_attempts == 0 then 
				GrindBindable:Fire(on_rail)
				print("Grinding: "..tostring(on_rail))
			end
			-- ⤹ Rail Speed ⤸ --
			local add = rail.CFrame.LookVector;
			if rail_dir == -1 then
				add = -add;
			end
			
			-- ⤹ Rotate If We Are Moving Backwards ⤸ --
			local offset = rail_dir == 1 and 0 or math.rad(180);
			if not config.turn_rail then
				offset = rail_origin == 1 and 0 or math.rad(180);
			end
			rail_ang = rail_ang:Lerp((rail.CFrame - rail.CFrame.p) * CFrame.Angles(0, offset, 0), (0.1 + (rail_speed/4) ^ delta));
			
			-- ⤹ Move The Character ⤸ --
			HumanoidRootPart.CFrame = CFrame.new((rail.CFrame * CFrame.new(0, 0, cur_pos) * CFrame.new(0, humanoid.HipHeight + config.hip_offset, 0)).Position) * rail_ang;
			
			-- ⤹ Anchor The Player ⤸ --
			HumanoidRootPart.AssemblyLinearVelocity = Vector3.zero;
			
			
			coroutine.wrap(railCheck)(add)
			
			-- ⤹ Variables For Slope Detection ⤸ --
			local a1a = add;
			local dotp = (a1a.unit):Dot((Vector3.new(0, -1, 0)).Unit);
			local slope = -dotp;
			local acc = 0;

			-- ⤹ Check? ⤸ --
			if math.abs(slope) > 0.1 then
				if slope < 0 then
					-- ⤹ Moving Down, Accelerate ⤸ --
					acc += math.abs(slope) * config.rail_accel;
				else
					-- ⤹ Moving Up, Slow Down ⤸ --
					acc -= math.abs(slope) * config.rail_decel;
				end
			end

			-- ⤹ Calculate Rail Speed ⤸ --
			rail_speed += acc / config.rbx_amplif
			if rail_speed <= 0 and slope ~= -0 then
				-- ⤹ Change Direction, Since We Are Going Too Slow ⤸ --
				if rail_dir == 1 then
					rail_dir = -1;
				else
					rail_dir = 1;
				end

				-- ⤹ Accelerate ⤸ --
				rail_speed = config.turn_spd;
			end

			-- ⤹ Friction ⤸ --
			rail_speed = math.max(0, rail_speed - config.rail_friction)

			-- ⤹ Speed Cap ⤸ --
			if math.abs(rail_speed) > config.speed_cap then
				rail_speed = math.sign(rail_speed) * config.speed_cap;
			end
			-- ⤹ Minimum Speed ⤸ --
			if math.abs(rail_speed) < config.min_spd and config.min_spd ~= -1 then
				rail_speed = math.sign(rail_speed) * config.min_spd;
			end

			-- ⤹ Move ⤸ --
			if rail_dir == 1 then
				cur_pos -= rail_speed;
			else
				cur_pos += rail_speed;
			end

			-- ⤹ Rail Debounce ⤸ --
			if not on_rail then
				rail_debounce = config.debounce;
			end
		elseif rail_debounce <= 0 and humanoid:GetState() == Enum.HumanoidStateType.Freefall or Enum.HumanoidStateType.Jumping then
			event_fire_attempts = 0 
			-- ⤹ Get Rails ⤸ --
			for _, temp_rail in pairs(workspace:GetPartBoundsInRadius(HumanoidRootPart.Position, 3)) do
				-- ⤹ Check If It's Actually A Rail ⤸ --
				if temp_rail.Name == "Rail" then
					-- ⤹ Unanchor The HumanoidRootPart ⤸ --
					local offset, player_axis = GetRailRelative(temp_rail);
					local player_axis2 = temp_rail.CFrame:VectorToObjectSpace(HumanoidRootPart.Position + HumanoidRootPart.Velocity);

					-- ⤹ Are We Going Backwards? ⤸ --
					local offset2 = player_axis - player_axis2;
					if offset2.Z < 0 then
						-- ⤹ Yes ⤸ --
						rail_dir = -1;
					elseif offset2.Z > 0 then
						-- ⤹ No ⤸ --
						rail_dir = 1;
					end

					-- ⤹ Can We Go On The Rail ⤸ --
					if math.abs(offset.Z) < (temp_rail.Size.Z/2) then
						-- ⤹ Set Rail Part & Speed ⤸ --
						rail = temp_rail;
						rail_origin = rail_dir
						
						local horizontalSpeed = (HumanoidRootPart.AssemblyLinearVelocity - Vector3.new(0, HumanoidRootPart.AssemblyLinearVelocity.Y, 0)).Magnitude
						
						rail_speed = horizontalSpeed/config.rbx_amplif;
						
						if horizontalSpeed <= 0 then 
							rail_speed = config.min_spd
						end
						
						-- ⤹ Set The HumanoidRootPart Angle & Rail Position ⤸ --
						rail_ang = HumanoidRootPart.CFrame - HumanoidRootPart.CFrame.Position;
						cur_pos = offset.Z;

						-- ⤹ We Are Now On The Rail ⤸ --
						on_rail = true;
						if event_fire_attempts == 0 then 
							event_fire_attempts = 1 
							GrindBindable:Fire(on_rail)
						end

						-- ⤹ Contact Sound ⤸ --
						contact_sound:Play();

						-- ⤹ Enable Particles ⤸ --
						for i=1, #particles do
							particles[i].Enabled = true
						end
					end
				end
			end
		else
			-- ⤹ Unanchor The HumanoidRootPart ⤸ --
			HumanoidRootPart.Anchored = false;
			event_fire_attempts = 0 
		end

		-- ⤹ Rail Sound ⤸ --
		if not on_rail then
			rail_sound:Stop();
		else
			if not rail_sound.IsPlaying then
				rail_sound:Play();
			end
		end

		-- ⤹ Animation ⤸ --
		if not on_rail and load_animation.IsPlaying then
			load_animation:Stop();

			-- ⤹ Enable Particles ⤸ --
			for i=1, #particles do
				particles[i].Enabled = false
			end
		elseif on_rail and not load_animation.IsPlaying then
			load_animation:Play();
		end
		
		-- ⤹ Increament Tickrate Counter ⤸ --
		next_tick += framerate
	end
end)

Any help is extremely appreciated as this is kind of a doozy.