Projectile replication delay compensation with FastCast

Enemy Fireball Script
local Players = game:GetService("Players")
local ReplicatedStorage = game:GetService("ReplicatedStorage")

local Quire = require(ReplicatedStorage:WaitForChild("Shared").Quire)
local ProjectileReplication_Server = Quire("ProjectileReplication_Server")

local tool = script.Parent
local handle = tool.Handle
local character = tool.Parent
local humanoid = character.Humanoid
local rightHand = character.RightHand

local workspaceProjectiles = workspace.Projectiles

local statsFolder = tool.Stats
local SPEED = statsFolder.Speed.Value
local RANGE = statsFolder.Range.Value
local DAMAGE = statsFolder.Damage.Value
local BULLET_TYPE = statsFolder.BulletType.Value
local FIREBALL_OFFSET = statsFolder.FireballOffset.Value
local BULLET_ACCELERATION = statsFolder.BulletAcceleration.Value
local COOLDOWN_TIME = statsFolder.CooldownTime.Value

local stateFolder = character.State
local isAttackingValue = stateFolder.IsAttacking
local targetObjectValue = stateFolder.Target



local shooting = false

local function shoot()
	if shooting then return end
	shooting = true
	
	while targetObjectValue.Value and shooting do
		local targetPrimary = targetObjectValue.Value.PrimaryPart
		
		local origin = handle.Position + FIREBALL_OFFSET
		local direction = (targetPrimary.Position - origin).Unit
		local speed = (direction * SPEED) + targetPrimary.Velocity
		
		local projectileData = {
			bulletType = BULLET_TYPE,
			owner = nil,
			enemyOwned = true,
			damage = DAMAGE		
		}
	
		local castArgs = {
			origin = origin,
			directionWithMagnitude = direction * RANGE,
			velocity = speed,
			blacklist = {character, workspaceProjectiles},
			cosmeticBulletObject = nil,
			ignoreWater = true,
			bulletAcceleration = BULLET_ACCELERATION,
			canPierceFunction = function(hitPart, hitPoint, normal, material)
				if Players:GetPlayerFromCharacter(hitPart.Parent) then
					return true
				end
				return false
			end
		}	
	
		ProjectileReplication_Server.fireProjectile(projectileData, castArgs)
		wait(COOLDOWN_TIME)
	end
end	


isAttackingValue.Changed:Connect(function()
	if not shooting and isAttackingValue.Value then
		shoot()
	elseif shooting and not isAttackingValue.Value then
		shooting = false
	end
end)
ProjectileReplication_Client Module
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")

local DEBUG_RAYS_ENABLED = true

local Quire = require(ReplicatedStorage:WaitForChild("Shared").Quire)
local NetworkUtil = Quire("NetworkUtil")
local Caster = Quire("FastCast").new()

local projectileReplicationEvent = NetworkUtil:GetRemoteEvent("ProjectileReplicationEvent")
local projectileFolder = ReplicatedStorage:WaitForChild("Projectiles")
local workspaceProjectiles = workspace:WaitForChild("Projectiles")

local projectileList = {
	Fireball = {
		cosmeticBulletObject = projectileFolder:WaitForChild("FireballPart"),
		fadeTweenInfo = TweenInfo.new(.2),
		fadeTweenProperties = {Transparency = 1}
	}
}

local ProjectileReplication_Client = {}

function ProjectileReplication_Client.fireProjectile(projectileData, castArgs, startTime)
	local latency = tick() - startTime
	print("latency", latency)
	if projectileData.bulletType == "Fireball" then
		local newBullet = projectileList.Fireball.cosmeticBulletObject:Clone()
		newBullet.Parent = workspaceProjectiles
		castArgs.cosmeticBulletObject = newBullet
	end 
	
	Caster:FireWithBlacklist(
		projectileData, castArgs.origin, castArgs.directionWithMagnitude, castArgs.velocity, castArgs.blacklist,
		castArgs.cosmeticBulletObject, castArgs.ignoreWater, castArgs.bulletAcceleration, castArgs.canPierceFunction
	)
