Client-Server Projectile

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

https://devforum.roblox.com/t/archived-securecast-server-authoritative-projectiles-with-lag-compensation-multi-threading-and-more/

This might be up your alley.

2 Likes