Is it possible to create this effect with my projectiles

Hello
So right now I am working on projectiles. What I am trying to do is make the server do the actual collision detection, and the client will render the projectile in sync with the server. Right now, I have the exact effect I want, but it only renders to the client that shot the projectile and nobody else. The server calculates it all, and the client has a part move perfectly every frame to match the server simulation. I am kind of stumped and I dont know how Im gonna render it to the other clients without some sort of delay or making the projectile not move perfectly. I will provide the scripts, hopefully you guys have ideas.

Localscript:

local replicatedStorage = game:GetService("ReplicatedStorage")

local weaponModules = replicatedStorage.WeaponModules
local projectileModule = require(weaponModules.ProjectileModule)

local projectile = replicatedStorage.Projectiles.Paintball

local player = game:GetService("Players").LocalPlayer

local mouse = player:GetMouse()

local tool = script.Parent
local fire = tool.Fire
local attachment = tool.Handle.Attachment

local weaponType = tool.Name

local function activated()
	fire:FireServer(mouse.Hit.Position,weaponType)
	
	local blacklist = tool.Parent:GetDescendants()
	
	local projectileProperties = {1,blacklist,Vector3.new(0,0,0),8,0,0,projectile,projectile.Size.Z/2}
	
	local projectile = projectileModule.new(table.unpack(projectileProperties))
	projectile:CastOnClient(attachment.WorldPosition,mouse.Hit.Position,7)
end

tool.Activated:Connect(activated)

Script:

local replicatedStorage = game:GetService("ReplicatedStorage")

local weaponModules = replicatedStorage.WeaponModules
local projectileModule = require(weaponModules.ProjectileModule)

local projectile = replicatedStorage.Projectiles.Paintball

local tool = script.Parent
local attachment = tool.Handle.Attachment
colors = {45, 119, 21, 24, 23, 105, 104}

tool.Fire.OnServerEvent:Connect(function(player,hit,weaponType)
	if weaponType == tool.Name then
		tool.Handle.Fire:play()

		local spawnCFrame = attachment.WorldPosition
		
		local blacklist = tool.Parent:GetDescendants()
		
		local projectileProperties = {1,blacklist,Vector3.new(0,0,0),8,0,0,projectile,projectile.Size.Z/2}
		
		local projectile = projectileModule.new(table.unpack(projectileProperties))
		projectile:CastOnServer(spawnCFrame,hit,7)
	end
end)

Module:

local replicatedStorage = game:GetService("ReplicatedStorage")

local debrisService = game:GetService("Debris")
local runService = game:GetService("RunService")

local projectile = {}
 
