So im trying to make a travelling projectile that moves smoothly and sync with the client and server, detect hits accurately and optimized, since it will be used on my tower defense game. Any help would be appreaciated!
4 Likes
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.
1 Like
This might be up your alley.
2 Likes