I’ve started creating my first ever ability with VFX. This one consists of an ice ability, which rapidly shoots three projectiles that follow the path of varying bezier curves with the destination being the current location of the mouse. I am handling all effects on client, and the hitbox on the server.
My issue is that I have the projectile effects client side, which is what I believe to be the ideal place for the effects to take place. However, at the moment, the speed of the effects are greatly influenced by the players FPS. This makes for an unsynced effect and hitbox unless the player is at a steady 60 FPS. I am unsure as to why this is the case.
Video examples
60 FPS (works as intended):
Above 60FPS (hitbox on usual timing, effects too fast):
I’ve provided my code below. Now, I must warn you, my code is very messy as I am still learning, but I’ve tried to add some comments to help you venture through the mess :> (tips on improving any of it also appreciated haha). Side note, I do plan on adding debounce!! (Although, throwing down an unholy amount of spells onto your friends is far too fun).
Local script
--Local script--
--Services--
local UserInputService = game:GetService("UserInputService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Debris = game:GetService("Debris")
local player = game:GetService("Players").LocalPlayer
local TweenService = game:GetService("TweenService")
local character = player.Character or player.CharacterAdded:Wait()
--Constant variables--
local MAX_DISTANCE = 70 --Max distance target can be
local MAX_VIEW_DISTANCE = 400 --Max distance player can see VFX from
local remote = ReplicatedStorage:WaitForChild("IceBallRemote")
local mouse = player:GetMouse()
--Check if player has pressed Q
local function DetectInput(input, gameprocessed)
if gameprocessed then return end
if input.KeyCode == Enum.KeyCode.Q then
local position = character.HumanoidRootPart
for i = 1, 3 do
--Check if desired target is in range
if (position.Position - mouse.Hit.Position).Magnitude < MAX_DISTANCE and character:WaitForChild("Humanoid").Health > 0 then
local position = character.HumanoidRootPart
remote:FireServer(mouse.Hit, position)
task.wait(0.3)
end
end
end
end
--All iceball VFX--
local function IceBallBezier(mouseHit, randomNumber, position)
local iceBall = ReplicatedStorage.FX:WaitForChild("IceBall"):Clone()
iceBall.Parent = workspace.WorkspaceFX
local HumanoidRootPart = position
Debris:AddItem(iceBall, 1.5)
--Bezier curve calculations
local function Curve(t, P0, P1, P2)
local A = P0:Lerp(P1, t)
local B = P1:Lerp(P2, t)
return A:Lerp(B, t)
end
local Mid = (HumanoidRootPart.Position - mouseHit.Position).Magnitude
local Part0 = HumanoidRootPart.Position + Vector3.new(0, 0, -2)
local Part1 = (HumanoidRootPart.CFrame * CFrame.new(-Mid/randomNumber, Mid/4, -Mid/4)).Position
local Part2 = mouseHit.Position
iceBall["Fire Crackling Sound"]:Play()
--Move iceball across curve
for i = 0,1,0.045 do
local Bezier = Curve(i,Part0,Part1,Part2)
iceBall.Position = Bezier
task.wait()
end
--Disable active particles in iceball
local function StopParticles()
iceBall.Transparency = 1
iceBall["Fire Crackling Sound"]:Stop()
for i, particle in ipairs(iceBall.Middle:GetChildren()) do
if particle:IsA("ParticleEmitter") then
if particle.Name ~= "Vortex" then
particle.Enabled = false
else
particle:Destroy()
end
task.wait()
end
end
end
--Explosion function
local function Explosion()
iceBall:FindFirstChild("ActiveLight"):Destroy()
--Part for explosion location
local explosionPoint = Instance.new("Part", workspace.WorkspaceFX)
explosionPoint.Anchored = true
explosionPoint.CanCollide = false
explosionPoint.CanQuery = false
explosionPoint.CanTouch = false
explosionPoint.Position = mouseHit.Position
explosionPoint.Transparency = 1
local light = Instance.new("PointLight", explosionPoint)
light.Color = Color3.fromRGB(143, 188, 255)
light.Brightness = 12
light.Range = 12
light.Shadows = true
--Tween info for explosion light
local tweenInfo = TweenInfo.new(
0.7,
Enum.EasingStyle.Sine,
Enum.EasingDirection.Out,
0,
false,
0)
--Tween explosion light off
TweenService:Create(light, tweenInfo, {Brightness = 0}):Play()
local attatchment = Instance.new("Attachment", explosionPoint)
Debris:AddItem(explosionPoint, 3.5)
--Play explosion sounds
local hitSound = ReplicatedStorage.SFX["Ice Spawn SFX"]:Clone()
hitSound.Parent = explosionPoint
hitSound:Play()
local hitSound2 = ReplicatedStorage.SFX["Ice 2 SFX"]:Clone()
hitSound2.PlaybackSpeed = 1.5
hitSound2.Parent = explosionPoint
hitSound2:Play()
local hitSound3 = ReplicatedStorage.SFX["water_magic"]:Clone()
hitSound3.Parent = explosionPoint
hitSound3:Play()
--Emit explosion particles
for i, particle in ReplicatedStorage.FX.Explosion:GetChildren() do
local newParticle = particle:Clone()
newParticle.Parent = attatchment
--Emit current particle amount in EmitCount attribute
newParticle:Emit(newParticle:GetAttribute("EmitCount"))
end
end
task.spawn(StopParticles)
task.spawn(Explosion)
end
--Client side
local function OnClient(mouseHit, position)
--If player is further away than the max viewing distance, return
if (character.HumanoidRootPart.Position - mouseHit.Position).Magnitude > MAX_VIEW_DISTANCE then return end
local finalNum
local randomNum = math.random(1,2)
if randomNum == 1 then
finalNum = 1
else
finalNum = -1
end
local function foo()
IceBallBezier(mouseHit, (math.random(20,60)/10)*finalNum, position)
end
task.spawn(foo)
end
UserInputService.InputBegan:Connect(DetectInput)
remote.OnClientEvent:Connect(OnClient)
Server script
--Server script--
--Services--
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")
local remote = ReplicatedStorage:WaitForChild("IceBallRemote")
--Constant variables--
local HITBOX_SIZE = Vector3.new(14,10,14)
local DAMAGE = 15
--Hitbox creation--
local function CreateHitBox(mouseHit, position)
local overlapParams = OverlapParams.new()
local hitContents = workspace:GetPartBoundsInBox(mouseHit*CFrame.new(0,HITBOX_SIZE.Y/2,0), HITBOX_SIZE, overlapParams)
local hitList = {}
for i, v in pairs(hitContents) do
local targetCharacter = v.Parent
local humanoid = targetCharacter:FindFirstChild("Humanoid")
if humanoid then
local character = position.Parent
--Make sure not to damage player who cast the spell
if not hitList[humanoid] and targetCharacter ~= character then
hitList[humanoid] = true
humanoid:TakeDamage(DAMAGE) --Deal specified damage
end
end
end
end
--Server side
local function ServerRecieved(player, mouseHit, position)
local function spawnHitbox()
task.wait(0.43) --How long until hitbox spawns from when Q is pressed
CreateHitBox(mouseHit, position)
end
task.spawn(spawnHitbox)
remote:FireAllClients(mouseHit, position)
end
remote.OnServerEvent:Connect(ServerRecieved)
Apologies for the lengthiness of the scripts and thank you for your time! If anyone knows any good bezier curve modules that can help simplify the process for me I’d also be extremely grateful