Im trying to code a tool that is a “Dead Rat” and when activated it casts a projectile - the dead rat - at a target.
Usually what i do for fast projectiles is making it all client based, passing only the origin and direction to the other clients for them to recreate the projectile themselves, in this case though the projectile - a big rat - move slower and im having trouble to make it look nice on the other clients.
I tried many things like:
Having a projectile preloaded on server with network ownership given to the plr
Full server projectile
Full client projectile
and honestly each one of my tries shows a big problem.
What do you guys think its the better way for handling this situation?
What exactly has been your issue with the “full client projectile” method?
For all of my projectile cases, I would usually:
Client sends event to say they’ve fired a shot in X direction from Y position
Server validates the client’s request
Server replicates that to other client(s) assuming they would realistically be able to interact with and/or see the projectile
The other clients would then:
Note: If your issue is that they move at different speeds for different clients, it may be that you’re not correctly addressing time step issues - learn more about that here
Instantiate the projectile on their client - if you really want to, you could adjust the starting position of the projectile based on the ping on both the receiving client and the original client
While the projectile meets XYZ conditions, e.g. hasn’t hit anything yet, will then move the projectile via its CFrame property
This client will check to see if it hits anything, if it does, it will exit the movement and/or do any effect(s) that aren’t replicated, e.g. smoke effect when hitting a wall
In the meantime, if the original client sends a request to the server to damage another client after its projectile hit something:
Validate what the client is suggesting it hit i.e. was it possible for that player to hit the other player at that point in time knowing that it sent the last projectile from Y position towards X direction at Z time?
If it’s valid, do whatever effects that projectile is meant to do; and replicate any effect(s) that the other client(s) need to see
The problem would be, since the projectile speed is some 40*deltaTime (slow), its hard to garantee that the projectile hits the target realistically based on ping. In the last step you described, when the projectile hits something, the ping time might make the projectile on the third clients teleport far and go through characters or through any moving parts.
Also i make the rat bounce off the wall and that movement after this event is organic from roblox’s default physics - changing projectile style in the middle of the proccess causes lag too.
Do you know any good way to reduce lag on GRANADES for exemple? Might look more with what i want then gun bullets.
Looks like the best solution indeed for now compared to the alternatives, but who knows someone knows something…
Sadly this will always be the case due to the nature of networked solutions. The best you can do is just try to make it appear as close as possible.
That’s why you’re doing the collision checks on each client and accounting for the latency of each client during the request. The thing that will appear most jarring for players will be what they can actually see, so if you’re waiting for the server/some other client to tell you that the projectile hit something there’s going to be significant latency.
Here’s a working example solution:
Example Server Code
-- SERVER
local PlayerService = game:GetService('Players')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
-- const
local MAX_CHARACTER_DISTANCE = 50
-- build remotes
local networking = Instance.new('Folder')
networking.Name = 'NetworkingFolder'
networking.Parent = ReplicatedStorage
local fireRemote = Instance.new('RemoteEvent')
fireRemote.Name = 'FireActionRemote'
fireRemote.Parent = networking
local replRemote = Instance.new('RemoteEvent')
replRemote.Name = 'FireReplicationRemote'
replRemote.Parent = networking
-- utils
local function validateParams(types, ...)
for i = 1, #types do
local value = select(i, ...)
local desiredType = types[i]
if typeof(value) ~= desiredType then
return false
end
end
return true
end
local function tryGetCharacterRootPart(player)
local t = typeof(player)
if t ~= 'Instance' or not player:IsA('Player') or not player:IsDescendantOf(PlayerService) then
return false, nil
end
local character = player.Character
if not character or not character:IsDescendantOf(workspace) then
return false, nil
end
local humanoid = character:FindFirstChildOfClass('Humanoid')
if not humanoid or humanoid:GetState() == Enum.HumanoidStateType.Dead or humanoid.Health <= 0 then
return false, nil
end
local rootPart = humanoid.RootPart
if not rootPart then
return false, nil
end
return true, rootPart
end
-- handle incoming event(s)
fireRemote.OnServerEvent:Connect(function (player, position, direction)
-- confirm that the player's request body is valid
local hasValidParams = validateParams({ 'Vector3', 'Vector3' }, position, direction)
if not hasValidParams then
return false
end
-- check to see if the character is alive and whether it has a root
-- part that we can query
local isValid, rootPart = tryGetCharacterRootPart(player)
if not isValid then
return false
end
-- TODO:
-- Do some checks here to det. whether
-- the player should actually be able to fire
-- this weapon e.g. cooldown check(s) etc
-- det. whether the character is within range of the
-- position that the player alleges they fired from
local origin = rootPart.Position
local distance = (position - origin).Magnitude
if distance > MAX_CHARACTER_DISTANCE then
return false
end
-- det. whether the direction is a unit vector
-- and whether it's not NaN
local length = direction.Magnitude
if length > 1 or length ~= length then
return false
end
-- TODO:
-- Filter this players array such that
-- it only contains players that are valid
-- and that would realistically be able to see
-- this weapon being fired
local players = PlayerService:GetPlayers()
local ping = player:GetNetworkPing()
for i = 1, #players do
local client = players[i]
if client == player then
continue
end
local weaponDetails = nil -- +/- send any information about the weapon here
local processedTime = workspace:GetServerTimeNow()
replRemote:FireClient(client, player, ping, processedTime, position, direction, weaponDetails)
end
end)
Example Client Code
-- CLIENT
local RunService = game:GetService('RunService')
local PlayerService = game:GetService('Players')
local UserInputService = game:GetService('UserInputService')
local ReplicatedStorage = game:GetService('ReplicatedStorage')
-- const
local UPD_FREQUENCY = 1 / 60 -- i.e. update at 60fps
local DEBOUNCE_TIME = 0.5 -- e.g. some cooldown time
local BULLET_VELOCITY = 120 -- i.e. some bullet speed, could be taken from a weapon variable
local BULLET_LOSS_DELAY = 1 -- i.e. how long until we remove the bullet in seconds
-- deps
local player = PlayerService.LocalPlayer
local networkContainer = ReplicatedStorage:WaitForChild('NetworkingFolder')
local fireRemote = networkContainer:WaitForChild('FireActionRemote')
local replRemote = networkContainer:WaitForChild('FireReplicationRemote')
-- utils
local function fireBulletFrom(client, position, direction, timeOffset)
timeOffset = timeOffset or 0
-- e.g. can be used to det. whether we should fire 'HitCharacter' events etc
local isLocalPlayer = client == player
-- var
local startTime = os.clock() - timeOffset
local bulletGravity = Vector3.yAxis*-workspace.Gravity -- or from some weapon variable
local initialVelocity = direction*BULLET_VELOCITY
local previousPosition = position
previousPosition += initialVelocity*timeOffset + 0.5*bulletGravity*math.pow(timeOffset, 2)
local rayParams = RaycastParams.new()
rayParams.FilterType = Enum.RaycastFilterType.Exclude
rayParams.FilterDescendantsInstances = { client.Character }
local bullet = Instance.new('Part')
bullet.Size = Vector3.one + Vector3.zAxis
bullet.Shape = Enum.PartType.Block
bullet.Position = previousPosition
bullet.Anchored = true
bullet.CanTouch = false
bullet.CanQuery = false
bullet.CanCollide = false
bullet.BrickColor = BrickColor.Random()
bullet.Parent = workspace
-- bullet runtime
local lastUpdate, connection = 0, nil
connection = RunService.Stepped:Connect(function (gt, dt)
lastUpdate += dt
-- constrain our timestep, see: https://gafferongames.com/post/fix_your_timestep/
if lastUpdate < UPD_FREQUENCY then
return
end
lastUpdate = math.fmod(lastUpdate, UPD_FREQUENCY)
local now = os.clock()
local elapsedTime = now - startTime
local displacement = initialVelocity*elapsedTime + 0.5*bulletGravity*math.pow(elapsedTime, 2)
local desiredPosition = position + displacement
local result = workspace:Raycast(previousPosition, desiredPosition - previousPosition, rayParams)
local instance = result and result.Instance
if instance then
print('Hit:', instance, '@', result.Position)
-- exit our bullet runtime
if connection and connection.Connected then
connection:Disconnect()
end
-- update the desired position based on our intersection
desiredPosition = result.Position
-- either (a) let the bullet sit there for a while if we hit something anchored; or (b) remove it immediately if we hit something unanchored
if instance.Anchored then
bullet.CFrame = CFrame.new(desiredPosition, desiredPosition + displacement.Unit)
task.delay(BULLET_LOSS_DELAY, function ()
pcall(bullet.Destroy, bullet)
end)
else
pcall(bullet.Destroy, bullet)
end
if isLocalPlayer then
-- TODO:
-- send some event to the server to say we hit XYZ
-- assuming it has a humanoid etc etc
end
-- TODO:
-- Do some particle effect now that we hit something
return
end
bullet.CFrame = CFrame.new(desiredPosition, desiredPosition + displacement.Unit)
end)
end
-- handle incoming events
replRemote.OnClientEvent:Connect(function (otherClient, ping, processedTime, position, direction, weaponDetails)
local elapsed = workspace:GetServerTimeNow() - processedTime
elapsed += ping
-- fire the weapon from that client accounting for the delay
fireBulletFrom(otherClient, position, direction, elapsed)
end)
-- some example fire method
local lastFired
UserInputService.InputBegan:Connect(function (input, gameProcessed)
if gameProcessed then
return
end
local inputType = input.UserInputType
if inputType ~= Enum.UserInputType.MouseButton1 then
return
end
if lastFired and lastFired < DEBOUNCE_TIME then
return
end
lastFired = os.clock()
local character = player.Character
local rootPart = character and character.PrimaryPart
if not rootPart then
return
end
local origin = rootPart.CFrame
local position = origin.Position
local direction = origin.LookVector
-- tell the server, and other clients, that we're firing our weapon
fireRemote:FireServer(position, direction)
-- start firing the weapon for our own client
fireBulletFrom(player, position, direction, 0)
end)
Bounces can be handled on each client still, but if you’re worried about desync of moving dynamic that the bullets can bounce off then you could simulate it on the server and replicate those changes to the client so that they can sync the positions over time.
Similarly, assuming you aren’t bouncing off dynamic objects, you could always compute the projectile path in advance and then check for any intersections with players along that path when each client plays the path. For example, see this reference here.