PartCache, for all your quick part-creation needs

So to my understanding, this module premakes parts for you, and alleviates the need to use Instance.new() or destroy for parts. But what if I want multiple hundreds of parts, each with unique properties? It seems like partcache can only contain parts with the same properties within a cache. In my use case, I want to create multiple hundreds of parts, each with different sizes, all at the same time. But from what I can understand from the API, you have to already know the properties and amount of parts you want before you even make them. Which doesn’t seem very intuitive for something that supposedly removes the need to use instance.new(). My understanding is probably entirely wrong, just going off of what I can understand of the API.

edit: Just realized you could probably change the properties of the parts as you grab them from the cache, but would that have any bearing on the performance? And what if there aren’t enough parts in the cache?

I’m dumb, this module is incredible, I just didn’t read the API properly

1 Like

aha
same thing’s happening to me
yesterday i made this post: updateRenderQueue slow - #2 by NGC4637
basically me being very confused on my game’s slowdown

after a bit of scratching my head, i’ve found out the cause, ID_Transparent showing me that it’s computing ~6100 triangles

Hey can I ask,does spawning and deleting parts in the CIlent(Cilent-sided spawning and deleting part) lags too?

Running into a couple of issues. 1. When doing :ReturnPart, they don’t return to the parent set in SetCacheParent.

Also, any parts that are welded to the part set in PartCacheModule.new don’t get moved with the main part :confused:

SetCacheParent defines the parent for all of the parts created for that pcache. Whenever you change it, all those parts are re-parented to the new parent. I don’t understand when you say “they don’t return to the parent”, they should never have left the parent.

Example:

-- grab the module
local PartCache = require(workspace.PartCache)

-- find the part you want to cache 
local p1 = workspace:WaitForChild("P1")

-- select a parent object for the cached objects
local pool = workspace:WaitForChild("Pool")

-- I'm just using the baseplate here, but it is an alternative parent for the cache
local baseplate = workspace:WaitForChild("Baseplate")

-- create a partcache of p1's
local pcache = PartCache.new(p1,3)
-- we now have 3 objects, identical to p1 defined in a pcache

-- set the pcache parent to the pool object
pcache:SetCacheParent(pool)
-- the 3 objects created by part cache are now parented to the pool object
-- you will see the original p1 still in workspace, it is no longer part of this test.

-- summon a p1 from the pcache
local newpart = pcache:GetPart(pool)

-- do whatever you like with it
newpart:PivotTo(CFrame.new(10, 0, -10) * CFrame.new(0, 0, -10))

-- change the pcache parent to baseplate
pcache:SetCacheParent(baseplate)
-- you will now see the 3 pcache p1s parented to the baseplate (including the one you are currently moving around as newpart)

When you return a part to the cache using pcache:ReturnPart(part ) it keeps the same parent, but gets moved a long way from the origin of the map.

If WeldConstraint parts are not moving with their parent you have another problem… are they anchored, part of a model, have a badly defined constraint or constrained in some other way? You can try a simple test by seeing if you can move them successfully without using partcache. Just set up a simple pivotto script for one and see if the whole thing moves OK.

Fixed the type definition, I was converting a large majority of my code to be type strict and noticed methods were not typed, so I fixed all the type issues. Also removed the parameter variable shadowing the local below the function declaration in @LostShedGames’s reply

This should finally fix this issue:

--!strict

--[[

	
	Forked by yyyyyy09 (Crushmero) added type enforcement for methods - (09/05/2024)

	PartCache V4.0 (Fork) by Xan the Dragon // Eti the Spirit -- RBX 18406183
	Update V4.0 (Fork) has added Luau Strong Type Enforcement.
	
	Creating parts is laggy, especially if they are supposed to be there for a split second and/or need to be made frequently.
	This module aims to resolve this lag by pre-creating the parts and CFraming them to a location far away and out of sight.
	When necessary, the user can get one of these parts and CFrame it to where they need, then return it to the cache when they are done with it.
	
	According to someone instrumental in Roblox's backend technology, zeuxcg (https://devforum.roblox.com/u/zeuxcg/summary)...
		>> CFrame is currently the only "fast" property in that you can change it every frame without really heavy code kicking in. Everything else is expensive.
		
		- https://devforum.roblox.com/t/event-that-fires-when-rendering-finishes/32954/19
	
	This alone should ensure the speed granted by this module.
		
		
	HOW TO USE THIS MODULE:
	
	Look at the bottom of my thread for an API! https://devforum.roblox.com/t/partcache-for-all-your-quick-part-creation-needs/246641
--]]

