Projectile Compensation? (fastcast)

So i’m new to client/serverside replication so bare with me, I could have the complete wrong idea so please correct me if i’m doing this all wrong.

So currently I’m using a module called fastcast, I create a projectile on the client, fire an event immediately after for the server to also create it’s own projectile to decide if it was a hit and deduct the amount of damage needed, The issue i’m having is they are wildly out of sync, If you shoot the projectile while moving in any direction the client will be way ahead of the server in all directions, how do I go about syncing them up?

4 Likes

You can get the character’s speed and direction using AssemblyLinearVelocity, then add this to the position of the projectile’s origin.

local firePoint = gun.FirePoint.Position  --source of the projectile
local characterVelocity = character.HumanoidRootPart.AssemblyLinearVelocity / 3   --divide by 3 otherwise it would be too big (trial and error to get the right number)

fastcast:Fire(firePoint + characterVelocity, direction, bulletSpeed, rayBehavior)
1 Like

Wouldn’t it be different every single time for every different ping though?

1 Like

When you send over the attack to the server, you should send data so the server can exactly replicate it.

For example, in your case, you would ideally send the initial fire position, the fire direction, and the fire time, then on the server make a projectile at the fire position going in the fire direction but have it skip ahead so that it lines up with the fire time.

So if the server gets a remote event saying the shot was fired 200 ms ago, it would skip ahead 200 ms of the shot’s arc so they’re synced up.

(Also make sure to sanity check, so if the time difference is too much (aka lag or exploiting) it just ignores the shot and tells the client who shot to ignore it too.)

2 Likes

This works perfectly though i’m trying to figure out how i’d move it 200 ms ahead if that was the case,

So my current understanding of it would be to do this:

local latency = currentTime - timeFired

giving me the time difference, but unsure how i’d go about executing it to skip what would be 200 ms ahead.

2 Likes

Basically you do some physics equations.

You just add the acceleration (aka gravity) times the time to the fire velocity to get the velocity after the skip.

Then for the position, you do the equation on the lower left, which is the fire velocity times the time plus half the acceleration times the time difference squared, plus the initial position.

Here’s that in code:

-- Get these from your remote event
local fireTime = ...
local fireVelocity = ...
local firePosition = ...

-- Use a precise time source (milliseconds)
local currentTime = ...
 -- The time to skip ahead
local deltaTime = currentTime - fireTime

-- The acceleration from gravity (FastCast might have you use a custom gravity value for a faster or slower drop, if it does use that instead) 
local gravityAcceleration = Workspace.Gravity * Vector3.new(0, -1, 0)

-- The predicted "future" velocity
local newVelocity = fireVelocity + gravityAcceleration * deltaTime
-- The predicted "future" position
local newPosition = firePosition + fireVelocity * deltaTime + 0.5 * gravityAcceleration * (deltaTime ^ 2)

-- Now you have the new start position and new start velocity