function projectile.new(gravity,blacklist,alternateForce,despawnTime,maxBounces,decay,projectileType,radius)
	local newProjectile = {}
	
	newProjectile.Gravity = gravity
	newProjectile.AlternateForce = alternateForce
	newProjectile.DespawnTime = despawnTime
	newProjectile.MaxBounces = maxBounces
	newProjectile.Decay = decay
	newProjectile.Radius = radius
	
	local params = RaycastParams.new()
	params.FilterType = Enum.RaycastFilterType.Exclude
	params.FilterDescendantsInstances = blacklist
	newProjectile.Params = params
	
	function newProjectile:CastOnClient(start,destination,force)
		local conversion = 196.2/9.8
		local velocity = (destination - start).Unit * force * conversion

		local a = Vector3.new(self.AlternateForce.X,self.AlternateForce.Y -self.Gravity * 9.8,self.AlternateForce.Z) * conversion

		local t = 0
		local totalTime = 0
		local bounces = 0
		local currentVelocity = velocity
		local currentPosition = start
		local raycastResult = nil
		local found = false

		local currentVelocity
		
		local projectiles = {}
		local projectile = projectileType:Clone()
		projectile.Parent = workspace.Projectiles

		local connection = runService.RenderStepped:Connect(function(dt)
			if not found then
				t += dt
				totalTime += dt

				currentVelocity = velocity + a * t

				local projectilePosition = Vector3.new(
					start.X + velocity.X * t + 0.5 * a.X * t * t,
					start.Y + velocity.Y * t + 0.5 * a.Y * t * t,
					start.Z + velocity.Z * t + 0.5 * a.Z * t * t
				)

				raycastResult = workspace:Raycast(currentPosition,projectilePosition - currentPosition,self.Params)

				if projectile then
					projectile.Position = currentPosition
				end

				currentPosition = projectilePosition

				if raycastResult then
					print("ok")
					if bounces >= self.MaxBounces then
						found = true
					else
						local normal = raycastResult.Normal

						velocity = currentVelocity - 2 * currentVelocity:Dot(normal) * normal

						start = raycastResult.Position
						currentPosition = raycastResult.Position

						t = 0
						bounces += 1
					end
				end
				if totalTime > self.DespawnTime then
					found = true
				end
			end
		end)

		while not found do
			wait()
		end

		connection:Disconnect()
		
		projectile:Destroy()
	end
	
	function newProjectile:CastOnServer(start,destination,force)
		local conversion = 196.2/9.8
		local velocity = (destination - start).Unit * force * conversion
		
		local a = Vector3.new(self.AlternateForce.X,self.AlternateForce.Y -self.Gravity * 9.8,self.AlternateForce.Z) * conversion
		
		local t = 0
		local totalTime = 0
		local bounces = 0
		local currentVelocity = velocity
		local currentPosition = start
		local raycastResult = nil
		local found = false
		
		local currentVelocity
		
		local connection = runService.Heartbeat:Connect(function(dt)
			if not found then
				t += dt
				totalTime += dt
				
				currentVelocity = velocity + a * t
				
				local projectilePosition = Vector3.new(
					start.X + velocity.X * t + 0.5 * a.X * t * t,
					start.Y + velocity.Y * t + 0.5 * a.Y * t * t,
					start.Z + velocity.Z * t + 0.5 * a.Z * t * t
				)
				
				raycastResult = workspace:Spherecast(currentPosition,self.Radius,projectilePosition - currentPosition,self.Params)
				print(raycastResult)
				
				currentPosition = projectilePosition
				
				if raycastResult then
					print("ok")
					if bounces >= self.MaxBounces then
						found = true
					else
						local normal = raycastResult.Normal
						
						velocity = currentVelocity - 2 * currentVelocity:Dot(normal) * normal

						start = raycastResult.Position
						currentPosition = raycastResult.Position
						
						t = 0
						bounces += 1
					end
				end
				if totalTime > self.DespawnTime then
					found = true
				end
			end
		end)
		
		while not found do
			wait()
		end
		
		connection:Disconnect()
		
		if raycastResult then
			print("Detected")
		end
	end
	
	return newProjectile
end

return projectile

Is this a lost cause? I dont know but any help would be appreciated.

:white_check_mark: Solution Overview

Make All Clients Render the Projectile

You need to:

  1. Have the server broadcast projectile data (start, direction, force, etc.) to all clients.
  2. Each client will then run their own local CastOnClient(...) using that data, just like the shooter does.

How to Do It

Step 1: Create a RemoteEvent for Projectile Broadcast

In ReplicatedStorage, create a new RemoteEvent called ReplicateProjectile.

lua

CopyEdit

-- ReplicatedStorage.ReplicateProjectile (RemoteEvent)

###Step 2: Modify the Server Script to Broadcast

Update the CastOnServer logic in the Script to do this after starting the server simulation:

-- Inside Script (server-side)
replicatedStorage.ReplicateProjectile:FireAllClients(spawnCFrame, hit, 7, weaponType)

So your OnServerEvent becomes:

tool.Fire.OnServerEvent:Connect(function(player, hit, weaponType)
	if weaponType == tool.Name then
		tool.Handle.Fire:Play()

		local spawnCFrame = attachment.WorldPosition
		local blacklist = tool.Parent:GetDescendants()
		
		local projectileProperties = {1, blacklist, Vector3.new(0, 0, 0), 8, 0, 0, projectile, projectile.Size.Z / 2}
		local projectile = projectileModule.new(table.unpack(projectileProperties))
		
		projectile:CastOnServer(spawnCFrame, hit, 7)

		-- Broadcast to all clients including the shooter
		replicatedStorage.ReplicateProjectile:FireAllClients(spawnCFrame, hit, 7, weaponType)
	end
end)

Step 3: Listen to Replication on All Clients

Now, in your LocalScript, add a handler to ReplicateProjectile:

replicatedStorage.ReplicateProjectile.OnClientEvent:Connect(function(startPos, destination, force, weaponType)
	if weaponType == tool.Name then
		local blacklist = tool.Parent:GetDescendants()
		local projectileProperties = {1, blacklist, Vector3.new(0, 0, 0), 8, 0, 0, projectile, projectile.Size.Z / 2}
		local proj = projectileModule.new(table.unpack(projectileProperties))
		proj:CastOnClient(startPos, destination, force)
	end
end)

