I’ve used the same method when creating projectile weapons for a while now, it goes something like this:
--<< Client has clicked or pressed the keybind >>--
local Origin = CFrame.new(0, 0, 0) -- Origin reference
local Position = Vector3.new(0, 0, 0) -- Position reference (The mouse's position or wherever you want the projectile to end)
--<< Projecitle creation >>--
local Projectile = Instance.new("Part") -- However you create / reference the projectile
Projectile.CFrame = CFrame.lookAt(Origin.Position, Position) -- This basically makes the projectile face the end position
--<< Misc variables >>--
local Key = 0 -- I'll explain this later but the key can never be 0, so set to 0
local DT = 0 -- Set as 0 for the function later
local Speed = 100 -- Example speed for projectile
local MaxDistance = 200 -- How far the projectile can travel
--<< Invoke the server for the key but use task.spawn to avoid yielding >>--
task.spawn(function()
--<< Let me elaborate on this part, because it can seem confusing >>--
--<< The function that receives the request on the server is in charge of checks to prevent exploiters from abusing it >>--
--<< For example, is the weapon equipped? Is the player dead? Does the weapon have ammo? Is the weapon reloading? You get the point >>--
--<< It also is in charge of cooldowns, a key is not created if there is a cooldown, so nil would be returned >>--
--<< If you're dealing with a weapon with ammo or cooldowns, the function on the server should be in charge of that while also returning the key needed >>--
--<< The origin of the projectile can be referenced on the server, so a key is stored in a table with the player who initiated it and the origin's current position on the server >>--
--<< The only reason we send the origin is for visual replication on other clients >>--
Key = YourRemoteFunction:InvokeServer(Origin, Position)
end)
local function CastRay(Pos, Direction)
--<< Create ray using given pos, direction, speed and DT >>--
local Ray = workspace:Raycast(Pos, Direction * (Speed * DT))
local Distance = (Origin.Position - Pos).Magnitude
if (Ray) then
--<< Ray was hit >>--
--<< Delete the projectile >>--
Projectile:Destroy()
--<< Like I said earlier, the key can never equal 0 (at least in my system), so yield until the key does not equal 0 >>--
while (Key == 0) do task.wait() end -- Probably not the most optimal way, but it gets the job done
if (Ray.Instance ~= nil and Ray.Instance.Parent:FindFirstChild("Humanoid")) then
--<< Ray has hit a player >>--
--<< Invoke server to verify the hit >>--
--<< The function that receives this request on the server does checks as well >>--
--<< For example, is the key given valid? Does the key given belong to the player that requested verification? Does the given instance exist on the server? For round based games, is the player playing? You get the point >>--
--<< As stated earlier, the origin's initial position upon firing is stored in a table with the key on the server >>--
--<< In my system, I do a simple raycast check from the initial position to each of the player's limbs or whatever instance is sent to see if the hit is in LOS of the initial position >>--
--<< That is definitely not the most optimal way for projectiles, but if you're dealing with really fast projectiles like most FPS games, it shouldn't matter >>--
--<< If the ray hits something and the ray's instance equals the sent instance, then the hit is verified and the server deals damage >>--
if (HitDetectionFunction:InvokeServer(Key, Ray.Instance.Parent)) then
--<< Hit was verified: give player some sort of hit verification (damage indicator, etc) >>--
end
end
elseif (Distance <= Range) then
--<< Ray was not hit but projectile can still travel >>--
--<< Here, we'll update the position of the projectile >>--
Projectile.Position += Projectile.CFrame.LookVector * (Speed * DT)
DT = game:GetService("RunService").RenderStepped:Wait() -- Update DeltaTime
CastRay(Projectile.Position, Projectile.CFrame.LookVector) -- Run the function again
else
--<< Ray was not hit and the distance has maxxed out, delete the projectile >>--
Projectile:Destroy()
--<< Like I said earlier, the key can never equal 0 (at least in my system), so yield until the key does not equal 0 >>--
while (Key == 0) do task.wait() end -- Probably not the most optimal way, but it gets the job done
--<< Fire a remote to the server to delete the key stored to this projectile, as it is no longer needed >>--
YourRemoteEvent:FireServer(Key)
end
--<< Run the function for the first time >>--
CastRay(Projectile.Position, Projectile.CFrame.LookVector)
That about covers it! In short:
- Create projectile on client
- Invoke server for key exclusive to the projectile (The key is stored in a table with the origin’s initial position and the player who initiated it)
local keys = {}
keys[GivenKey] = {Player, InitialOriginPosition} -- As an example
- In the same function that receives the invoke request on the server, deal with checks such as if the player has ammo, if the player is on a cooldown, etc. Also, reduce ammo and add a cooldown (if needed) and replicate to other clients using the origin and position.
- Hit detection should be dealt on the client, it is the best way for most projectiles (Use raycasting)
- If ray hits something on the client, invoke server for verification with the key earlier
- When verifying on the server, check that the key was created by this player and if the given instance is in line of sight of the initial origin position that is stored on the server with a simple raycast. The key can now be removed from the table, as it is no longer needed. If the ray hits the same instance on the server, deal damage and return true.
- Show the player that they hit the instance with some sort of visual (Dmg indicator, etc)
I’m sorry if this was a bit lengthy! If you have any questions, let me know! There also may be a few errors in my code that I’m not seeing (I’m tired as I’m writing this )