-- TODO: Raycast in an arc from firePosition to newPosition to make sure nothing is in the way of the time skip
-- (i.e. the shot didn't hit anything while the remote event was sending)
-- (rare case, could result in players firing through walls though if you don't check)

-- TODO: Start fastcast ray from newPosition with newVelocity and make visuals
5 Likes

First time I’ve ever seen someone put equations with their lua answer, really useful though - I had a similar issue.

3 Likes

This is perfect, though hopefully my final question that i’m a bit confused by, (probably me just having an oversight)
firePosition + fireVelcoity?
How would that work considering wouldn’t it be a vector3 for a position?

2 Likes

Generally speaking, hit detections should be done on the client, sanity checks on the server. It may leave your game a little weaker to exploiters, but it provides a much smoother experience in the long run.

2 Likes

You want fireVelocity to be a vector too. In physics, velocity is a speed with a direction, so it’s a vector, while speed is just the magnitude (e.g. a speed would be 50 mph while a velocity would be 50 mph north).

So basically the velocity should be the speed and direction in Vector3 form.

If they’re doing knife throwing they don’t really need to do stuff on the client because the “bullet” is moving relatively slowly (at least compared to gun bullets). If they’re doing guns this is generally a good idea (though more advanced systems will transition it to the server for longer shots using the method above).

Nice! It looks like fastcast already has a built in function to skip ahead.

I believe you can use ActiveCast:Update(deltaTime) to skip ahead. This means you don’t need to do the check raycast either, since it will be included in your raycast.

I haven’t used FastCast yet, but basically:

-- Get from your remote event
local fireTime = ...

local activeCast -- the cast you create from the remote event data
-- Create your hit connection I think

-- The time to skip ahead
local deltaTime = currentTime - fireTime

-- Skips ahead deltaTime and does an instant raycast over the skipped area of the arc
activeCast:Update(deltaTime)

-- No need to check over the skipped time's arc, since the update function runs the raycasts for you

https://etithespir.it/FastCastAPIDocs/fastcast-objects/activecast/#updatenumber

There’s quite a few benefits to doing this on the client-side:

  1. The knife won’t jolt ahead when spawned, making it visually smoother.
  2. Similarly, the knife can be thrown at point blank, and won’t be thrown through walls either.
  3. Most important of all, there’s no delay when the mouse direction is gotten vs when the knife is thrown.

The animation on the server will be out of sync with the real raycast if it doesn’t jolt ahead, regardless of method. The animation on the client who does the action is exactly matched, so on the client who does the throw it will be perfectly in sync with no jolt. For other clients, both methods require jumping the knife, at least if the visual is meant to be accurate. Most big games interpolate a little with the visuals, so the initial throw lerps to the real raycast so there is no jumping.

The server checks the first bit instantly, so it can’t go through walls. There is definitely a good case though for using a mix of both–the client checks the first little bit, and as it gets further it switches to being sever authoritative. That’s what large games that don’t use hitscan (aka infinitely fast bullets) do.

There is no delay, because it fires on the client and plays the visual, then the exact path the client’s knife takes is replicated to the server. Essentially, the server and the client work with the same knife path, so there isn’t delay since the client can just work with the assumption the server is doing the same thing (but not for hits, because the server has different more up-to-date data for hits).

The delay for the client would be registering when the knife hits something, though since the client has an exact copy of the knife path, the client can use this for prediction.

TL;DR: Triple A games use hit scan for instant bullets in the way you’re talking about, though for moving bullets they do something like what I’m explaining but a little more advanced (they switch between from the client to the server for anything that’s gone for more than the latency time, which for roblox is usually 70-160 ms).

I’m assuming we’re talking about the visual of the knife itself, not an animation played by the character. In that case, the client’s knife is only ahead by a little, and technically, not at all. As the client would own the knife and replicate its position to the server, there time between the next update and when a hit is called would roughly be the same. There would be no way to tell the difference unless you were playing two computers at the same time, and even then it would be trivial.

There… would be. There is a delay between when a RemoteEvent is fired and when it is recieved. So when the server gets the instructions for the knife throw, it’s already ‘outdated’ information. Even if it’s something as small as 80ms, it will be noticed and feel clunky.

Your solution addresses the immediate issue, that being the knife desyncing between the client/server, but not the larger problem of the lag between client and server feeling unresponsive. Giving the client the workload not only prevents a desync, but also uses less resources.

The server’s knife is behind by the ping time, which always exists, so it’s always behind.

If we’re talking about a knife model with client network ownership (ignoring the exploits), the replication would need to look something like this:
Client throws knife, create visual
Send remote to server (one-time 1 latency unit delay)
Server creates knife model, gives ownership to client
Client replaces their knife with the new one from the server they have ownership over
Client continuously replicates their knife’s position to the server (the server’s copy is 1 latency unit behind
The server replicates the position being replicated by the client to the other clients, each client is 2 latency units behind.
Knife hits something on the client, client has 0 LU of delay
Replicates to server (1 LU delay)
Server replicates to other players, 2 LU delay)

Compared to:
Client throws knife, create visual
Send remote to server (one-time 1 latency unit delay)
Server reconstructs the exact path of the knife (OG client has 0 LU of delay, server has 0 LU of delay)
Server sends reconstruction to clients (one-time 2 LU delay for replication, which is absolute best case) (clients have 0 LU of delay after)
Knife hits something on server, replicates to all clients, every client has 1 LU delay
In 98% of cases, the zero delay copy can be used to approximate when the knife hits, so all clients have virtual 0 LU delay

I think the problem we might have here is the assumption that latency is “roughly” zero. If you assume latency is zero and there is “technically” no delay, then any replication solution works regardless.

Would be for who? The client has zero position delay, and can approximate hits in 0 LU and get confirmation in 1 LU. The server has 0 LU for position and 0 LU for hits. The other clients also have 0 LU for the position, 0 LU for hit approximation and 1 LU for hits.

The only delay is on the initial update. The client has no update since they start with the info. The server has 1 LU update. The data then goes from the server to the other clients, so a 2 LU update. This is optimal–it’s logically impossible to go lower.

With the method you describe, the delay isn’t only initial–there is a constant delay because the position is constantly 1-2 LU off, instead of only initially having 1-2 LU off.

Proper sanity checking requires the server to do all the work again anyways. In fact, because the server has an exact copy of the path and doesn’t need to do the sanity checking after the fact, the work load is equal or less, assuming proper verification.

Edit:

I’ll DM you explaining why this isn’t the case. It has to do with how the bullet’s uninterrupted path is deterministic.

1 Like

Oh my what did I come back to lol.
Anyway if I must ill just experiment with both client server and sole client hit detection stuff, ill mark as solution tomorrow probably

Appreciate all the help here everyone

For me, what could work best is sending the client CFrame and AssemblyLinearVelocity along with its current time to calculate delta time and then calculate the possible position the client is, if you were to do this of course you would need to add proper sanity checks, when messing with position + velocity both on client and server, you need to be cautious since if the player is with a high ping it starts to extrapolate too much, on my Studio test, with 40ms it was fine, tested with 200+ms, wasn’t that bad but when falling down it would think my root part would be almost below the ground, here’s a screenshot of my test:
The parts in the air, are from a moment i jumped
Parts aligned from the moment i walked forward


Colors explanation:
Red: Server CFrame
Purple: Server CFrame + Velocity (delta used is the time it elapsed for the server to receive the remote)
Blue: Client CFrame received by the remote
White: Client CFrame + Velocity (same delta used)
Green: Current Client CFrame at the time of the server replicating the parts

Others might have some better calculations for your specific use case but personally thats how I would deal with ping on those stuff (i don’t use fastcast)

Am I correct in saying it should be as follows, since I believe the caster returns its self.

Something like this?

local activecast = caster:Fire(orig, dir, veloc, behaviour)
activecast:Update(dt)

but that doesn’t seem to work for me

1 Like

It turns out the docs say that function’s only for internal use. The other method above should work though.