Strange delay with LookVector and BodyVelocity

I’m working on an ability system for my game, one of the abilities you can use is a dive roll mechanic that propels the player in the direction they’re facing using a BodyVelocity and their LookVector.

It works fine enough but if the player turns quickly and then uses it, it creates a ‘delay’ and shoots the player off in a direction they were previously facing. Any idea how to fix it? I’ve provided a video below (as well as the code I’m using, which is in a ModuleScript).

return function(player)
	print(player.Name .. " has fired " .. script.Name .. "!!")
	local character = player.Character
	local humanoid = character:WaitForChild("Humanoid")
	local animator = humanoid:WaitForChild("Animator")
	local rootPart = humanoid.RootPart
	
	--local toapply = CFrame.new(Vector3.new(rootPart.CFrame.X, rootPart.CFrame.Y, rootPart.CFrame.Z)) * CFrame.Angles(0, math.pi, 0)
	
	local diveAnim = Instance.new("Animation")
	diveAnim.AnimationId = "rbxassetid://8274917642"
	
	--[[
	local diveSound = Instance.new("Sound")
	diveSound.SoundId = "rbxassetid://6873845809"
	diveSound.Parent = rootPart
	]]
	
	local diveForce = Instance.new("BodyVelocity")
	diveForce.Parent = rootPart
	diveForce.Velocity = rootPart.CFrame.LookVector * 90
	diveForce.MaxForce = Vector3.new(35000, 50000, 35000)
	
	local diveTrail = script.Trail:Clone()
	diveTrail.Parent = character.Torso
	diveTrail.Attachment0 = character.Torso.BodyBackAttachment
	diveTrail.Attachment1 = character.Torso.BodyFrontAttachment
	
	local diveParticle = script.ParticleEmitter:Clone()
	diveParticle.Parent = character.Torso
	diveParticle.Enabled = true
	
	local diveAnimTrack = animator:LoadAnimation(diveAnim)
	diveAnimTrack:Play()
	
	--diveSound:Play()
	
	wait(.4)
	diveForce:Destroy()
	diveTrail.Enabled = false
	diveParticle.Enabled = false
	wait(4)
	diveTrail:Destroy()
	diveParticle:Destroy()
end

I am aware BodyVelocity is depricated, btw.

Send the direction of the dash/roll from the client instead of checking in the server, since there is no way to avoid lag.

2 Likes

This worked (thank you!), but has now made a separate issue where once the player dies/respawns, the LookVector gets stuck and only performs the roll in the direction the player was facing when they died.

Here’s part of the LocalScript, the rest of the script is just separate code for finding the equipped ability and defining variables.

local db = false

function abilityBegin(input, bool)
	if userInputService:IsKeyDown(Enum.KeyCode.E) then
		if not bool then
			if db == false then
				db = true
				local lv = rootPart.CFrame.LookVector
				lookVectorEvent:FireServer(lv)
				abilityFiredEvent:FireServer(abilityName)
				wait(abilityCooldown)
				db = false
			end
		end
	end
end

userInputService.InputBegan:Connect(abilityBegin)

I’m guessing this is for a ServerScript?

Anything dealing with the Player should be handled by the Client. If you don’t want anyone hacking the game and using the ability then fine, but if you are waiting for the Humanoid and Animator every time it fires you may have issues.

Can you just do a quick if check to see if those are loaded then perform the animation immediately?

You are also doing a lot of other calculations before playing the animation, you may need to do those beforehand.

This isn’t about the animation, although I do realise now that waiting for the Humanoid every time on the server is a little redundant.

The main issue here, though, is the issues I’ve been having with LookVector / BodyVelocity. I fixed the issue in the original post thanks to Lielmaster’s help, but it’s created a separate problem now which I posted about above.

Yes, but all of the calculations you are doing/waiting for can be the make or break difference causing your issue.
Player going one direction.
Player clicks whatever to do the ability.
Player changes direction during the split second the calculations are being done.
Ability plays in the original direction.

Also loading the animation each time takes a moment. You should load it before the function. The script will always have it loaded and you can just play it in the function.

The other thing would be to recheck the direction right before the ability is performed and point it that way.

This is called lag. It’s normal and avoidable.
Please, do not do movement mechanics on the server.