end


Caster.RayHit:Connect(function(projectileData, hitPart, hitPoint, normal, material, cosmeticBulletObject)
	if projectileData.bulletType == "Fireball" then
		local projectileInfo = projectileList.Fireball
		local fadeTween = TweenService:Create(cosmeticBulletObject, projectileInfo.fadeTweenInfo, projectileInfo.fadeTweenProperties)
		fadeTween:Play()
		fadeTween.Completed:Wait()
	end
	cosmeticBulletObject:Destroy()
end)

Caster.LengthChanged:Connect(function(projectileData, origin, segmentOrigin, segmentDirection, length, cosmeticBulletObject)
	local baseCF = CFrame.new(segmentOrigin, segmentOrigin + segmentDirection)
	local projectHalfSize = cosmeticBulletObject.Size.Z/2
	cosmeticBulletObject.CFrame = baseCF * CFrame.new(0, 0, -(length - projectHalfSize))	
	
	if DEBUG_RAYS_ENABLED then
		local part = Instance.new("Part")
		part.Anchored = true
		part.CanCollide = false
		part.BrickColor = BrickColor.Green()
		part.Size = Vector3.new(1.2, .8, length)
		part.CFrame = baseCF
		part.Parent = workspaceProjectiles
		Debris:AddItem(part, 5)
	end	

end)

projectileReplicationEvent.OnClientEvent:Connect(ProjectileReplication_Client.fireProjectile)

return ProjectileReplication_Client
ProjectileReplication_Server Module
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")

local DEBUG_RAYS_ENABLED = true

local Quire = require(ReplicatedStorage:WaitForChild("Shared").Quire)
local NetworkUtil = Quire("NetworkUtil")
local Caster = Quire("FastCast").new()


local projectileReplicationEvent = NetworkUtil:GetRemoteEvent("ProjectileReplicationEvent")
local projectileFolder = ReplicatedStorage:WaitForChild("Projectiles")
local workspaceProjectiles = workspace:WaitForChild("Projectiles")


local function fireAllClientsExcept(clientToIgnore, ...)
	for _, client in ipairs(Players:GetPlayers()) do
		if client ~= clientToIgnore then
			projectileReplicationEvent:FireClient(client, ...)
		end
	end
end


local ProjectileReplication_Server = {}

function ProjectileReplication_Server.fireProjectile(projectileData, castArgs)
	local projectileOwner = projectileData.owner
	
	local now = tick()
	fireAllClientsExcept(projectileOwner, projectileData, castArgs, now)
	
	Caster:FireWithBlacklist(
		projectileData, castArgs.origin, castArgs.directionWithMagnitude, castArgs.velocity, castArgs.blacklist,
		castArgs.cosmeticBulletObject, castArgs.ignoreWater, castArgs.bulletAcceleration, castArgs.canPierceFunction
	)
		
	
end


Caster.RayHit:Connect(function(projectileData, hitPart, hitPoint, normal, material, projectile)
	if projectileData.bulletType == "Fireball" then
		local humanoid = hitPart and hitPart.Parent:FindFirstChild("Humanoid")
		if not humanoid then return end
		humanoid.Health = humanoid.Health - projectileData.damage
	end
end)

Caster.LengthChanged:Connect(function(projectileData, origin, segmentOrigin, segmentDirection, length, cosmeticBulletObject)
	local baseCF = CFrame.new(segmentOrigin, segmentOrigin + segmentDirection)
	if DEBUG_RAYS_ENABLED then
		local part = Instance.new("Part")
		part.Anchored = true
		part.CanCollide = false
		part.BrickColor = BrickColor.Blue()
		part.Size = Vector3.new(1, 1, length)
		part.CFrame = baseCF
		part.Parent = workspaceProjectiles
		Debris:AddItem(part, 5)
	end
end)


