ModelMover - A simple solution to CFrame inaccuracies from repeated calls to SetPrimaryPartCFrame

Introduction to ModelMover

If you’ve made extensive use of Model:SetPrimaryPartCFrame you are no stranger to the inherent inaccuracies it can acquire over time. For instance, if I rotate a model by getting it’s PrimaryPart’s CFrame, modifying that, and then setting it to the new value, you can very obviously see the parts drift as you repeat calls.

The solution to this problem is incredibly simple – Cache the original original CFrames (the ones from before the first call to move the model). That’s precisely what my module does.

When using my module, you start by calling GetCFrameTable on a model. This will return a table whose keys are parts and values are their CFrames relative to PrimaryPart. After that, you call SetPrimaryPartCFrame via my module and pass in that table, and rather than grabbing the CFrames fresh every time, it uses the baked CFrames to retain accuracy.

Here’s an example of this in action. The top model uses my system, and the bottom model uses the stock SetPrimaryPartCFrame method. The video was started after a mere two full rotations about the Y axis. The CFrame of the PrimaryParts of each model, top and bottom, are -2, 5, 25446 and -2, -3, 25446 respectively (this was done far away to purposely get high inaccuracy)


The Module

The module and an example (the thing in the video) can be found here: https://www.roblox.com/library/5476680460/ModelMover

API

table ModelMover.GetCFrameTable(Model model)
Returns a table where keys are the part descendants of the model, and values are their CFrames relative to the model’s PrimaryPart. Errors if the input value is not a model, or if it is a model but it does not have a PrimaryPart set.

void ModelMover.SetPrimaryPartCFrame(Model model, CFrame cframe, table originalCFrameTable)
Sets the CFrame of the model’s PrimaryPart to the given cframe, using originalCFrameTable to orient the parts inside around the PrimaryPart. This table should be acquired via the GetCFrameTable function. Errors if the input model is not a Model or if it is a model but does not have a PrimaryPart set, errors if cframe is not a CFrame, and warns for every part that is in the model but not present in originalCFrameTable. If originalCFrameTable is nil, this will call Roblox’s default SetPrimaryPartCFrame method automatically.

For implementation of this API, refer to the bundled example.

31 Likes

Is this expensive to use for big models?

2 Likes

For all of you wondering what the source code is:

local ModelMover = {}

-- Error to throw when the input type is invalid.
-- Format args: ParameterName, ExpectedType, ActualType
local ERR_INVALID_TYPE = "Invalid type for parameter '%s' (Expected %s, got %s)"

-- Error to throw if the model's primary part is nil
local ERR_NO_PRIMARY_PART = "ModelMover.SetPrimaryCFrame() failed because no PrimaryPart has been set, or the PrimaryPart no longer exists. Please set Model.PrimaryPart before using this."

function ModelMover.SetPrimaryPartCFrame(model, cframe, originalCFrameTable)
	assert(typeof(model) == "Instance", ERR_INVALID_TYPE:format("model", "Instance (Model)", typeof(model)))
	assert(typeof(cframe) == "CFrame", ERR_INVALID_TYPE:format("cframe", "CFrame", typeof(cframe)))
	assert(model:IsA("Model"), ERR_INVALID_TYPE:format("model", "Model", model.ClassName))
	assert(model.PrimaryPart ~= nil, ERR_NO_PRIMARY_PART)
	if (originalCFrameTable == nil) then
		model:SetPrimaryPartCFrame(cframe)
		return
	end
	
	model.PrimaryPart.CFrame = cframe
	local modelDescendants = model:GetDescendants()
	for index = 1, #modelDescendants do
		local object = modelDescendants[index]
		if object == model.PrimaryPart then continue end
		if not object:IsA("BasePart") then continue end
		if originalCFrameTable[object] == nil then warn("WARNING: Object CFrame data for " .. object:GetFullName() .. " was not defined! This part may suffer inaccuracies for repeated calls!") end
		object.CFrame = model.PrimaryPart.CFrame * originalCFrameTable[object]
	end
end

function ModelMover.GetCFrameTable(model)
	assert(typeof(model) == "Instance", ERR_INVALID_TYPE:format("model", "Instance (Model)", typeof(model)))
	assert(model:IsA("Model"), ERR_INVALID_TYPE:format("model", "Model", model.ClassName))
	assert(model.PrimaryPart ~= nil, ERR_NO_PRIMARY_PART)
	
	local primaryCFrame = model.PrimaryPart.CFrame
	local altCFrames = {}
	local modelDescendants = model:GetDescendants()
	for index = 1, #modelDescendants do
		local object = modelDescendants[index]
		if object == model.PrimaryPart then continue end
		if not object:IsA("BasePart") then continue end
		altCFrames[object] = primaryCFrame:ToObjectSpace(object.CFrame)
	end
	return altCFrames
end

return ModelMover

The bigger the model is, the more expensive it is, even with the default :SetPrimaryPartCFrame that Roblox provides. If you want it to run faster, you can remove the assert commands in there(those are for debugging purposes):

local ModelMover = {}

function ModelMover.SetPrimaryPartCFrame(model, cframe, originalCFrameTable)
	if (originalCFrameTable == nil) then
		model:SetPrimaryPartCFrame(cframe)
		return
	end
	
	model.PrimaryPart.CFrame = cframe
	local modelDescendants = model:GetDescendants()
	for index = 1, #modelDescendants do
		local object = modelDescendants[index]
		if object == model.PrimaryPart then continue end
		if not object:IsA("BasePart") then continue end
		if originalCFrameTable[object] == nil then warn("WARNING: Object CFrame data for " .. object:GetFullName() .. " was not defined! This part may suffer inaccuracies for repeated calls!") end
		object.CFrame = model.PrimaryPart.CFrame * originalCFrameTable[object]
	end
end

function ModelMover.GetCFrameTable(model)
	local primaryCFrame = model.PrimaryPart.CFrame
	local altCFrames = {}
	local modelDescendants = model:GetDescendants()
	for index = 1, #modelDescendants do
		local object = modelDescendants[index]
		if object == model.PrimaryPart then continue end
		if not object:IsA("BasePart") then continue end
		altCFrames[object] = primaryCFrame:ToObjectSpace(object.CFrame)
	end
	return altCFrames
end

return ModelMover
4 Likes

Woah! I just finsihed my animation/tweening the hard way, and you posted it after I finished. :slight_smile:

WHY WHY WHYYYYYYYYYYYYYYYYYYY

note: I’ll book mark it just in case i might need it again. Thanks for this tho!

1 Like

Legend. Needed this desperately.
Thank you so much.

8 Likes

Good job. Would’ve been better a lot earlier as I literally just adopted a new system from one that depended heavily on SetPrimaryPartCFrame into one that depends on attachments :stuck_out_tongue:

Anywho, thanks for this! :Q

This is great! Extremely useful but should’ve come out earlier because some people might be having trouble finding methods to rotate models.