This ensures everyone sees the projectile, not just the shooter.

:white_check_mark: Since you’re doing all collision server-side, clients only care about rendering and don’t affect game logic — which is perfect for anti-exploit protection and smooth visuals.

I’m just curious, is there a way to do it without the delay of remote events? Couldn’t invoke server work?

You’re asking a great question about optimizing network communication for projectiles in Roblox! The core of it boils down to: how do we minimize the perceived delay for players while maintaining server authority for critical game logic (like hit detection and damage)?

First, let’s address your question directly: “Could invoke server work?”

RemoteFunctions (InvokeServer) vs. RemoteEvents (FireServer/FireAllClients):

  • RemoteEvents (FireServer, FireAllClients, OnClientEvent, OnServerEvent):
    • One-way communication: The sender doesn’t expect an immediate return value.
    • Asynchronous: The sending script continues to run without waiting for the receiver to process the event.
    • Good for: Notifying, triggering actions, sending data where a direct response isn’t immediately needed.
    • FireAllClients: Only available on the server to send data to all connected clients.
  • RemoteFunctions (InvokeServer, OnServerInvoke, InvokeClient, OnClientInvoke):
    • Two-way communication: The sender expects a return value from the receiver.
    • Synchronous: The sending script yields (pauses) until the receiver processes the function and returns a value.
    • Good for: Requesting data, performing server-side calculations that a client needs the result of before continuing.
    • InvokeClient is generally discouraged from the server to client: If a client fails to respond (e.g., due to lag, exploit, or disconnection), the server script will yield indefinitely, potentially freezing parts of your game logic.

Regarding your projectile system:

You’re trying to replicate a visual projectile to all clients. For this, RemoteEvents (FireAllClients) are the correct choice.

Why RemoteEvents and not RemoteFunctions for projectile replication?

  1. Delay: Even with a RemoteFunction, there’s still network latency. The delay comes from the time it takes for data to travel from the server to each client. InvokeServer would simply force your server script to wait for the client to render the projectile and return something, which is unnecessary and inefficient for visual replication. You want the server to fire and forget for visuals.
  2. Broadcasting: RemoteFunctions are designed for one-to-one communication (a client invokes the server, or the server invokes one client). To tell all clients about a projectile, you’d have to loop through game.Players:GetPlayers() and InvokeClient on each, which would be incredibly slow and prone to freezing the server if any client lags. FireAllClients is built precisely for this broadcast scenario.
  3. Visual vs. Logic: Your current approach correctly separates visual rendering (client-side) from collision/damage logic (server-side). The server needs to tell clients to render, but it doesn’t need to wait for them to do it.

Addressing “without the delay of remote events” for the shooter’s perspective:

This is where Client-Side Prediction / Client-Side Visuals comes in.

The solution you’ve been working with already implements a key part of this:

  1. Client (shooter) fires:
  • Immediately: The client renders the projectile for itself (proj:CastOnClient). This makes the action feel instant and responsive to the player.
  • Sends to Server: The client also fires a RemoteEvent (FireServer) to the server, providing the necessary data for validation and server-side hit detection.
  1. Server receives and validates:
  • The server performs CastOnServer for hit detection.
  • Broadcasts to other clients: The server then uses FireAllClients to tell everyone else (including the original shooter, as you’ve set it up) to also render the projectile.

The “delay” you perceive is primarily for other players seeing your projectile. The shooter doesn’t experience this delay for their own projectile because they draw it instantly on their client.

How to further mitigate the perceived delay for other players (advanced techniques):

While RemoteEvents are the fastest way to broadcast, there’s always network latency. For games that require extremely precise and smooth projectile replication (like competitive FPS games), developers employ techniques like:

  1. Timestamping:
  • When the client fires the FireServer event, it can also send a tick() or os.clock() timestamp.
  • The server receives this timestamp and can calculate how long ago the shot was perceived to be fired by the client.
  • When the server FireAllClients, it includes this timestamp.
  • Clients receiving the event can then calculate how “ahead” or “behind” they are from the intended projectile path and potentially “fast-forward” the projectile’s visual slightly to try and synchronize it with the server’s authoritative path. This is complex to implement perfectly.
  1. Prediction and Correction:
  • The client that fires the projectile predicts its path.
  • The server also calculates the projectile’s authoritative path.
  • If the server’s path significantly deviates from the client’s prediction (due to lag or discrepancy), the client might “snap” its visual projectile to the server’s authoritative position. This can lead to visual “jumps” but ensures consistency.
  1. Unreliable Remote Events (for visual-only, high-frequency data):
  • Roblox offers UnreliableRemoteEvents. These are not guaranteed to arrive and can be dropped by the network if there’s congestion.
  • Pros: Can sometimes be used for very high-frequency visual updates (e.g., bullet tracers in a fast-paced shooter) where missing a few packets isn’t catastrophic.
  • Cons: Not suitable for critical data (like initial projectile spawn, hit confirmations) because data loss would be detrimental. For your current ReplicateProjectile event, you want it to be reliable so every client does see the projectile.