return ProjectileReplication_Server

Enemy AI shoots a projectile calling ProjectileReplication_Server.fireProjectile(projectileData, castArgs) and then fires a remote event to all players telling them to call ProjectileReplication_Client.fireProjectile(projectileData, castArgs, startTime) so the client can make their own projectile for visuals

note: the enemy shoots where the target is moving depending on the velocity of the target

The server does hit detection for dealing damage, and doesn’t render a projectile object, the client renders a projectile for visuals, and does hit detection to remove the fireball object on hit

doing it on client allows for smooth projectiles, but I’m having issues with delay, the server hits a character, but the client slightly usually misses

blue is the server ray and green is the client, the server ray hits, but the client ray does not

it works as expected if you’re not moving, or not change directions

I have two ideas of my own so far, the first one is passing the time the attack was fired with the remote event, and adjusting the client projectile based on that, but I’m not sure how to go about doing that

local latency = tick() - startTime
print("latency", latency)

my second idea is to do all the hit detection on the client, but that isn’t very secure

any help is appreciated, the only scripts worth looking at are the 3 at the top of this post, but I’ll include a place file for anyone who wants to test themselves

ProjectileReplicationRepro.rbxl (88.0 KB)

Edit: updated images with gifs, and made the debug rays straighter

8 Likes

Well after reading, I can’t really target what the issue is.

1 Like

The goal is for the projectile to fade and remove when it hits a character, I don’t want it to pierce

I’m standing in your exact same situation right now with my game. I’m also rendering all my graphics on the client and have the server do the damage control but there’s a noticeable delay. I will stay around for help or possibly share my own experiences as I continue to come up with new ideas and post them here :slight_smile:

2 Likes

It could be a little late on response, can you make a remote that is fired once a second or like .1 - .2 so that the client knows where its supposed to be aiming?

That won’t work with FastCast, the cast args only need to be sent to client once, but I’m just trying to figure out how to compensate for the slight delay

1 Like

After some testing with another person, I noticed that the projectiles sync near perfectly if you’re viewing from another client, and the only delay is if you’re the one getting shot at

Does anyone know why this is?

Like I said in many posts, there isn’t really any actual way to “combat” latency.
Maybe though, if you were to send the hitpart ( if it hit a client ) and then adjust to the hit part’s updated position on the client when showing the visual?

or you could both raycast on server and client, use the client raycast for visuals and use the server raycast for damage/hit detection.

Some of the posts where I talked about this :

Hope this helps you!

I’m already doing visuals on client and hit detection on server

Can’t you just remove the projectile on the client when the server detects the hit? You said you’re doing hit detection on the server, so just fire an event to clients on hit to tell them to destroy the projectile.

It would still not look right, as the projectile would just randomly disappear instead of hitting their character and disappearing

I’m gonna try re calculating on the client, rather than just sending the ones the server calculated

It’s a good compromise, I use it in my game and it looks fairly consistent. I am using my own module and projectile speeds might not be as high, but it appears fine to clients.
Bottom line is there isn’t really a way to fix this latency problem without actually changing the path of the projectile. The client is always gonna be ahead of the server by a bit.

Edit: I realize your projectiles don’t exactly work the same way as mine since your projectiles need a predefined target, in that case you can just calculate the path on the client.

1 Like

Recalculating the origin, direction, and speed on the client being attacked, rather than using the ones the server calculated works perfectly

Before I was passing the origin, direction, and speed to all clients to render visuals, and that usually works perfectly, but has more delay if you’re the one being attacked

5 Likes

I think that’s what @BasedFX suggested to you so you should probably mark his answer as correct instead of marking your own reply; not trying to be rude or anything, just letting you know.

1 Like

I read his post, he didn’t say anything I wasn’t already doing, and I believe he added more stuff in an edit after I marked the solution (talking to him in dms he said he didnt edit after)

I was already handling visuals on the client, the only thing I’m doing differently now is recalculating on the client who is being attacked