How would I completely optimize projectiles without any latency

I’ve seen so many big developers with their games achieved this. I just can’t ever figure out what their secret recipe is. What am I talking about?

Games like “Arsenal”, “BIG Paintball”, “Phantom Forces” have guns and weapons that fires immediately without any delay from the click of the mouse to the launch of the projectile (bullets, rockets, you name it).


This is an example of a Slingshot in Arsenal.
Notice how as soon as my mouse is inputted, the ball launches out.
Complete 0 seconds of delay, and the ball does not stutter while moving.

I was trying to do the same thing as they did for many attempts, but none of them seems to work, this is my progress so far:

Local script (This is put inside a “Slingshot” tool):

-->> LOCAL <<--
local storage = game:GetService("ReplicatedStorage")
local remotes = storage.Remotes
local remote = remotes:WaitForChild("Remote_Slingshot")

local localplr = game:GetService("Players").LocalPlayer
local char = localplr.Character or localplr.CharacterAdded:Wait()
local hum = char:WaitForChild("Humanoid")
local tool = script.Parent
local animations = tool.Animations
local anim_idle = animations.Idle
local anim_fire = animations.Fire
local track_idle = hum:LoadAnimation(anim_idle)
local track_fire = hum:LoadAnimation(anim_fire)

function Equipped()
	track_idle:Play()
end
function Unequipped()
	track_idle:Stop()
end
tool.Equipped:Connect(Equipped)
tool.Unequipped:Connect(Unequipped)

function Fire()
	track_fire:Play()
	remote:FireServer()
end
tool.Activated:Connect(Fire)

Server script (This is put inside ServerScriptStorage)

--==== Weapon: Slingshot
local storage = game:GetService("ReplicatedStorage")
local remotes = storage.Remotes
local remote = remotes.Remote_Slingshot
local vec3 = Vector3
local col3 = Color3
local shape_ball = Enum.PartType.Ball
local projectilebase = Instance.new("Part")
projectilebase.Shape = shape_ball
projectilebase.Size = vec3.new(0.6,0.6,0.6)
projectilebase.Color = col3.fromRGB(115,56,33)

function FireEvent(plr)
	local char = plr.Character
	local head = char.Head
	local direction = head.CFrame.LookVector
	local p = projectilebase:Clone()
	p.Position = head.Position + direction*0.6
	p.Parent = workspace
	p:SetNetworkOwner(nil)
	p.Velocity = direction * 150 + Vector3.new(0,50,0)
end
remote.OnServerEvent:Connect(FireEvent)

I also have a RemoteEvent inside the ReplicatedStorage if you have noticed.


Using p:SetNetworkOwner(nil) seems to make the projectile stutter for a bit, so I changed the NetworkOwner to p:SetNetworkOwner(plr) and this is the result so far:
This may look smooth and does not lag, but if you watch the video carefully. You can see that after I click my mouse, it takes about 0.1 - 0.3 seconds before the projectile spawn and starts to launch.
That small number is a big problem because in combat, people with weak internet will notice their ball is not spawning and departing after they input. And when the player moves too fast, this will happen:

But if you look at the first video I posted from Arsenal, they are able to solve this problem. Proving that this is not an impossible problem, but I just can’t figure out how to replicate it.
Do you guys have solutions that can achieve the same thing?

4 Likes

The best approach here is to handle as much as it on the client as possible.

Hit detection is probably the only thing that needs to be replicated and this can be done with raycasting.

These games use a trick where you create the part and it looks like it’s been created on your side, but the server is creating it in the background without the firing client knowing, this allows it to do stuff like hit detection and movement seamlessly.

This can be done by using a RemoteFunction that returns the created part and then destroying on the client

2 Likes

Further explaining this idea…

Essentially every time you shoot, bullets and other effects are created on the client without needing to wait for the server to do things. Then as stated previously, the server will do the actual hit-detection/verification without the client knowing about it.

This is why in many games if you lose connection to the server you can still shoot/interact with the game without any issues except in cases where you would need to change something important (e.g. damage another player) – it’s all handled on the client.

Once you understand that, another aspect to consider is manually replicating things like bullets from other players to each client. In some cases, e.g. Phantom Forces, having the bullets visible on the server would appear laggy to other players (and it also negatively impacts performance).

Instead, what they do (this is an educated guess) is fire a RemoteEvent to all other players telling them some information about the bullet (e.g. start location, direction, velocity, type) and then each of the clients will see this information and create the bullet client-side using that information. These bullets will then appear smoother then if the bullets were on the server.

And to explain SetNetworkOwner, what it does is essentially reverse the roles of server and client for a specific part. If Player1 is the network owner of Part1, things such as physics will be calculated on the Player1 client and then the client will give this information to the server and all other clients. If Part1 has no network owner, then the server is exclusively in control of the part and information will be sent to all of the clients instead. This is why the bullets you have made run smoothly on the client (after being created by the server, which is what is causing the delay).

2 Likes

Just to clarify in case somebody gets confused by this, FilteredDev means the bullet is created instantly on your client without waiting for the server to explicitly tell you to create it.

I wrote a mock up of a system using your code that you could probably try going with :

This would be on the client :

local storage = game:GetService("ReplicatedStorage")
local remotes = storage.Remotes
local remote = remotes:WaitForChild("Remote_Slingshot")

