Physics replication affecting every client's locally scripted parts - trying to find workaround

Hi all. I’ve been reading this forum for a couple of years but this is my first question.

The situation:

In my obby game, I’ve got some prismatically constrained platforms that go down when a player gets on them. Each player has their own copy of each platform. i.e. It’s cloned by their client when the game starts. The scripts controlling the platforms are localscripts.

But when one player lands on their platform, the other players’ platforms go down, too. This is regardless of me checking in the local script that only the local player can trigger their own platform.

Digging through the forum, I suspect this is probably due to client-to-client physics replication?

e.g. I read this topic:

where someone’s solution was, I think, to use collision groups, so that no player could collide with another player’s platforms.

However I haven’t been able to wrap my head around how to do this. Won’t every client need a unique collision group that includes only their platforms? It’s sort of like a team doors coding problem, except - each player is a team, and only exists when each player joins. Or is it much simpler than that?

I’m looking for help in how to approach the problem, and how to code it server and client-wise. Thanks much.

PS PS I also don’t want players to collide with each other, and had already coded that in a typical way in a serverscript:

local PhysicsService = game:GetService("PhysicsService")
local Players = game:GetService("Players")

PhysicsService:RegisterCollisionGroup("Characters")
PhysicsService:CollisionGroupSetCollidable("Characters", "Characters", false) -- Characters can't collide with other characters

local function onDescendantAdded(descendant)
	-- Set collision group for any part descendant
	if descendant:IsA("BasePart") then
		descendant.CollisionGroup = "Characters"
	end
end

local function onCharacterAdded(character)
	-- Process existing and new descendants for physics setup
	for _, descendant in character:GetDescendants() do
		onDescendantAdded(descendant)
	end
	character.DescendantAdded:Connect(onDescendantAdded)
end

Players.PlayerAdded:Connect(function(player)
	-- Detect when the player's character is added
	player.CharacterAdded:Connect(onCharacterAdded)
end)
4 Likes

Other clients are not replicating physics because they don’t know the part exists. What’s happening is most likely that each client is detecting other players on the platform.

1 Like

If it is a local script, then in the script, ignore all other players and just use references to LocalPlayer in a StarterCharacter script:

local player = game:GetService("Players").LocalPlayer
local character = player.Character or player.CharacterAdded:Wait()
1 Like

I am already checking that the player touching the part is the local player. Here is the bit of script that makes the connection

local player = game.Players.LocalPlayer
local Character = player.Character or player.CharacterAdded:Wait()

local function d1bump(hit,object) -- the object is d1, not the whole descender model
	if object.Parent.sunkYet.Value == false then
		if hit and hit.Parent == Character then
			object.Parent.sunkYet.Value = true
			object.Parent:WaitForChild("d1pole"):WaitForChild("PrismaticConstraint").TargetPosition = -30
			print("Touched d1.")
		end
	end
end

Am I checking for the local player correctly? I think I am, because this “if hit and hit.Parent == Character” code seems to be working in other player-specific detection scripts in my game. But it does not seem to be working for these prismatically constrained parts.

1 Like

I can add that I just did a test with 2 players. If player 1 jumps on their platform, the test message ‘touched d1’ fires for them. Player 2 isn’t getting that message (you wouldn’t expect they would.) Yet player 2 can see their own client’s platform activated at this moment. We know it’s not player 1’s platform they’re seeing because that only exists on player 1’s client. So player 1 is making player 2’s platform move, though the part doesn’t exist on their client, and the localscript is checking that only the localplayer can activate it. This still suggests physics to me… or some other mechanism I don’t know about yet (and this wouldn’t be the first time :slight_smile: )

I’m not entirely certain how to go about this, but it’d have to involve CollisionGroups, and even then I’m not entirely sure if you an properly set that on the Client. I don’t mess with them too much!

Anyways I’m here to confirm suspicions for you rather than solve it anyways.

Yes, it IS due to replication… But not Client-to-Client physics replication (this is also nonexistent, it’s Client-to-Server and then Server-to-Client, otherwise there’d be a peer-to-peer connection in the mix which adds network vulnerabilities and might even bypass FilteringEnabled) but rather just the replication of the other players character.

The reason it’s interacting is simply due to your Client replicating their character where it is. Now while it is doing that, it is also simulating the Physics for your platform, which, in turn, results in it simulating how your platform should behave in the case a player stood upon it. It’s quite dumb and I am unsure if it’s a desirable behavior outside of very niche scenarios, but either way, it is what is occurring here.

1 Like

Thanks for that explanation. Hm, I’ll have to think on how to get around this.

Do this on the server:

local physicsService = game:GetService("PhysicsService")
local players = game:GetService("Players")

physicsService:RegisterCollisionGroup("ClientCharacter")
physicsService:RegisterCollisionGroup("OtherCharacters")

physicsService:CollisionGroupSetCollidable("ClientCharacter", "OtherCharacters", false)
physicsService:CollisionGroupSetCollidable("OtherCharacters", "OtherCharacters", false)

And this on the client:

local player = players.LocalPlayer

local function trackCharacter(character, isLocal)
	local collisionGroup = (isLocal) and ("ClientCharacter") or ("OtherCharacters")

	for _, part in pairs(character:GetDescendants()) do
		if (not part:IsA("BasePart")) then continue end
		part.CollisionGroup = collisionGroup
	end

	local connection = character.DescendantAdded:Connect(function(descendant)
		if (not descendant:IsA("BasePart")) then return end
		descendant.CollisionGroup = collisionGroup
	end)

	task.delay(5, function()
		connection:Disconnect()
		connection = nil
	end)
