I’ve written a quick mockup, but haven’t tested in studio. It should show you the general idea though. (You don’t need to rely on GetPartsInPart to do this. You can also use raycasts)
--Server
--// Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
--// Variables
local Remotes = ReplicatedStorage.Remotes
--// Types
type _Projectile = {
["Lifetime"] : number, -- Lifetime of Projectile
["Velocity"] : number, -- Speed of the Projectile
["CFrame"] : CFrame, -- Position + Direction of Projectile
["Key"] : string -- Key used to identify the projectile, to allow us to find and destroy on client.
}
--// Functions
local function SomeRandomSkill(Player : Player, Mouse : Vector3)
local Character = Player.Character
if not Character then return end
if typeof(Mouse) ~= "Vector3" then return end
local Projectile : _Projectile = {
["Key"] = Player.Name .. DateTime.now().UnixTimestampMillis + math.random(1, 1000),
["CFrame"] = CFrame.lookAt(Character:GetPivot().Position, Mouse),
["Velocity"] = 100,
["Lifetime"] = 4000, --Equivalent to 4 seconds.
}
local Hitbox = Instance.new("Part") -- I actually recommend looking into PartCache!
Hitbox:PivotTo(Projectile.CFrame)
Hitbox.Parent = workspace.SkillDebris.Projectiles
local Params = OverlapParams.new()
Params.FilterDescendantsInstances = {Character, workspace.SkillDebris}
--Params.CollisionGroup = "Projectile" -- I highly recommend utilizing CollisionGroups!!!
Params.MaxParts = 1
--// Send information to client!
Remotes.SetFx:FireAllClients(
{
["SkillName"] = "SomeRandomSkill",
["Status"] = "Projectile",
["Projectile"] = Projectile
}
)
--//
local StartTimer = DateTime.now().UnixTimestampMillis
local Connected
local Hit = false
Connected = RunService.Heartbeat:Connect(function(deltaTime)
if (DateTime.now().UnixTimestampMillis - StartTimer) >= Projectile.Lifetime then
Hit = true
end
local PartsInPart = workspace:GetPartsInPart(Hitbox, OverlapParams) --Raycast checks or PartsInPart. Im just gonna use PartsInPart to get idea across
if PartsInPart[1] then
Hit = true
end
if Hit then
Connected:Disconnect()
--// Send information to client!
Remotes.SetFx:FireAllClients(
{
["SkillName"] = "SomeRandomSkill",
["Status"] = "Hit",
["Data"] = {
["HitPosition"] = Hitbox.CFrame,
["Key"] = Projectile.Key,
}
}
)
--//
table.clear(Projectile)
Projectile = nil
StartTimer = nil
Connected = nil
Params = nil
Hit = nil
else
Hitbox.CFrame += Hitbox.CFrame.LookVector.Unit * Projectile.Velocity * deltaTime
end
end)
end
--// Scipt
Remotes.RequestAttack.OnServerEvent:Connect(SomeRandomSkill)
--Client
local SkillFx = {}
--// Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
--// Variables
local Remotes = ReplicatedStorage:WaitForChild("Remotes")
local SkillDebris = workspace:WaitForChild("SkillDebris")
--// Functions
function SkillFx.SetFx(Data) -- Finds hitbox and sets attribute 'Active' to nil.
local SkillFound = SkillFx[Data.SkillName]
if typeof(SkillFound) == "function" then
SkillFound(Data)
end
end
function SkillFx.SetInactive(Key)
local Found = SkillDebris:FindFirstChild(Key)
if Found then
Found:SetAttribute("Active", nil)
end
end
function SkillFx.SomeRandomSkill(Data)
if Data.Status == "Projectile" then
local Projectile = Data.Projectile
if not Projectile then
return
end
--Generate visuals stuff
local Part = Instance.new("Part") -- Once again I recommend PartCache!!!!
Part.Name = Projectile.Key
Part:SetAttribute("Active", true)
Part:PivotTo(Projectile.CFrame)
Part.Parent = SkillDebris
local StartTimer = DateTime.now().UnixTimestampMillis
local Connected
Connected = RunService.RenderStepped:Connect(function(deltaTime)
if Part:GetAttribute("Active") == nil then
Connected:Disconnect()
Part:Destroy()
StartTimer = nil
Connected = nil
Part = nil
elseif (DateTime.now().UnixTimestampMillis - StartTimer) >= Projectile.Lifetime then
Part:SetAttribute("Active", nil) -- Lifetime exceeded so set to nil (also works as failsafe!!!)
else
Part.CFrame += Projectile.CFrame.LookVector.Unit * Projectile.Velocity * deltaTime
end
end)
elseif Data.Status == "Hit" then
--Hit visual stuff
local HitData = Data.HitData
if not HitData then
return
end
SkillFx.SetInactive(HitData.Key) -- Call function to set projectile to nil
-- Other visuals, etc
end
end
--// Script
Remotes.SetFx.OnClientEvent:Connect(SkillFx.SetFx)
I’ve been using this method in my Projects, and everything seems all aligned and consistent. If you need help understanding my code, just reply.