The client can move the player character however it likes at any time and it will automatically replicate to the server. [1]
You can create the BodyMover on the client and the player will immediately move in the correct direction with no problems whatsoever.
The server does not need to know any lookvector.

Local script:

local db = false

function abilityBegin(input, bool)
	if userInputService:IsKeyDown(Enum.KeyCode.E) then
		if not bool then
			if db == false then
				db = true
				
				local diveForce = Instance.new("BodyVelocity")
				diveForce.Parent = rootPart
				diveForce.Velocity = rootPart.CFrame.LookVector * 90
				diveForce.MaxForce = Vector3.new(35000, 50000, 35000)
				wait(.4)
				diveForce:Destroy()
				
				wait(abilityCooldown - 0.4) -- quick hack to keep the timing right
				db = false
			end
		end
	end
end

userInputService.InputBegan:Connect(abilityBegin)

This will make the E button dive forward for a short time.

The particle effects, trail and animation are trickier.
If you fire the event and just let the server create and enable them, then they will show up shortly after the dash has begun, or perhaps even the dash has ended (if ping is over 400ms).
You have to enable them both on the server (so all players see them) and on the client (so that the player using the ability sees it immediately).
If you do exactly that, then the trail and sparkles might seemingly work correctly, but the sound and animation will play twice.

One way to solve this is to have another remote that means “this player used an ability”. This remote will run a function that enables the particles, trail etc.
The player who used an ability will call the same function that this remote would’ve fired to get the particles instantly. A script on the server side will fire this remote to all players except the player who used the ability so that everyone will see it, but the original player won’t see it twice.
It’s solid, but takes more annoying remote event code.

local remote = ReplicatedStorage.AbilityUsed

-- Server
remote.OnServerEvent:Connect(function(player, name)
	for _,v in ipairs(Players:GetPlayers()) do
		-- retransmit to all players except for the one who called it
		if v ~= player then
			remote:FireClient(v, name)
		end
	end
end)

-- Client
local function abilityDash(player)
	-- enable trail, particles, animation... on player's character
end

remote.OnClientEvent:Connect(function(player, name)
	if name == "Dash" then
		abilityDash(player)
	end
end)

-- when the player uses the dash ability, show effects on self
abilityDash(Players.LocalPlayer)

This isn’t rock solid, an exploiter might fire this event when they have no character to spam errors in console or something stupid like that, or create tons of lag for everyone.
It’s something to keep in mind with any remote at all, for that matter.

Another way I just thought of is to clone the trail, particles etc. on the client and delete the originals.
When the server enables the originals, all other players will see the effects as normal, but the effect-using player won’t (and shouldn’t, because it saw the effect before the server saw it)
The effects should be set up in advance (but dormant/disabled) instead of being created when the effect starts. If the server creates a new effect on the fly, then there was no point in deleting anything and the effect will run twice anyway.

-- I recommend writing all of this in a module script

local trail
local particles
-- etc.

local function shadowClone(instance)
	local clone = instance:Clone()
	instance.Parent = nil -- do not care, do not want
	return clone
end

local function showEffects()
	-- enable trail, show particles, play sound
	-- wait
	-- disable
end

local function initializeDash()
	-- if on server
		-- create/copy over effects
		trail = script.Trail:Clone()
		-- etc.
		
	-- elseif on client
		-- wait for server to set up effects, grab local copies
		trail = shadowClone(torso:WaitForChild("Trail"))
		-- etc.
	-- end
end

This method is annoying because you cannot just create and destroy the effects, which was kinda convenient earlier.
There’s also a bunch of synchronization to worry about. What if multiple effects try to create a trail with the same name? A lot of things can go wrong. But it’s just another way to do it.

[1] An exploiter can also teleport to whereever they like instantly, they just set HumanoidRootPart.CFrame.
Think like an exploiter. Can you make your movement effect work client-side without any remotes? If so, then do it, don’t let the exploiter be better than you.

5 Likes

Thank you so much for this explanation. I’m actually not even sure why I was doing the calculation on the server, in hindsight that was a pretty bad choice and was most likely one of the main reasons I had the delay. The other points about the effects are useful too, I’ll do some tinkering later to figure out the best solution for that.