PartCache, for all your quick part-creation needs

I’ve just finished designing and testing a little module I’m calling “PartCache”.

This module is used to make the illusion of quickly creating and destroying parts for stuff like laser beams and such. This module uses CFrame to its advantage to work, since creating parts with Instance.new() can get very laggy.

Here’s a stress test. I have 64 sentries here each shooting up to 10 bullets per second. It’s completely lagless!

After zeuxcg made a post on a thread talking about how updating properties and creating instances can cause lag especially in regards to render-related things, I decided to make use of what he said – CFrame is the only property you can update every frame without heavy code kicking in. By keeping these cached parts in the workspace and CFraming them to a location really far away when they aren’t in use, I’ve found that I can go so far as to triple the performance of systems like the gif shown above simply by CFraming existing parts rather than creating parts and deleting them after.

While this has a number of other performance implications (e.g. moving parts far away does remove them from their current cluster which means they need to be re-added when being moved back), it’s orders of magnitude better than creating a whole new instance only to delete it right after, so the benefits outweigh the caveats quite extremely.


You can get the module here. https://www.roblox.com/library/2904807375/PartCache-Open-Source

Let me know what you think, and please don’t hesitate to post your creations using this module in the replies below!

Click here to see the API / How to use this module.

API

PCache PartCacheModule.new(BasePart template, int precreatedParts = 5)

Creates a new part cache (“PCache”) using the specified template part, creating precreatedParts parts to grab from the cache. This will error if template is nil or precreatedParts < 0.


BasePart PCache:GetPart()

Gets a part from the cache that isn’t currently in use. If there are no more parts in the cache, it will send a warning to the output and add a new part to the cache.


void PCache:ReturnPart(BasePart part)

Returns a part to the cache after you’re done using it. Automatically CFrames it to a location far away. This will throw an error if the part passed in to the function was not part of the cache’s in-use list (either by passing in a part that has already been returned to the cache or a part that was never a member of the cache in the first place).


void PCache:DestroyPart(BasePart part) [Deprecated]

Use PCache::ReturnPart instead.


void PCache:SetCacheParent(Instance parent)

Sets the parent of all cached parts to the specified instance.
Errors if the parent is not an Instance, errors if the parent is nil, errors if the cached parts will not be a descendant of Workspace.


void PCache:Dispose()

Deletes all cached parts regardless of if they are in use or available, then removes the cache object itself. Attempting to call any methods or functions of the cache will throw an error.

556 Likes

This is pretty cool. Would it really be much slower to reparent the object though, instead of moving it far away?

7 Likes

I’ve noticed that reparenting is slower, yes. As for why, I’m not entirely sure, since moving it far away also pulls it from the render queue due to being out of range.

18 Likes

Are you going to combine this with FastCast? Seems very doable but only you can comfirm that it’s a good idea.

Anyways keep up the awesome work, we need more people like you.

16 Likes

I’ve never imaginged the use of caching parts like that. I’ll end up using your module for my gun script.

Thanks for the module!

5 Likes

Wow, that’s pretty cool. I hope I can find a use for it!

4 Likes

It’s not likely that it’ll be default behavior, but it is certainly an option for the user writing the gun script.

Update for people of the future: This was added to FC.

22 Likes

I might do that with my FastCast rewrite.

3 Likes

It would be nice if you could give us some concrete statistics (ie percentages in script performance tab) on how much more efficient this is compared to reparenting / repeated instancing.

1 Like

@zeuxcg is CFrame being the only ‘fast’ updated property still the case?

Or has there been an update in this regard?

I don’t know if you meant to say that to me, but yeah, I’ll check as soon as possible.

Edit: I’m not sure of a good way to actually benchmark it. My benchmark tests seem to indicated the opposite of what zeuxcg said, so I probably implemented it wrong.

1 Like

I hope I don’t get blasted into low Earth orbit for bumping, but your module came in really handy when I was optimizing my arrow tracers. Now instead of rendering everything on the server, I just send some data off to the clients that use this module to render the tracers and I am in love with how it performs!