-----------------------------------------------------------
-------------------- MODULE DEFINITION --------------------
-----------------------------------------------------------

local PartCacheStatic = {}
PartCacheStatic.__index = PartCacheStatic
PartCacheStatic.__type = "PartCache" -- For compatibility with TypeMarshaller

-- TYPE DEFINITION: Part Cache Instance
export type PartCacheMethods = {
	__index: PartCacheMethods,
	__type: string,
	new: (
		template: BasePart,
		_numPrecreatedParts: number?,
		currentCacheParent: Instance?
	) -> PartCache,
	GetPart: (self: PartCache) -> BasePart,
	ReturnPart: (
		self: PartCache, 
		part: BasePart
	) -> (),
	SetCacheParent: (
		self: PartCache, 
		newParent: Instance
	) -> (),
	Expand: (
		self: PartCache,
		numParts: number
	) -> (),
	Dispose: (self: PartCache) -> ()
}

type PartCacheDataMembers = {
	Open: {[number]: BasePart} | {},
	InUse: {[number]: BasePart} | {},
	CurrentCacheParent: Instance,
	Template: BasePart,
	ExpansionSize: number
}


export type PartCache = typeof(
	setmetatable(
		{} :: PartCacheDataMembers, 
		{} :: PartCacheMethods
	)
)


-----------------------------------------------------------
----------------------- STATIC DATA -----------------------
-----------------------------------------------------------					

-- A CFrame that's really far away. Ideally. You are free to change this as needed.
local CF_REALLY_FAR_AWAY = CFrame.new(0, 10e8, 0)

-- Format params: methodName, ctorName
local ERR_NOT_INSTANCE = "Cannot statically invoke method '%s' - It is an instance method. Call it on an instance of this class created via %s"

-- Format params: paramName, expectedType, actualType
local ERR_INVALID_TYPE = "Invalid type for parameter '%s' (Expected %s, got %s)"

-----------------------------------------------------------
------------------------ UTILITIES ------------------------
-----------------------------------------------------------

--Similar to assert but warns instead of errors.
local function assertwarn(requirement: boolean, messageIfNotMet: string)
	if requirement == false then
		warn(messageIfNotMet)
	end
end

--Dupes a part from the template.
local function MakeFromTemplate(template: BasePart, currentCacheParent: Instance): BasePart
	local part: BasePart = template:Clone()
	-- ^ Ignore W000 type mismatch between Instance and BasePart. False alert.
	
	part.CFrame = CF_REALLY_FAR_AWAY
	part.Anchored = true
	part.Parent = currentCacheParent
	return part
end

function PartCacheStatic.new(
	template: BasePart, 
	_numPrecreatedParts: number?, 
	currentCacheParent: Instance?
): PartCache
	local newNumPrecreatedParts: number = _numPrecreatedParts or 5
	local newCurrentCacheParent: Instance = currentCacheParent or workspace
	
	--PrecreatedParts value.
	--Same thing. Ensure it's a number, ensure it's not negative, warn if it's really huge or 0.
	assert(newNumPrecreatedParts > 0, "PrecreatedParts can not be negative!")
	assertwarn(newNumPrecreatedParts ~= 0, "PrecreatedParts is 0! This may have adverse effects when initially using the cache.")
	assertwarn(template.Archivable, "The template's Archivable property has been set to false, which prevents it from being cloned. It will temporarily be set to true.")
	
	local oldArchivable = template.Archivable
	template.Archivable = true
	local newTemplate: BasePart = template:Clone()
	
	template.Archivable = oldArchivable
	template = newTemplate
	
	local object = {
		Open = {},
		InUse = {},
		CurrentCacheParent = newCurrentCacheParent,
		Template = template,
		ExpansionSize = 10
	}
	setmetatable(object, PartCacheStatic)
	
	for _ = 1, newNumPrecreatedParts do
		table.insert(object.Open, MakeFromTemplate(template, object.CurrentCacheParent))
	end
	object.Template.Parent = nil
	
	return object :: PartCache
