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.