├── selene.toml ├── default.project.json ├── .vscode └── settings.json ├── rokit.toml ├── .gitignore ├── wally.toml ├── sourcemap.json ├── dev.project.json ├── wally.lock ├── src ├── index.d.ts └── init.luau ├── package.json ├── LICENSE └── README.md /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ObjectCache", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "luau-lsp.sourcemap.rojoProjectFile": "dev.project.json", 3 | "discord.enabled": true 4 | } -------------------------------------------------------------------------------- /rokit.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | rojo = "rojo-rbx/rojo@7.4.2" 3 | wally = "UpliftGames/wally@0.3.2" 4 | selene = "Kampfkarren/selene@0.27.1" -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Roblox Studio place files 2 | /*.rbxlx 3 | /*.rbxl 4 | 5 | # Roblox Studio model files 6 | /*.rbxmx 7 | /*.rbxm 8 | 9 | # Project miscellaneous files 10 | /Packages 11 | /*.lock 12 | -------------------------------------------------------------------------------- /wally.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pyseph/objectcache" 3 | version = "1.4.6" 4 | registry = "https://github.com/UpliftGames/wally-index" 5 | realm = "shared" 6 | description = "A robust, blazing fast model and part cache for Roblox." 7 | license = "MIT" -------------------------------------------------------------------------------- /sourcemap.json: -------------------------------------------------------------------------------- 1 | {"name":"ObjectCache","className":"DataModel","filePaths":["dev.project.json"],"children":[{"name":"ReplicatedStorage","className":"ReplicatedStorage","children":[{"name":"ObjectCache","className":"ModuleScript","filePaths":["src\\init.luau"]}]}]} -------------------------------------------------------------------------------- /dev.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ObjectCache", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "$className": "ReplicatedStorage", 7 | "ObjectCache": { 8 | "$path": "src" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /wally.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Wally. 2 | # It is not intended for manual editing. 3 | registry = "test" 4 | 5 | [[package]] 6 | name = "boatbomber/hashlib" 7 | version = "1.0.0" 8 | dependencies = [] 9 | 10 | [[package]] 11 | name = "pyseph/objectcache" 12 | version = "0.0.1" 13 | dependencies = [["HashLib", "boatbomber/hashlib@1.0.0"]] 14 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | interface ObjectCache { 2 | GetPart(partCFrame?: CFrame): T; 3 | ReturnPart(part: T): void; 4 | 5 | IsInUse(part: T): boolean; 6 | 7 | ExpandCache(amount: number): void; 8 | SetExpandAmount(amount: number): void; 9 | 10 | Update(): void; 11 | } 12 | declare const ObjectCache: new (template: T, cacheSize?: number, cachesContainer?: Folder) => ObjectCache; 13 | export = ObjectCache; 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rbxts/objectcache", 3 | "version": "1.0.0", 4 | "main": "src/init.luau", 5 | "scripts": { 6 | "build": "rbxtsc", 7 | "watch": "rbxtsc -w", 8 | "prepublishOnly": "npm run build" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "description": "", 14 | "types": "src/index.d.ts", 15 | "files": [ 16 | "src" 17 | ], 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "devDependencies": { 22 | "@rbxts/compiler-types": "^2.3.0-types.2", 23 | "@rbxts/types": "^1.0.805", 24 | "roblox-ts": "^2.3.0", 25 | "typescript": "^5.6.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Pyseph 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | ObjectCache 4 |
A robust, blazing fast part cache for Roblox
5 | 6 | ## Introduction 7 | ObjectCache is a modern implementation on handling thousands of active parts, while keeping performance high. 8 | 9 | ## Why ObjectCache? 10 | The source code is minimal and with no bloat, and with an important difference - it uses Roblox's `BulkMoveTo` API to achieve peak performance. Instead of manually setting the CFrame of each part one-by-one, all parts are batched together. And best of all, you don't need to worry about any of this! ObjectCache puts all returned objects into a queue to move them away, in the same frame as you call the method. 11 | 12 | # Supports models 13 | By utilizing the engine's behavior, the ObjectCache supports using models directly -- and it remains as efficient as ever, much faster than calling `:PivotTo`! To use a model as the template, you just have to set a PrimaryPart, and weld all parts in the model to the PrimaryPart. ObjectCache will call `BulkMoveTo` on the PrimaryPart, which will automatically move all the other parts in the model with it, at much faster speeds than normal! 14 | 15 | ## API 16 | The API is dead simple - easy to learn, and eventually forget. 17 | ObjectCache is fully typed, but an API reference is always handy to have: 18 | ```luau 19 | ObjectCacheConstructor.new(Template: BasePart, CacheSize: number?, CachesContainer: Folder?) -> ObjectCache 20 | 21 | ObjectCache:GetPart(PartCFrame: CFrame?): BasePart 22 | 23 | ObjectCache:ReturnPart(Part: BasePart) 24 | 25 | ObjectCache:Destroy() 26 | ``` 27 | You may have noticed that `ObjectCache:GetPart` lets you provide an optional target CFrame. This argument lets you squeeze out the maximum performance out of the module! Since `:GetPart(TargetCFrame)` and `:ReturnPart(Object)` both use queued `workspace:BulkMoveTo` calls, it can actually be more performant to call these two methods every frame. 28 | The following video calls :GetPart and :ReturnPart every frame, yet runs smooth as butter: 29 | 30 | *The place file used in the above video* (*can be found here*)[] 31 | 32 | # Installation 33 | 34 | Simply grab the file from Roblox marketplace: https://create.roblox.com/store/asset/18819618773/ObjectCache 35 | -------------------------------------------------------------------------------- /src/init.luau: -------------------------------------------------------------------------------- 1 | --!strict 2 | --!native 3 | local FAR_AWAY_CFRAME = CFrame.new(2^24, 2^24, 2^24) 4 | local EXPAND_BY_AMOUNT = 50 5 | 6 | local MovingParts = table.create(10_000) 7 | local MovingCFrames = table.create(10_000) 8 | 9 | local ScheduledUpdate = false 10 | local function UpdateMovement() 11 | while true do 12 | workspace:BulkMoveTo(MovingParts, MovingCFrames, Enum.BulkMoveMode.FireCFrameChanged) 13 | 14 | table.clear(MovingParts) 15 | table.clear(MovingCFrames) 16 | 17 | ScheduledUpdate = false 18 | coroutine.yield() 19 | end 20 | end 21 | local UpdateMovementThread = coroutine.create(UpdateMovement) 22 | 23 | local Cache = {} 24 | Cache.__index = Cache 25 | 26 | function Cache:_GetNew(Amount: number, Warn: boolean) 27 | if Warn then 28 | warn(`ObjectCache: Cache retrieval exceeded preallocated amount! expanding by {Amount}...`) 29 | end 30 | 31 | local FreeObjectsContainer = self._FreeObjects 32 | local InitialLength = #self._FreeObjects 33 | local CacheHolder = self.CacheHolder 34 | 35 | local IsTemplateModel = self._IsTemplateModel 36 | local Template: Model | BasePart = self._Template 37 | 38 | local TargetParts = table.create(Amount) 39 | local TargetCFrames = table.create(Amount) 40 | local AddedObjects = table.create(Amount) 41 | for Index = InitialLength + 1, InitialLength + Amount do 42 | local Object = Template:Clone() 43 | local ObjectRoot: BasePart = if IsTemplateModel then (Object:: Model).PrimaryPart:: BasePart else Object:: BasePart 44 | 45 | FreeObjectsContainer[Index] = ObjectRoot 46 | 47 | local OffsetIndex = Index - InitialLength 48 | TargetParts[OffsetIndex] = ObjectRoot 49 | TargetCFrames[OffsetIndex] = FAR_AWAY_CFRAME 50 | AddedObjects[OffsetIndex] = Object 51 | end 52 | 53 | workspace:BulkMoveTo(TargetParts, TargetCFrames, Enum.BulkMoveMode.FireCFrameChanged) 54 | 55 | for _, Object in AddedObjects do 56 | (Object:: Instance).Parent = CacheHolder 57 | end 58 | 59 | return table.remove(FreeObjectsContainer) 60 | end 61 | 62 | function Cache:GetPart(PartCFrame: CFrame?): BasePart 63 | local Part = table.remove(self._FreeObjects) or self:_GetNew(self._ExpandAmount, true) 64 | 65 | self._Objects[Part] = nil 66 | if PartCFrame then 67 | table.insert(MovingParts, Part) 68 | table.insert(MovingCFrames, PartCFrame) 69 | 70 | if not ScheduledUpdate then 71 | ScheduledUpdate = true 72 | task.defer(UpdateMovementThread) 73 | end 74 | end 75 | 76 | return Part 77 | end 78 | function Cache:ReturnPart(Part: BasePart) 79 | if self._Objects[Part] then 80 | return 81 | end 82 | 83 | self._Objects[Part] = true 84 | 85 | table.insert(self._FreeObjects, Part) 86 | table.insert(MovingParts, Part) 87 | table.insert(MovingCFrames, FAR_AWAY_CFRAME) 88 | 89 | if not ScheduledUpdate then 90 | ScheduledUpdate = true 91 | task.defer(UpdateMovementThread) 92 | end 93 | end 94 | 95 | function Cache:Update() 96 | task.spawn(UpdateMovementThread) 97 | end 98 | 99 | function Cache:ExpandCache(Amount: number) 100 | assert(typeof(Amount) ~= "number" or Amount >= 0, `Invalid argument #1 to 'ObjectCache:ExpandCache' (positive number expected, got {typeof(Amount)})`) 101 | self:_GetNew(Amount, false) 102 | end 103 | function Cache:SetExpandAmount(Amount: number) 104 | assert(typeof(Amount) ~= "number" or Amount > 0, `Invalid argument #1 to 'ObjectCache:SetExpandAmount' (positive number expected, got {typeof(Amount)})`) 105 | self._ExpandAmount = Amount 106 | end 107 | 108 | function Cache:IsInUse(Object: BasePart): boolean 109 | return self._Objects[Object] == nil 110 | end 111 | 112 | function Cache:Destroy() 113 | self.CacheHolder:Destroy() 114 | end 115 | 116 | local function GetCacheContainer() 117 | local CacheHolder = Instance.new("Folder") 118 | CacheHolder.Name = "ObjectCache" 119 | 120 | return CacheHolder 121 | end 122 | 123 | local Constructor = {} 124 | function Constructor.new(Template: BasePart | Model, CacheSize: number?, CachesContainer: Instance?) 125 | local TemplateType = typeof(Template) 126 | assert(TemplateType == "Instance", `Invalid argument #1 to 'ObjectCache.new' (BasePart expected, got {TemplateType})`) 127 | 128 | assert(Template:IsA("BasePart") or Template:IsA("Model"), `Invalid argument #1 to 'ObjectCache.new' (BasePart or Model expected, got {Template.ClassName})`) 129 | assert(Template.Archivable, `ObjectCache: Cannot use template object provided, as it has Archivable set to false.`) 130 | if Template:IsA("Model") then 131 | assert(Template.PrimaryPart ~= nil, `Invalid Template provided to 'ObjectCache.new': Model has no PrimaryPart set!`) 132 | end 133 | 134 | local CacheSizeType = typeof(CacheSize) 135 | assert(CacheSize == nil or CacheSizeType == "number", `Invalid argument #2 to 'ObjectCache.new' (number expected, got {CacheSizeType})`) 136 | assert(CacheSize == nil or CacheSize >= 0, `Invalid argument #2 to 'ObjectCache.new' (positive number expected, got {CacheSize})`) 137 | 138 | local ContainerType = typeof(CachesContainer) 139 | assert(CachesContainer == nil or ContainerType == "Instance", `Invalid argument #3 to 'ObjectCache.new' (Instance expected, got {ContainerType})`) 140 | 141 | local PreallocAmount = CacheSize or 10 142 | local CacheParent = GetCacheContainer() 143 | 144 | local Objects: {[BasePart]: boolean} = {} 145 | local FreeObjects: {BasePart | Model} = table.create(PreallocAmount) 146 | 147 | local TargetParts = table.create(PreallocAmount) 148 | 149 | local IsTemplateModel = Template:IsA("Model") 150 | for Index = 1, PreallocAmount do 151 | local Object = Template:Clone() 152 | local ObjectRoot: BasePart = if IsTemplateModel then (Object:: Model).PrimaryPart:: BasePart else Object:: BasePart 153 | 154 | FreeObjects[Index] = Object 155 | TargetParts[Index] = ObjectRoot 156 | 157 | ObjectRoot.CFrame = FAR_AWAY_CFRAME; 158 | (Object:: Instance).Parent = CacheParent 159 | end 160 | 161 | CacheParent.Parent = CachesContainer or workspace 162 | 163 | return setmetatable({ 164 | CacheHolder = CacheParent, 165 | _ExpandAmount = EXPAND_BY_AMOUNT, 166 | _Template = Template, 167 | _FreeObjects = TargetParts, 168 | _Objects = Objects, 169 | _IsTemplateModel = IsTemplateModel, 170 | _PreallocatedAmount = PreallocAmount, 171 | }, Cache) 172 | end 173 | 174 | return Constructor --------------------------------------------------------------------------------