[Live] Change to Part.CFrame Setter Behavior

NOW LIVE

This change was enabled on September 10th at 3:05 PM PST. It should take effect within 10 minutes on servers and upon restart for clients and studio.

Original Post

We are planning to make some behavior changes to the BasePart.CFrame setter in the near future. If your game uses the CFrame property you should read this.

The value returned by the CFrame getter is unchanged. Only the behavior of assigning to this property is changing.

We’re planning to enable this change globally on September 10th.

What’s Changing?

Currently, if you assign a new value to Part.CFrame and the part is connected to other parts with Constraints or Welds, you may get some surprising results. The current behavior is to find the connected part called the “mechanism root” and assign the given CFrame value to that part. That part may be a different part from the one you are assigning too.

We’re changing this to instead move the part’s Assembly, such that the part you assigned to ends up at the given CFrame, as intended. An “Assembly” is what we call the grouping of parts connected to a part by rigid or kinematic joints like Welds, ManualWelds, WeldConstraints, or Motor6Ds. Basically you should get what you expect and the rest of the part’s rigid body comes along for the ride.

This should be a much more intuitive behavior.

The existing behavior is definitely unituitive. It’s impossible to determine which part is a mechanism root in Lua. It is deterministic internally, but externally it’s effectively random. We use it internally for some physics networking logic, but it has no relevance otherwise. You shouldn’t have to worry about this.

A common workaround for this is to Anchor the part before changing it’s CFrame. This works around the issue by making the part it’s own mechanism root, but has some performance ramifications associated with destroying and recomputing assemblies when changing the anchored state. If you are using this workaround, this behavior change will not affect you. This hack should not be needed in the future once this change is enabled, however. Once this is fixed, we would not suggest doing this.

How Does This Affect My Games?

If your game uses workarounds for odd CFrame setter behavior, this change may effect your game.

Unaffected

Model:SetPrimaryPartCFrame is unaffected. This is generally the best way to move collections of parts.

If you were anchoring parts before moving them (and un-anchoring them afterwards) this change will not affect your game. Stick with it for now, but we recommend removing this hack after the change takes effect.

Affected

If you were applying a fudge factor to compensate for some consistently incorrect behavior when changing CFrames, those fudge factors will become redundant and apply an additional offset.

If you were somehow relying on the setter moving another part instead, this will affect you, e.g. if you set wheel.CFrame on the wheel of a car, this would currently most likely move the larger body of the car that is connected to the wheel with Constraints to that CFrame instead. The constraints would snap the wheels back onto the car on the next physics step, pulling the car body back slightly. After this change only the single wheel will move, mostly likely snapping back to the car body on the next frame, so the car would not be moved much at all. If you’re doing something like this you will need to fix your scripts to set the CFrame on the correct body or use Model:SetPrimaryPartCFrame.

You can use this Lua function to replicate the new behavior now for testing:

function setCFrame(part, cframe)
    local assemblyRoot = part:GetRootPart()
    local wasAnchored = assemblyRoot.Anchored
    assemblyRoot.Anchored = true -- temp hack: makes sure part is a "mechanism root"
    assemblyRoot.CFrame = cframe * part.CFrame:inverse() * assemblyRoot.CFrame
    assemblyRoot.Anchored = wasAnchored
end

If you are concerned this change might negatively affect your game, let us know. We can enable this change early for particular places if you want to test in advance.

63 Likes

This is definitely welcome. For the elevators in my game, I spent half a day debugging why they were rotating 90 degrees whenever I changed their CFrame, even with part.CFrame = part.CFrame. In the end I had to name the primary part HumanoidRootPart to force it to be the root.

13 Likes

A most welcome change. I encountered this problem a couple months ago when I was trying to build a first person tool holding system. Thanks!

Yes, this is a great change

3 Likes

Thanks for making this usability improvement! This has resulted in many gray hairs as I figured out that setting Anchored was the only way to teleport a car around.

Quick question: do these changes also apply to a humanoid? E.g., if I warp the leg, will the client think that it’s pretty sure I meant to warp the HumanoidRootPart?
I know humanoids have always had special treatment…

“Rootness” for assemblies and mechanisms is mostly a weird function of size. HumanoidRootPart has a 10x weight multiplier and is more likely to be the root part of an assembly, so it’s most likely to be root. So currently yes, it would usually warp HumanoidRootPart to that CFrame instead of the leg.

After this change the leg will be moved to the given position, but HumanoidRootPart and the rest of the humanoid will be moved along with it, keeping the rest of the humanoid parts’ position relative to the leg.

2 Likes

Will there be any noticable performance differences for anchored parts? And I mean this at quite an extreme level, as I’m currently trying to manipulate hundreds of NPCs via CFrame

3 Likes

If you care about performance now the best thing to do is use GetRootPart() and CFrame that part. A Humanoid with no constraints attached to it is going to be it’s own mechanism so it should work fine in that case.