The approach outlined in your “Solution Overview” (client fires FireServer, server does CastOnServer and FireAllClients for visuals) is the standard and recommended way to handle projectiles in Roblox. It prioritizes instant feedback for the shooter and server authority for gameplay-critical elements.

You cannot truly eliminate network delay, but you can manage how it’s perceived. For visual projectile replication, RemoteEvents are the most appropriate and efficient choice. RemoteFunctions would introduce unnecessary yielding and complexity for this use case.

ChatGPT goes crazy hard!!! holy moly

3 Likes

Could you show an implementation of the timestamping strategy?

You’re right to want to dive into the timestamping strategy! It’s a fundamental technique for improving the perceived smoothness of replicated events in networked games.

The core idea is to measure the exact time an event occurred on the client that initiated it, send that timestamp to the server, and then have the server broadcast that timestamp to all other clients. When other clients receive the event, they can then calculate how “old” the event is and fast-forward their local simulation of the projectile to compensate for network latency.

Let’s modify your existing code structure to incorporate this.

Assumptions:

  • You have ReplicatedStorage.ReplicateProjectile (RemoteEvent).
  • Your projectileModule has a new function that takes properties, and CastOnClient takes startPos, destination, force. It should ideally also take a timeOffset or similar parameter to “fast-forward” the projectile.
  • Your CastOnClient function currently moves the projectile based on RunService.Heartbeat or similar, using deltaTime.

Step 1: Modify the Client Script (LocalScript)

When the shooter fires, they’ll send their local timestamp to the server.

– Inside LocalScript (client-side)
– Assuming ‘tool’, ‘attachment’, ‘replicatedStorage’, ‘projectileModule’, ‘projectile’ are defined as in your original setup

– This is the event the client fires to the server to request a projectile

local replicateProjectileEvent = replicatedStorage.ReplicateProjectile -- Your broadcast RemoteEvent from server

-- Your projectile properties (assuming these are constants or determined locally)
local localProjectileProperties = {1, {}, Vector3.new(0, 0, 0), 8, 0, 0, projectile, projectile.Size.Z / 2}
-- The blacklist will be set dynamically, hence empty table for now

-- Function to handle local projectile casting for the shooter
local function castLocalProjectile(spawnCFrame, destination, force)
    -- Update blacklist for local rendering
    localProjectileProperties[2] = tool.Parent:GetDescendants()
    local proj = projectileModule.new(table.unpack(localProjectileProperties))
    proj:CastOnClient(spawnCFrame, destination, force)
end

-- Listen to player input (e.g., MouseButton1Click for firing)
tool.Activated:Connect(function()
    local player = game.Players.LocalPlayer
    local character = player.Character
    if not character then return end

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid or humanoid.Health <= 0 then return end

    tool.Handle.Fire:Play()

    local spawnCFrame = attachment.WorldPosition -- Or tool.Handle.CFrame.Position etc.
    local mouse = player:GetMouse()
    local hit = mouse.Hit.Position -- Or raycast hit position

    local force = 7 -- Example force value

    -- *** CLIENT-SIDE PREDICTION: Render projectile immediately for the shooter ***
    castLocalProjectile(spawnCFrame, hit, force)

    -- *** Send data to server, including the *client's* current time ***
    -- Using workspace:GetServerTimeNow() is best practice for client-server time synchronization
    -- because it's the server's time as perceived by the client, which factors in ping.
    -- If you use tick() or os.clock(), you're using client's local machine time,
    -- which won't be in sync with the server's time or other clients' times.
    fireRemoteEvent:FireServer(spawnCFrame, hit, force, tool.Name, workspace:GetServerTimeNow())
end)