end

workspace.ChildAdded:Connect(function(character)
	local p = players:GetPlayerFromCharacter(character)
	if (p) then
		trackCharacter(character, p == player)
	end
end)

To each client, the collision group ClientCharacter will only contain their own character, and OtherCharacters will contain every other client’s character. This seems to work fine when I tested it in Studio.

1 Like

Edit: Sorry, took me a moment because I got stuck on another topic, but, I did in fact figure this out.
Here:

-- Client code
local Players = game:GetService("Players")
local Client = Players.LocalPlayer

local function CreatePart() -- Delete (for testing)
	local Part = Instance.new("Part")
	Part.CFrame = CFrame.new(-15,0.5,0)
	Part.Size = Vector3.new(4,1,2)
	Part.CollisionGroup = "Local" -- Use this collision group
	Part.Parent = workspace
	return Part
end

local function HandleCharacter(Player, Character)
	local Group = if Player == Client then "Local" else "Other"
	for _, Object in Character:GetChildren() do
		if Object:IsA("BasePart") then
			Object.CollisionGroup = Group
		end
	end
end

local function HandleJoined(Player)
	if Player.Character then
		HandleCharacter(Player, Player.Character)
	end
	Player.CharacterAdded:Connect(function(Character)
		HandleCharacter(Player, Character)
	end)
end

for _, Player in Players:GetChildren() do
	HandleJoined(Player)
end

CreatePart() -- Delete (for testing)

Players.PlayerAdded:Connect(HandleJoined)

and

-- Server code
local PhysicsService = game:GetService("PhysicsService")

PhysicsService:RegisterCollisionGroup("Local")
PhysicsService:RegisterCollisionGroup("Other")

PhysicsService:CollisionGroupSetCollidable("Local", "Other", false)
2 Likes

Thanks you two. I’m just looking through your solutions to make sure I understand them.

@Rinpix , so is it like this: The workspace.ChildAdded block is checking things that arrive in the workspace to see if they’re players, and if they are, it runs the track character function to give them a group. I follow the boolean stuff. Is the connection and disconnect part just to give parts of new players time to appear?

1 Like

That’s pretty much what it’s doing, yeah. You’re very welcome.

peer-to-peer won’t bypass FE, idk but what P2P is basically another type of net-code. Roblox uses Client Server model, not P2P. Closest thing to P2P in a client-server model is Listen Servers, which even that isn’t true “p2p” as there is still a authority which is the host player.

Network Ownership simply routes physics packets from a owner-client or the server, so if the server owns the part, it will just send the position/orientation updates to all clients normally, if a client owns it, then the owner will send the results to the server first before the server sends it back to other clients.

Locally created parts weren’t intended by Roblox, so there still some stuff left like local parts still sending replication data even tho the server has no knowledge of the part’s existance.

I know this, but I just clarified that a client-to-client connection would be peer to peer networking. Otherwise, I am fully aware of how Roblox handles their networking behind the scenes, but thank you for the reply!

Okay, the Rinpix / @IDoLua method is working to start with. But I found it breaks if the players move too far apart. That is to say… I have a cheat teleport key for testing, and in a 2-player test server, I would immediately teleport each player in turn to the descending platform I need to test all this code on, which is right up the other end of the workspace (>1024 studs).

So at that moment, the players would bump into each other, and the platform still wasn’t respecting the collision groups. It took me about 5 mintues to notice the players could walk through each other pre-teleport. When I moved them up the playfield together to the test zone, everything still worked – no player on player collisions, and players could only trigger their own platforms.

Maybe players are being retagged with default collision groups, in each respective client’s eyes, when they go out of range of the local player?

(I’ve got IDoLua’s code in place atm, because I was using Rinpix’s first, and wondered if there was a problem with it. But they both work - it’s just they both are being affected by, uh, whatever the new issue is.)

Sorry I was busy with other matters;
How are you teleporting the character?

I’m c-framing their humanoid part to a spot a tiny bit in the air above the target position. So,

player.Character.HumanoidRootPart.CFrame = CFrame.new(destination.Position) + Vector3.new(0,5,0)

Ah, actually, if their Character does despawn (fall out of replication distance) there’s a fair chance that it pulls the Server’s CollisionGroup state for their baseparts rather than your client’s first.

Okay. So, I guess playeradded doesn’t apply for them reentering that distance, otherwise your code would already have been retagging them. Could this be solved with collection services? I’m thinking - tag players with player added. Then track them the usual way with instanceadded, instanceremoved, and reapply the collision groups whenever they reappear. Though which part would I tag? The Humanoid Part? I’m still not terrific with the player/character/humanoid part distinctions yet.

Have you done any form of testing to confirm this is the issue?

If you don’t have StreamingEnabled on, there’s a fair chance this isn’t the issue, and that the physics engine has an odd edge case with CollisionGroups.

As a note, It’d also be advisable to just add a ChildAdded event to the Character rather than use loops and CollectionService in this instance, since it’s a rather niche issue, it makes wasting performance on a loop less than ideal.

I think this is the issue beacuse, If I use the same cheat-teleport code to move the test players up the field together (in steps) their collision groups are right when they get to the far end of the playfield. But if I immediately send one player to the >1024 studs away target, then bring the other guy by any means, the new collision system is broken.

I have streaming enabled in general, but I have set the 10 baseparts that act as teleport targets set to persistent, just to make the cheat-teleport system easier to code and use.

So I can see a few things to check here (manually look at player’s collision groups after the big teleport, teleport to something that isn’t set to Persistent, etc.) and I’ll report back with my progress. Thanks for the help so far!