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

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!

Okay! For future readers - I’ve got a local script that puts it all together and is working. It tracks players, sets player parts to client-specific collision groups, and sets them again if those parts stream out and back in (which causes them to otherwise go back in the Default collision group) or whenever a player dies and gets a new character.

The prerequisite is that you create collision groups in studio first, called ClientCharacter and OtherCharacters, and set it so neither can collide with itself or the other.

The whole point of this is so each client can have its own client-only collision groups, for the local player and All Other Players, which can be used to prevent non-local players’ simulated physics affecting certain objects on the client. (e.g. client-generated primsatically constrained platforms, in my case.) by stopping other players colliding with them.

You also have to make sure ClientCharacter can collide with whatever the parts in question are in your game, and that OtherCharacters cannot.

Whew…

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

local trackingConnections = {}

local function onInstanceRemoved(object)
	if trackingConnections[object] then
		trackingConnections[object]:Disconnect()
		trackingConnections[object] = nil
	end
end



-- Note player is only ever added once. Character will be replaced on player whenever player dies. 
local function HandleCharacter(Player, Character)
	print("HANDLING RUNNING FOR ",Player)
	local Group = if Player == Client then "ClientCharacter" else "OtherCharacters"
	for _, Object in Character:GetChildren() do
		if Object:IsA("BasePart") then
			Object.CollisionGroup = Group
			print(Object, "initially tagged as ",Group)
		end
	end
end


local function trackCharacterChildren(Player, Character)
	local Group = if Player == Client then "ClientCharacter" else "OtherCharacters"
	
	trackingConnections[Player] = Character.ChildAdded:Connect(function(Child)
		if Child:IsA("BasePart") then
			Child.CollisionGroup = Group
			print(Child, "added to ",Player," and tagged as ",Group)
		end
	end)
end


local function trackPlayer(Player, Character)
	HandleCharacter(Player, Character) -- tag its parts with appropriate Collision Group
	trackCharacterChildren(Player, Character) -- track future children
end

local function HandleJoined(Player)
	local Character = Player.Character or Player.CharacterAdded:Wait() -- wait to get character
	trackPlayer(Player, Character)
	
	Player.CharacterAdded:Connect(function(Character)
		trackPlayer(Player, Character)
	end)
	
end


local function onCharacterRemoving(character) -- Despawn fires when a player dies (at end of their death wait) AND
	--	if they leave the game. It doesn't fire if the player is streamed out for being out of range of local player.
	
	print(character.Name .. " is despawning")
	local characterNowGone = Players:GetPlayerFromCharacter(character)
	onInstanceRemoved(characterNowGone)
end



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

-- This next event happens once when a player joins. it doesn't fire again if they respawn or stream out and in.
Players.PlayerAdded:Connect(HandleJoined)

Rinpix and IDoLua’s code and suggestions got me here.

If anyone can see easy improvements to be made to this code, feel free to chime in. On the plus side, it’s already working.

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