Allow game developers to configure the physics replication rate for their games

As a Roblox developer, it’s unnecessarily difficult to deal with server-sided hit detection without making gameplay miserable for a majority of the player base. I’ll try to provide as much information as possible to explain why this WOULD be a really good feature to have, since I’ve been really looking forwards towards something like this for a while.

All of my tests will be done with the WalkSpeed of 16, although it is worth to mention that the issue gets worse the faster you go for obvious reasons. I will be utilizing a specific fast flag - DFIntS2PhysicsSenderRate - allowing me to control the physics replication frequency. It defaults to just 15 replications per second, which is really low. IncomingReplicationLag is consistently set to 0 for all the provided clips.

First, I’ll clarify what I mean. For this demonstration, I coded a very simple sword tool:

Click here to view code
--!strict
local Players = game:GetService("Players")

local tool = script.Parent

local player = script:FindFirstAncestorOfClass("Player")
local character: Model

if player then
	character = player.Character
else
	character = assert(script:FindFirstAncestorOfClass("Model"))
	player = Players:GetPlayerFromCharacter(character)
end

local rootPart = character:FindFirstChild("HumanoidRootPart"):: Part
local humanoid = character:FindFirstChildOfClass("Humanoid"):: Humanoid
local animator = humanoid:FindFirstChildOfClass("Animator"):: Animator

local size = Vector3.new(4, 5, 6)
local overlapParams = OverlapParams.new() do
	overlapParams.FilterDescendantsInstances = {character}
end

local swingAnim = animator:LoadAnimation(script.Slash)

local nextSwing = 0
local function onActivated()
	local t = os.clock()
	if t < nextSwing then
		return
	end
	
	nextSwing = t + 1
	
	tool.Handle.SwordSlash:Play()
	swingAnim:Play()
	
	task.wait(0.2)
	
	for _ = 1, 15 do
		local cframe = rootPart.CFrame * CFrame.new(0, 0, -(size.Z - 1) / 2)
		local hitList = workspace:GetPartBoundsInBox(cframe, size, overlapParams)
		
		local hadHit = false
		for _, part in hitList do
			-- extremely lazy check because this is a demo
			if part.Name == "HumanoidRootPart" then
				hadHit = true
				break
			end
		end
		
		local hitbox = Instance.new("Part")
		hitbox.Material = Enum.Material.ForceField
		hitbox.Transparency = hadHit and 0 or 0.5
		hitbox.Color = hadHit and Color3.new(0, 1, 0) or Color3.new(1, 0, 0)
		hitbox.CFrame = rootPart.CFrame * CFrame.new(0, 0, -2.5)
		hitbox.CastShadow = false
		hitbox.CanCollide = false
		hitbox.CanTouch = false
		hitbox.CanQuery = false
		hitbox.Anchored = true
		hitbox.Size = Vector3.new(4, 5, 6)
		hitbox.Parent = workspace
		
		task.delay(1, hitbox.Destroy, hitbox)
		if hadHit then
			break
		end
		
		task.wait(1/60)
	end
end

tool.Activated:Connect(onActivated)

ATTACKER PERSPECTIVE

To begin, we’ll check out how replication feels for the person holding the sword.

  1. Regular replication frequency
    It.. kind of works, until you realize how far behind the hitbox is, which results in hits that you believe should’ve registered, missing by a large margin.


    That doesn’t look fun, now does it? The hitbox is barely able to keep up with my character in studio, where you have virtually no latency.
    As you can probably imagine, it only gets worse past this point, eventually leaving the hitbox literally BEHIND you.
    (For reference, I average 80-100ms (60ms on a good day) in game servers)

  2. Same scenario as above, but the replication frequency is raised to 60


    That already looks WAY better. While not perfect, which is to be expected since we lack slight velocity-based compensation, it’s a major improvement from what we’ve seen previously.
    Quick comparison (before / after)


TARGET PERSPECTIVE

For this one, I coded a basic server position visualizer, which is the green part that follows my character around.

Click here to view code
--!strict
local RunService = game:GetService("RunService")

local target = workspace:WaitForChild("0h7w7")
local targetRootPart = target:WaitForChild("HumanoidRootPart"):: Part

local p = Instance.new("Part")
p.Size = Vector3.new(2, 2, 1)
p.CanCollide = false
p.CanTouch = false
p.CanQuery = false
p.CastShadow = false
p.Anchored = false
p.Material = Enum.Material.Neon
p.Transparency = 0.5
p.Color = Color3.new(0, 1, 0)
p.CFrame = targetRootPart.CFrame
p.Parent = workspace

RunService.PostSimulation:Connect(function()
	p.CFrame = targetRootPart.CFrame
end)
  1. Regular replication frequency
    I will be running away from a really basic AI that swings the sword whenever you’re in relatively close proximity.



    According to what we see, this shouldn’t have hit, yet it does because the server position lags behind. To reiterate once more, this is observed at minimal latency and WILL be way worse in-game.

  2. Same scenario as above, but the replication frequency is raised to 60


    Quick comparison (before / after)