-- *** Listen to projectile replication from the server ***
```replicateProjectileEvent.OnClientEvent:Connect(function(spawnCFrame, destination, force, weaponType, serverFiredTime)```
    -- Ensure this client didn't initiate the shot (to avoid double-rendering for the shooter)
    -- This relies on `tool.Name` being unique or some other identifier
    -- However, the original solution said "including the shooter", so this check might be removed
    -- if you truly want server to be the only source of truth for all clients, even the shooter.
    -- For timestamping, it's often better if the shooter *doesn't* render the server's replicated projectile.
    -- But since your original request implies the shooter does receive it, let's keep the check for others.
    -- The shooter already drew it instantly.

    if weaponType == tool.Name then -- Make sure it's for this specific weapon type
        local player = game.Players.LocalPlayer
        local character = player.Character
        if not character or character == game.Players.LocalPlayer.Character then
            -- This is the shooter's client, they already rendered it instantly.
            -- We don't need to re-render or fast-forward for them.
            return
        end
    end

    -- Calculate how much time has passed since the server *fired* this event
    local currentTime = workspace:GetServerTimeNow()
    local timePassed = currentTime - serverFiredTime

    -- *** Fast-forward the projectile's visual by 'timePassed' ***
    -- This is the key: tell the projectile module to jump ahead in its simulation
    local newProjectileProperties = {1, tool.Parent:GetDescendants(), Vector3.new(0, 0, 0), 8, 0, 0, projectile, projectile.Size.Z / 2}
    local proj = projectileModule.new(table.unpack(newProjectileProperties))
    proj:CastOnClient(spawnCFrame, destination, force, timePassed) -- Pass timePassed to CastOnClient
end)`
---

### **Step 2: Modify the Server Script (Script)**

The server receives the client's `serverFiredTime` and broadcasts it to everyone else.

-- Inside Script (server-side)
-- Assuming 'tool', 'attachment', 'replicatedStorage', 'projectileModule', 'projectile' are defined
local replicatedStorage = game:GetService("ReplicatedStorage") -- Make sure this is defined

-- Assuming 'tool.Fire' is your RemoteEvent for client -> server
```tool.Fire.OnServerEvent:Connect(function(player, spawnCFrame, hit, force, weaponType, clientFiredTime)
    if weaponType == tool.Name then
        tool.Handle.Fire:Play()

        local blacklist = tool.Parent:GetDescendants()

        local projectileProperties = {1, blacklist, Vector3.new(0, 0, 0), 8, 0, 0, projectile, projectile.Size.Z / 2}
        local projectileInstance = projectileModule.new(table.unpack(projectileProperties)) -- Renamed to avoid conflict

        projectileInstance:CastOnServer(spawnCFrame, hit, force)

        -- *** Broadcast to all clients, including the original client's timestamp ***
        -- We use the `clientFiredTime` received from the client for consistency.
        -- This `clientFiredTime` is already in the server's time scale (as per workspace:GetServerTimeNow()).
        replicatedStorage.ReplicateProjectile:FireAllClients(spawnCFrame, hit, force, weaponType, clientFiredTime)
    end
end)

Step 3: Modify your projectileModule (CastOnClient)

Your CastOnClient function needs to be able to “fast-forward” the projectile. This typically involves calculating the projectile’s position after timeOffset seconds and starting its visual simulation from that point.

– Inside your projectileModule (ModuleScript)


local ProjectileModule = {}

-- Example simplified projectile constructor (adjust to your actual module)
function ProjectileModule.new(initialProperties)
    local self = {}
    self.initialProperties = initialProperties
    self.speed = initialProperties[4] -- Assuming speed is at index 4

    -- You might have a visual part created here
    self.visualPart = Instance.new("Part")
    self.visualPart.Size = initialProperties[7].Size -- Or whatever your projectile visual is
    self.visualPart.BrickColor = BrickColor.new("Really red")
    self.visualPart.CanCollide = false
    self.visualPart.Anchored = false -- Must not be anchored to move
    self.visualPart.Parent = workspace -- Or a dedicated folder

    return self
end