My own little stress test:

External Media

I did notice your API reference doesn’t accurately reflect the module. The latest version of your module creates PartCaches via .new() but your API reference in the OP claims it’s done with :CreateNewCache()

With that aside, you saved me a boatload of work and testing. Thanks for the module!

29 Likes

Thanks for pointing this out. It’s been fixed.

I’m really glad you managed to find such a good use for this, it helps to show how handy this module can get to be! :stuck_out_tongue:

11 Likes

Read the docs – The DestroyPart method returns a part to the cache so that it can be used later. The Destroy method deletes the cache itself and deletes all of the parts that are within it so that it can no longer be used.

If you see it as a good idea, I could rename the method.
Edit: I may do that anyway actually, since I’m sure other users may have stumbled upon the same confusion.
Edit 2: The proper method is now PCache:ReturnPart – DestroyPart will still work for backwards compatibility.

5 Likes

Finally got to testing this module.

PartCache:
	ON_RAY_HIT: 0.007ms
	FIRE: 0.114ms

No PartCache:
	ON_RAY_HIT: 0.014ms 
	FIRE: 0.139ms

I was using this free model along with the microprofiler to test it out. With some optimizations, I got these mildly better results:

Optimized PartCache:
	ON_RAY_HIT: 0.006ms
	FIRE: 0.109ms

So it is faster.

16 Likes

Sorry for the bump, but I have a question as to how I would actually use this module. I’m using a custom implementation of this module for an ambient particle system, but I’m not sure how I would actually return a part to the open list.

100 parts are used every second/few seconds (depends on the algorithm) like this:

local part = Pool:GetUnusedPart()
part.CFrame = CFrame.new(pos.X, pos.Y, pos.Z)
part.ParticleEmitter.Acceleration = Vector3.new(rand:NextInteger(-5, 5), rand:NextInteger(-5, 5), rand:NextInteger(-5, 5))
part.ParticleEmitter:Emit(1)

I assume to put the part back into the open list, I would just use a coroutine:

local part = Pool:GetUnusedPart()
part.CFrame = CFrame.new(pos.X, pos.Y, pos.Z)
part.ParticleEmitter.Acceleration = Vector3.new(rand:NextInteger(-5, 5), rand:NextInteger(-5, 5), rand:NextInteger(-5, 5))
part.ParticleEmitter:Emit(1)
coroutine.wrap(function()
     wait(3)
     Pool:Return(part)
end)()

But I worry that creating a coroutine for every part could cause lag, defeating the entire purpose of such a module. Is this true? Is there some other way that people should add parts back to the unused list? Thanks for the help.

3 Likes

I wonder if this module can now be further optimized with the new method :BulkMoveTo()?

2 Likes

Correct me if I’m wrong, but BulkMoveTo doesn’t replicate the movements to clients for optimization?
If so, then it’s not a good solution as the parts will still be visible after being moved to the Cache.

1 Like

You can specify a CFrame property change (basically replicated to client) I believe but its hard to know since there’s not much API information on it but I think I am correct, as you can specify something like that within a parameter.

1 Like

It depends on your implementation. In your specific case, I’d advise against using parts if your goal is to have single particles emit at random locations. A better choice might (I say might because I don’t actually know what you’re trying to do) be to parent an Attachment to terrain. CFrame the attachment, emit the particle, delay, repeat. You could have a single attachment + emitter and just randomly emit around your area. That, or better yet, just make a big part and use Roblox’s random placement (but I assume you’re avoiding that for a reason).

To answer your question, I’d advise having it all in one single function, wait included. That, or use spawn instead of coroutine.wrap as spawn ties into Roblox’s scheduler (same system as wait for the most part)

It could be done relatively easily, the issue is that there’s no real purpose for it unless you’re building close to the preset position, which would cause other far more severe problems for your game

5 Likes