Yet another issue from the same category:

  1. Regular replication frequency


    You can clearly see how it takes quite a bit of time for the server to receive the new position, which is absolutely terrible for games with fast projectiles.

  2. Same scenario as above, but the replication frequency is raised to 60


    Once again, noticeable improvements (which makes a huge difference when it comes to dodging)


CONCLUSION

If Roblox were to provide a simple way to change the replication frequency, it would be way more feasible to rely on server-authoritative hit detection.

In my specific case, I wouldn’t be forced to write a custom replicator just to make the gameplay feel more fluid (for additional context, there were lots and lots of complaints about people getting hit from unreasonable distances until we implemented our own replicator, which mitigated that for the most part).

While I may be capable of managing all that on my own and, obviously, specialized solutions are usually better because they’re designed with the game in mind, it’s not really beginner friendly nor convenient for quick projects - it would be way better to expose this as an engine setting.

22 Likes

Doesn’t increasing the replication rate just open a pandoras box of problems though? Higher bandwidth load, higher ping, higher CPU load, and more noticeable desync are the first things that come to mind. For example, if you increase the replication rate from 15 times a second to 60 then that’s 4X the amount of data being getting sent back and forth.

To be entirely honest this should not be a beginner friendly or convenient feature. It would cause problems for the entire platform if it was.

2 Likes

Concerns about bandwidth are valid, however the clips demonstrate approximately what this would look like, and you can clearly see the sent KB/s. It’s not as bad as it may seem, the difference isn’t big enough for it to genuinely cause issues. This is also directly related to potentially raised ping, which means that shouldn’t happen either.

As for higher CPU load, I will assume that the only thing that changes is the amount of times the CFrame is replicated (not as expensive as the process of simulating physics on its own), so this isn’t really as big of an issue (for example, you can fire a remote event every frame with some data and the engine doesn’t explode).

Not sure how this would cause a desync, since all that’s happening is basically just interpolation to the expected CFrame with additional complexity, but I can’t really say much on this topic because only the engineers know how exactly it all works.

2 Likes

Support. I’ve had to get around the 15/s replication rate using custom position replication in the past, and it improved things a lot. Everyone I’ve talked to whom works on competitive games similarly uses custom position replication, specifically to get around the latency incurred by this low send rate.

Enabling a higher send rate specifically for competitive games would be awesome, and lower the amount of engineering necessary for some cases; similarly, I can see uses for a lower send rate for eg. roleplay games

5 Likes

I think server authority might solve this issue, because it introduces prediction, which should smoothen things out, but at the current stage, I believe it has too many issues to answer whether or not it will.

I do agree that there should be a way to control replication rate, ideally per assembly, but considering how Roblox wants everything automated, I have doubts that it will ever be implemented.

1 Like

Server authority IS a major improvement actually, I’ve played around with it and it’s clear that the delays are decreased by a lot, but it requires heavily specialized code. You’d have to effectively re-script the entire movement system if the client handles all of it.

In the way I see it, this shouldn’t be too complex to implement since it was already possible to do in-game prior to the fast flag whitelist and worked perfectly fine (I discovered this issue’s cause from those weird “fflag lists” in the first place).

Well, from what I am seen, it does predict physics, so it doesn’t require any special setup for that, the only part needing rework is the mindset behind the combat code, because you need to write deterministic code that runs both on client and server, as well as the fact that you can only change state on server as the client is no longer source of truth.

No, the clips do not demonstrate anything. All your testing is occurring in Studio which is not representative of what happens on a live server. Both your client and server are running in the same process so there is no TCP/UDP connection, so the metrics are inaccurate.

What actually happens on a live server is that every physics update or property change must be serialized, compressed, and wrapped with headers. That adds X number of bytes of overhead to each message sent over the network. Studio skips all that and just passes data through a reference in memory, so you can’t even see this performance metric. So, assuming the default replication rate is 15Hz and you increase it to 60Hz. Then data sent goes from 15Hz * Overhead → 60Hz * Overhead. 4X increase!

Spamming remotes doesn’t blow anything up because Roblox will automatically throttle your request. Go above their bandwidth limit and the packets just get queued or dropped entirely. CPU’s aren’t magic either, if they get 4X as many requests they have to do 4X as much work.

And finally, as for desync, let’s say you have 15Hz replication rate again and hit the bandwidth limit, while the schedular is catching up 2 updates are dropped. Now let’s say you have 60Hz replication rate and hit the bandwidth limit, 2 * 4 = 8 updates are dropped. The client just skipped over 8 updates instead of 2, desync objectively just became 4X worse as a result!

I don’t know where you got this information from, but the client in a Studio test should be running on a local server, which the client communicates with using the same type of connection the Roblox client would use to connect to a real Roblox server. Any data sent between the client and server is serialized, contains headers and gets compressed in the same exact way as when communicating with a real Roblox server. Internally the network request loops back instead of going through hardware, which is why ping is a lot lower than outside of Studio.

There is no reason to think the stats shown in Studio test would be significantly different from the stats in a real game, unless you can prove otherwise.

2 Likes