I was actually going to use a global table for all parts, call BulkMoveTo at the end of the frame, then clear the table. This approach seems best for updating unknown quantities of arbitrary parts within small/medium assemblies, as well as within my foliage system where hundreds of simple animated objects are stored in an intrusive-doubly-linked-list and updated/throttled with a fixed time budget.
Here’s what the system I’m ready to test looks like:
local GlobalBulkMoveToCount = {0}
local GlobalBulkMoveToPartList = {}
local GlobalBulkMoveToCFrameList = {}
game:GetService("RunService").RenderStepped:Connect(function()
-- [Update characters and UI here]
-- End of frame:
local count = GlobalBulkMoveToCount[1]--$const
if count >= 1 then
GlobalBulkMoveToCount[1] = 0 -- Reset counter
workspace:BulkMoveTo(GlobalBulkMoveToPartList, GlobalBulkMoveToCFrameList, Enum.BulkMoveMode.FireNoEvents)
for i = 1, count do -- Clear tables
GlobalBulkMoveToPartList[i] = nil
GlobalBulkMoveToCFrameList[i] = nil
end
end
end)
Here’s how a part is added:
local count = GlobalBulkMoveToCount[1] + 1
GlobalBulkMoveToCount[1] = count
GlobalBulkMoveToPartList[count] = part
GlobalBulkMoveToCFrameList[count] = partCFrame
Here’s an overview of how my skeletal animation system works and how it would use it:
local GetComponents = CFrame.new().GetComponents
-- This creates a fast bone update function for its specific state. This is cheap compared to how many times it will be called on average.
local createFastBoneUpdateFunction = function(bone)
local getBoneOffset = bone[2] -- This is a function that returns the bone's current offset cframe.
local partList = bone[3] -- The list of parts connected to this bone and their corresponding offsets. Stored {part1, offset1, part2, offset2, ...}
local staticFunctionList = bone[4] -- A list of functions connected to this bone, that only need to be updated when the bone's cframe changes. Primarily used by bones with a fixed offset.
local mobileFunctionList = bone[5] -- A list of functions connected to this bone that need to be called even if the bone's cframe doesn't change. Primarily used by bones that are currently animating.
-- There are a few hundred common cases with auto-generated source code. Here's a documented example:
if #partList == 2 and #staticFunctionList == 1 and #mobileFunctionList == 1 then
local part1, offset1, staticFunction1, mobileFunction1 = partList[1], partList[2], staticFunctionList[1], mobileFunctionList[1]
return function(cframe, positionThreshold, rotationThreshold)
-- 'cframe' is the parent bone's resulting CFrame, with the matrix transformed by CFrame.new(0,0,0, scaleFactor,0,0, 0,scaleFactor,0, 0,0,scaleFactor)
cframe = cframe * getBoneOffset() -- Animate the cframe
-- The thresholds are used to reduce the number of part updates, especially for characters that are far away or recently off-screen.
-- positionThreshold = (basePositionThresholdInStuds * scaleFactor)^2
-- potationThreshold = math.cos(rotationThresholdInRadians) * (scaleFactor^2)
-- Here we test to see if the cframe has changed enough to update.
-- This is huge optimization for characters that are standing still, but would benefit an API like 'cframe:FuzzyEq(other, positionThreshold, matrixThreshold)'
local px0, py0, pz0, xx0, yx0, zx0, xy0, yy0, zy0 = GetComponents(bone[1]) -- bone[1] is the bone's last updated cframe
local px1, py1, pz1, xx1, yx1, zx1, xy1, yy1, zy1 = GetComponents(cframe)
if (px1 - px0)^2 + (py1 - py0)^2 + (pz1 - pz0)^2 > positionThreshold or xx0*xx1 + yx0*yx1 + zx0*zx1 < rotationThreshold or xy0*xy1 + yy0*yy1 + zy0*zy1 < rotationThreshold then
bone[1] = cframe -- Set the last updated cframe
staticFunction1(cframe, positionThreshold, rotationThreshold) -- Update staticFunctionList
-- Update partList
local count = GlobalBulkMoveToCount[1] + 1
GlobalBulkMoveToCount[1] = count
GlobalBulkMoveToPartList[count] = part1
GlobalBulkMoveToCFrameList[count] = cframe * offset1
end
mobileFunction1(cframe, positionThreshold, rotationThreshold) -- Update mobileFunctionList
end
end
end