Anchoring and unanchoring triggers a lot of internal “spanning tree” rebuilding in the system that turns welded parts and constraints into combined rigid bodies and mechanisms. Anchoring and unanchoring a HumanoidRootPart is something like 20-50+ memory allocations internally. It’s actually pretty fast and not going to be an issue at all even for hundreds of NPCs being CFramed at the same time though.

Luckily after this change you’ll never need to worry about that. You’ll just CFrame and things will move as expected and performance will be good.

4 Likes

FYI this is in the queue to be enabled today. The change will likely take effect sometime in the early afternoon PST.

This change was enabled on September 10th at 3:05 PM PST. OP has been updated.

4 Likes

I’ve had some reports of vehicles flinging players about with this change in my game after this changed was enabled.

My game manually handles players entering and exiting vehicles via a hotkey, which when sat on, then tells the server to delete the player’s SeatWeld and immediately set the “exit” CFrame of the player after, essentially like this:

seatWeld:Destroy()
humanoidRootPart.CFrame = exitCFrame

After this change was enabled, it appears like humanoids are still treated like they’re a part of the vehicle’s assembly for about a second after Weld destroy, as the vehicle sort of moves towards the player. This causes vehicles to sometimes get stuck under the map or fling players away.

Adding a wait() in-between destroying the weld and setting the CFrame seems to have primarily solved this but I’m still getting some reports of it happening.

I made a simple uncopylocked place which implements a rudimentary repro of what happens on my game:

Sitting in either “vehicle” will trigger the server to destroy your SeatWeld after 3 seconds:

  • The blue “vehicle” has the wait() and moves the player away as intended without the “vehicle” moving
  • The red “vehicle” doesn’t have a wait() and appears to move with the player as the player’s CFrame is set

Disabling the FVariable DFFlagInsaneInTheCFrame causes both “vehicles” to work as intended as they did before the change.

It’s a bit odd and I’m lost as to how I can solve this properly in my game.

1 Like

Looks like the removal of the Weld and CFrame assignment are being re-ordered in replication. For the red car, flag is enabled or not, the client receives the CFrame assignment before the weld removal.

On the server:

SeatWeld removed
Workspace.Player1.HumanoidRootPart cframe

No surprise, it’s in the same order as the script…

Now on the client after InsaneInTheCFrame

Workspace.RedModel.BigPart cframe -- HumanoidRootPart assignment applied to current assembly root
SeatWeld removed

So it moves the car instead of the Humanoid because it sets the CFrame before it splits them appart… The swapped order means we get a different result…

Now on the client before InsaneInTheCFrame

Workspace.Player1.HumanoidRootPart cframe -- NOT ASSEMBLY ROOT, DROPPED
SeatWeld removed

Wait… This is wrong too! You CFrame assignment is being totally ignored!

Before my change that out of order CFrame assignment is simply dropped. Internally it’s setting the CFrame of a part that isn’t an assembly root, which does nothing.

With DFFlagInsaneInTheCFrame false I’m seeing the red car not quite “working as intended”. The player is not moved away from the car, the simply stand up in place on the seat and the CFrame assignment is dropped. THIS IS ALSO REALLY BAD! This actually uncovers a deeper issues with replication re-ordering with welds…

Thinking about what to do now, but I’ll probably disable the flag. I can make a change so this works as it did before, but that’s still broken.

Thanks.

8 Likes

Hey @KarlXYZ, we ended up not disabling this change. We did just enable a change that should make the red car in your example behave like it did before.

This is still fundamentally broken, but now it will be broken in the same way that it has been for a long time. It’ll take us a lot more work to fix that case properly. For now you can do something like CFrame the player on the client that owns the character as soon as the weld breaks from a LocalScript. We just can’t ensure the order of CFrame assignment replication at the same time as adding/removing welds at the moment, sadly.

2 Likes

Ah, thanks for that. Yeah on the live game I did apply the CFrame change on both client and server and this fix has returned the old “stable” behavior.

3 Likes

Also thinking about this more… If possible it’s probably best to CFrame the player from LocalScript on the client that owns the character when it detects the weld breaks. Break weld on server, wait to receive on client, then CFrame the player locally when it detects being unseated. That should work consistently without waits. No delay to the player. In general it’s probably best to only CFrame parts on the system that is the network owner where possible.

We do want to fix this ordering issue properly long term. Will take a while to come up with a solid solution and implement it though.

I’ve found that removing the CFrame setter from the server causes my vehicles to jitter a bit in the direction of the player, though, I believe this is down to collisions the server is simulating in the delay between removing the weld and the client setting its CFrame.

I have collision groups for vehicles and players, and setting the CFrame exclusively on the client works flawlessly if they’re non-colliidable. But I can’t seem to find the best time to update these collision groups after a player exits the vehicle so I just set the CFrame on the server as well which avoids this issue.

1 Like