end

-- Gets a part from the cache, or creates one if no more are available.
function PartCacheStatic:GetPart(): BasePart
	assert(getmetatable(self) == PartCacheStatic, ERR_NOT_INSTANCE:format("GetPart", "PartCache.new"))
	
	if #self.Open == 0 then
		warn("No parts available in the cache! Creating [" .. self.ExpansionSize .. "] new part instance(s) - this amount can be edited by changing the ExpansionSize property of the PartCache instance... (This cache now contains a grand total of " .. tostring(#self.Open + #self.InUse + self.ExpansionSize) .. " parts.)")
		for i = 1, self.ExpansionSize, 1 do
			table.insert(self.Open, MakeFromTemplate(self.Template, self.CurrentCacheParent))
		end
	end
	local part = self.Open[#self.Open]
	self.Open[#self.Open] = nil
	table.insert(self.InUse, part)
	return part
end

-- Returns a part to the cache.
function PartCacheStatic:ReturnPart(part: BasePart)
	assert(getmetatable(self) == PartCacheStatic, ERR_NOT_INSTANCE:format("ReturnPart", "PartCache.new"))
	
	local index = table.find(self.InUse, part)
	if index ~= nil then
		table.remove(self.InUse, index)
		table.insert(self.Open, part)
		part.CFrame = CF_REALLY_FAR_AWAY
		part.Anchored = true
	else
		error("Attempted to return part \"" .. part.Name .. "\" (" .. part:GetFullName() .. ") to the cache, but it's not in-use! Did you call this on the wrong part?")
	end
end

-- Sets the parent of all cached parts.
function PartCacheStatic:SetCacheParent(newParent: Instance)
	assert(getmetatable(self) == PartCacheStatic, ERR_NOT_INSTANCE:format("SetCacheParent", "PartCache.new"))
	assert(newParent:IsDescendantOf(workspace) or newParent == workspace, "Cache parent is not a descendant of Workspace! Parts should be kept where they will remain in the visible world.")
	
	self.CurrentCacheParent = newParent
	for i = 1, #self.Open do
		self.Open[i].Parent = newParent
	end
	for i = 1, #self.InUse do
		self.InUse[i].Parent = newParent
	end
end

-- Adds numParts more parts to the cache.
function PartCacheStatic:Expand(numParts: number): ()
	assert(getmetatable(self) == PartCacheStatic, ERR_NOT_INSTANCE:format("Expand", "PartCache.new"))
	if numParts == nil then
		numParts = self.ExpansionSize
	end
	
	for i = 1, numParts do
		table.insert(self.Open, MakeFromTemplate(self.Template, self.CurrentCacheParent))
	end
end

-- Destroys this cache entirely. Use this when you don't need this cache object anymore.
function PartCacheStatic:Dispose()
	assert(getmetatable(self) == PartCacheStatic, ERR_NOT_INSTANCE:format("Dispose", "PartCache.new"))
	for i = 1, #self.Open do
		self.Open[i]:Destroy()
	end
	for i = 1, #self.InUse do
		self.InUse[i]:Destroy()
	end
	self.Template:Destroy()
	self.Open = {}
	self.InUse = {}
	self.CurrentCacheParent = nil
	
	self.GetPart = nil
	self.ReturnPart = nil
	self.SetCacheParent = nil
	self.Expand = nil
	self.Dispose = nil
end

return PartCacheStatic
2 Likes