– Critical change: Add ‘timeOffset’ parameter to CastOnClient

    timeOffset = timeOffset or 0 -- Default to 0 if no offset is provided

    self.visualPart.Position = startPos
    self.visualPart.CFrame = CFrame.new(startPos, destination) -- Point towards destination

    local direction = (destination - startPos).Unit
    local initialVelocity = direction * self.speed

    local currentPosition = startPos

    -- *** Calculate initial "fast-forwarded" position ***
    -- This is a simplified linear motion. For parabolic motion (gravity), it's more complex.
    -- For linear, it's just: Position = InitialPosition + Velocity * Time
    currentPosition = startPos + initialVelocity * timeOffset
    self.visualPart.Position = currentPosition

    -- We need to continue from this "fast-forwarded" position
    local lastUpdateTime = timeOffset -- Start the "timer" for this projectile from the offset time

    local connection
    connection = RunService.Heartbeat:Connect(function(deltaTime)
        -- Update the internal timer for the projectile's lifetime/distance
        lastUpdateTime += deltaTime

        -- Calculate the new position based on the current time since it "started" (including offset)
        -- This keeps the motion consistent with the server's perceived time.
        local newPosition = startPos + initialVelocity * lastUpdateTime

        self.visualPart.Position = newPosition

        -- Simple distance check for demonstration (you'd use your actual projectile end condition)
        if (newPosition - startPos).Magnitude > (destination - startPos).Magnitude + 5 then -- A little buffer
            self.visualPart:Destroy()
            connection:Disconnect()
            connection = nil
            return
        end
    end)
end

-- Assuming CastOnServer exists and handles server-side logic
function ProjectileModule:CastOnServer(spawnCFrame: Vector3, destination: Vector3, force: number)
    -- Your server-side logic for collision, damage, etc.
    -- This part *does not* handle visual movement, only the hit detection.
    -- Example (using a simple raycast for demonstration):
    local direction = (destination - spawnCFrame).Unit
    local raycastParams = RaycastParams.new()
    raycastParams.FilterType = Enum.RaycastFilterType.Exclude
    raycastParams.FilterDescendantsInstances = self.initialProperties[2] -- Use the blacklist from initial properties

    local raycastResult = workspace:Raycast(spawnCframe, direction * 500, raycastParams) -- Max distance 500 studs

    if raycastResult then
        local hitPart = raycastResult.Instance
        local hitHumanoid = hitPart.Parent:FindFirstChildOfClass("Humanoid")
        if hitHumanoid then
            -- Handle damage on the server
            hitHumanoid:TakeDamage(20)
            print("Server detected hit on: " .. hitHumanoid.Parent.Name)
        end
    end
    -- The server projectile instance might be destroyed here or after a short delay
    -- You might not even have a visible `self.visualPart` on the server in a purely server-authoritative hitbox system.
    -- If your server-side projectile moves, its movement loop would be independent of the visual client ones.
end

return ProjectileModule

How Timestamping Works and Its Benefits:

  1. Shooter’s Responsiveness: The shooter’s client still draws the projectile instantly (castLocalProjectile). This is crucial for a good player experience.
  2. Server Authority: The server still performs the authoritative hit detection (CastOnServer). It doesn’t rely on the client’s visual position.
  3. Other Clients’ Synchronization:
  • When another client receives the ReplicateProjectile event, they get the serverFiredTime (which is actually the clientFiredTime passed through the server).
  • They calculate timePassed = currentTime - serverFiredTime. This timePassed represents the network latency and server processing time for that specific event.
  • By passing timePassed to CastOnClient, the CastOnClient function can immediately calculate what the projectile’s position should be at that moment, given how much time has elapsed since it was “fired” on the client that initiated it.
  • The projectile then starts its visual journey from this fast-forwarded position, making it appear more in sync with where it should be, rather than starting from startPos and lagging behind.

Important Considerations and Limitations:

  • Lag Spikes: If a client experiences a sudden lag spike, timePassed might be very large, causing the projectile to “jump” a significant distance. You might want to add sanity checks on timePassed to prevent extremely large jumps (e.g., if timePassed is > 1 second, cap it or ignore the replication).
  • Physics/Parabolic Motion: The fast-forwarding logic in CastOnClient needs to match the projectile’s actual movement logic (e.g., if it’s affected by gravity, the fast-forward calculation needs to account for the parabolic arc). The example currentPosition = startPos + initialVelocity * timeOffset is for linear motion.
  • Visual vs. Server Position Discrepancy: Even with timestamping, there will always be some discrepancy due to constant network fluctuations and the fact that player movement itself is subject to replication delay. The goal is to make it look good and feel responsive, while the server still dictates the authoritative game state.
  • workspace:GetServerTimeNow(): This is the best function for cross-client and server time synchronization. It provides the server’s tick() value as seen by the client, taking into account the client’s ping.

This timestamping strategy significantly improves the visual smoothness for non-shooter clients without sacrificing server authority.

How do you think big paintball made their projectiles? They seem flawless

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.