PartCache, for all your quick part-creation needs

Sorry for the double reply but i managed to fix the issue by making a small tweak to the PartCache script, i made it so bullets were created in a closer position to the player’s spawn under the map and then moved them back towards the storage area after a wait()

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 = CFrame.new(0, 0, 0) --modified from 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
...
for _ = 1, newNumPrecreatedParts do
		local part = MakeFromTemplate(template, object.CurrentCacheParent)
		wait()
		part.CFrame = CF_REALLY_FAR_AWAY
		table.insert(object.Open, part)
	end
...

function PartCacheStatic:GetPart(): BasePart
...
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
			local part = MakeFromTemplate(self.Template, self.CurrentCacheParent)
			wait()
			part.CFrame = CF_REALLY_FAR_AWAY
			table.insert(self.Open, part)
		end
	end
...

Not sure if this is the cleanest solution but hopefully it helps anyone having a similar issue

Have you tried putting the parts in a model with ModelStreamingMode Persistent?

Can you add in a new function named PCache:GetAvailableParts() or PCache:GetUsingParts(), or even PCache:GetPreCreatedPart()? I am trying to detect when there are no using parts, so that when the ReturnPart() function is called, i can check whether it is the last one to delete the part cache. Basically, i am using FastCast, but i encountered a problem in which the bullets completely disappear. My most recent solution is to create a new partcache with a relatively low precreated part amount, then dispose it once all bullets has been returned.

Putting this here for anyone else who gets this issue:

Sometimes when you return parts to their cache, the shadows glitch and flash off an back on. This is undesirable especially if you’re returning a bunch of parts frequently.

To resolve this just go in the module and change the huge CFrame constant to something smaller. I did 1e7

6 Likes

.CFrame do be taking performances however it is only possible to notify at around thousands of parts, which lets be real will probably not happen in a single frame often, but if you somehow manage to get conditions like this, i’m pretty sure you can use something called workspace:BulkMoveTo(partList, cframeList)

basically it allows you to update more parts cframes in a single frame way faster, here we could kinda get every moved part in the frame, then use bulkmoveto to move all of them at once, which could probably make performances better?

Is this also possible with other instances than only BaseParts like SurfaceGuis for example?

Is there a way to get an already existing partcache?

I found what I think is a bug in PartCache.

If you GetPart(), then set the CFrame and then set Anchored = false the part will fall (as expected).

But if you ReturnPart() then do the same thing again the Part will disappear.

This can be fixed by changing this line in ReturnPart()

part.Anchored = true

Change to:

part.Anchored = true
part.Velocity = Vector3.new(0, 0, 0)
part.RotVelocity = Vector3.new(0, 0, 0)

It seems just setting Anchored = true alone is not enough, otherwise the cached part retains the velocity data and when re-summoned it will be moving at the old velocity as soon as it is unanchored.

Do you know if there a way to get an already existing partcache?

for future people: its too late, i just made my own partcache plugin lol

how would i refill the cache if a cache part gets destroyed?
im using part cache for debris, but if the debris falls into the void and gets destroyed, i dont think the part cache refills itself, as the part isnt being returned. how do i refill the cache?

Thank you for sharing this useful module.

I noticed there is a very minor error in PartCacheStatic.new definition.
You use:

local newNumPrecreatedParts: number = numPrecreatedParts or 5

to deal with the possibility of getting a nil argument for numPrecreatedParts, but then two lines later you

assert(numPrecreatedParts > 0, “PrecreatedParts can not be negative!”)
assertwarn(numPrecreatedParts ~= 0, “PrecreatedParts is 0! This may have adverse effects when initially using the cache.”)

Whilst this still works, it is responsible for some of the type def warnings (using a relational operator against a nil), I changed the function to this:

function PartCacheStatic.new(template: BasePart, _numPrecreatedParts: number?, currentCacheParent: Instance?): PartCache
	local numPrecreatedParts: number = _numPrecreatedParts or 5
	local newCurrentCacheParent: Instance = currentCacheParent or workspace

	assert(numPrecreatedParts > 0, "PrecreatedParts can not be negative!")
	assertwarn(numPrecreatedParts ~= 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: PartCache = {
		Open = {},
		InUse = {},
		CurrentCacheParent = newCurrentCacheParent,
		Template = template,
		ExpansionSize = 10
	}
	setmetatable(object, PartCacheStatic)
	
	for _ = 1, numPrecreatedParts do
		table.insert(object.Open, MakeFromTemplate(template, object.CurrentCacheParent))
	end
	object.Template.Parent = nil
	
	return object
	-- ^ Ignore mismatch here too
end

I think the metatable type warning for the return object is unavoidable.

1 Like

Is PivotTo() faster than directly changing the CFrame? I understand that it doesn’t fire property changed events, but are we sure there isn’t some other overhead?

PivotTo() is basically .CFrame but mostly used for models or objects with a different pivot. PivotTo is just meant to replace SetPrimaryPartCFrame.

I have seen in multiple posts that they are around the same speed as they both update the geometry in the same way kinda (and from an entity systems i have used back then with 2k enemies basically around 14000~16000 parts, pivotto wasn’t any different than .CFrame).

1 thing which is more efficient however is workspace:BulkMoveTo(), which is the best in therm of performance, but you should only use it when you have to move a lot of parts in the same frame (reason why it is called BulkMoveTo), else it kinda becomes useless

i don’t really think it will be of any use for part cache however

yes late answer

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