local player = game:GetService("Players").LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
local hum = character:WaitForChild("Humanoid")
local tool = script.Parent
local animations = tool.Animations
local anim_idle = animations.Idle
local anim_fire = animations.Fire
local track_idle = hum:LoadAnimation(anim_idle)
local track_fire = hum:LoadAnimation(anim_fire)

local shape_ball = Enum.PartType.Ball
local projectilebase = Instance.new("Part")
projectilebase.Shape = shape_ball
projectilebase.Size = Vector3.new(0.6,0.6,0.6)
projectilebase.Color = Color3.fromRGB(115,56,33)

local function Equipped()
	track_idle:Play()
end
local function Unequipped()
	track_idle:Stop()
end
tool.Equipped:Connect(Equipped)
tool.Unequipped:Connect(Unequipped)

local function createProjectile(headpos,direction)
	local p = projectilebase:Clone()
	p.Position = headpos + direction*0.6
	p.Parent = workspace
    p.Velocity = direction * 150 + Vector3.new(0,50,0)
end

local function fireProjectile()
    local character = player.Character
    local head = character and character:FindFirstChild("Head")
    if not (head and character) then return end
    track_fire:Play()
    local headpos,direction = head.Position,head.CFrame.LookVector
    createProjectile(headpos,direction) -- Create the projectile on the client then create it for other clients
    remote:FireServer(headpos,direction)
end

remote.OnClientEvent:Connect(createProjectile) -- creating the projectile if another client fired

This would be on the server :

local storage = game:GetService("ReplicatedStorage")
local players = game:GetService("Players")

local remotes = storage.Remotes
local remote = remotes:WaitForChild("Remote_Slingshot")

local shape_ball = Enum.PartType.Ball
local projectilebase = Instance.new("Part")
projectilebase.Shape = shape_ball
projectilebase.Size = Vector3.new(0.6,0.6,0.6)
projectilebase.Color = Color3.fromRGB(115,56,33)

function shotProjectile(player,headpos,direction)
    local character = player.Character
    local head = character and character:FindFirstChild("Head")
    if not (head and character) then return end -- can't shoot if no head and character??
    -- add your own bullet validation logic here 
    for _,v in ipairs(players:GetPlayers()) do
        if v ~= player then
            remote:FireClient(v,head.Position,head.CFrame.LookVector) -- create the pojectile for other clients
        end
    end
    -- damage validation and stuff use the headpos and direction from the client and server
    -- then try compensating between both of them and validating the shot :D 
end

remote.OnServerEvent:Connect(shotProjectile)

The idea is that you take the information the client sent, create the projectile on it’s screen to avoid latency for visuals and then send a request to the server with the said info, on the server you take the information validate it then create it for other clients ( visuals should always be handled on the client )
after creating the visuals on other clients just take the information from the server and from the client and try compensating between the two because of “latency” then handle damage.

Hope this helps !

2 Likes

Thanks for the help. But there are things that I noticed when I use it.

  • Only players that have the slingshot can see the projectile from other players cause the OnServerClient is in the LocalScript inside the tool.
  • The projectile moves really sketchy on the other player’s screen. It’s really offset because Roblox Physics is funky when things don’t sync together for 1 nanosecond.

What do you mean with moves really sketchy on other player’s screen could you show a video please?
Also the remote.OnClientEvent is supposed to be in another script or you use one script for the tools ( sorry didn’t tell you )


Player1 shoots Player2 and damages him but Player2 moved and still get damaged for no reason.
Should we blame Player2 for having a bad internet?

Also what method would you use for the server to damage Player2 so that exploiters don’t just spam the RemoteEvent and run scripts to move the projectiles inside other players and damage everybody.

Sadly there is no actual way to combat latency to where each player screen will be 100% synced with the server.
If you look at other games that are FPS or have to do with shooting like CS:GO, Fortnite, Overwatch and such they all suffer from that issue.
This is where the fun part comes in, yes the client with the bad internet is blamed and has to suffer “lag” issues or “damage” issues. Damage is handled on the server so it is NOT asking either of the clients where the damaged player is so It is always truthy. Now CS:GO or valve combats this using “Lag Compensation” which is a completely different topic to cover.
The issue with lag compensation is that you are going to let the client tell you where the player is located when shot that is all. ( aka they can fake lag and stuff )
If you want to read more about it : https://developer.valvesoftware.com/wiki/Lag_compensation ; https://developer.valvesoftware.com/wiki/Source_Multiplayer_Networking#Lag_compensation

What phantom forces does for damage ( assuming ), they take as much client input as possible and validate it all on the server including but not limited to : ray,hitpart,hitpos,currentpos,targetpos,gunpos,lookvector etc.
And they just do validation on the server like : is the lookvector actually facing the position of the damaged player? is the gun position correct? is the target position correct? is the current position correct? what’s the hitpart and is the player able to damage it? are there any obstacles that are in the way?
and apply damage accordingly.
Lastly I just wanted to say that THERE IS NO WAY to fully stop exploiters when there is client input, be whoever you want to be you WILL NOT be able to stop 100% of exploiters when using client input, because at the end of the day shooting games have to rely on client input in order to function.

2 Likes

Sorry for the bump, so you guys are saying creating a “fake part” on the client which moves until the server is done creating the part, when the server is done creating the part the client destorys its fake part?