“ChunkLoader, a way to add more detail to your games
without sacrificing performance.”
About
ChunkLoader is a efficient performance optimization system for all roblox games. Effectively increasing performance across all games no matter the genre, build or age.
Description
Performant:
ChunkLoader was originally made as a way to reduce lag within my games, but
it worked so well I decided to share it to the public!
Useful features:
ChunkLoader is only a single module, which means you can customize
anything without looking through a dozen dependencies.
Easily Integrated:
ChunkLoader has customizable render distance, customizable render origin,
and customizable filter list for items you dont want to unrender.
It also doesnt interfere with anything server-side, so it’s only purpose is really to just improve performance, making it easily integrated into already existing games.
How does it work?
The way ChunkLoader works is by using asset pooling, i.e all the chunks are prefilled at the start, and while the game runs the module decides what chunks to render or unrender depending on the distance from the render origin, and if it is in the dontUnrender list or not.
How To Import
Simply paste the code below into a ModuleScript and require it inside of a LocalScript within StarterPlayerScripts to use the module
local ReplicatedStorage = game:GetService "ReplicatedStorage"
local RunService = game:GetService "RunService"
local Players = game:GetService "Players"
local player = Players.LocalPlayer
local Constructor = {}
local Renderer = {}
local chunkLoaders = {}
type RBXScriptSignal = NetSignal.RBXScriptSignal
-- Creates a new Renderer object for this client.
function Constructor.new(name: string, renderDistance: number, chunkSize: number, renderOrigin: BasePart|Camera, dontUnrender: {Instance})
local self = setmetatable({}, {
__index = Renderer
})
self.renderDistance = renderDistance or 4
self.chunkSize = chunkSize or 64
self.chunks = {}
self.bound = {}
self.filteredTags = {}
self.dontUnrender = dontUnrender or {}
self.renderOrigin = renderOrigin or workspace.CurrentCamera
chunkLoaders[name] = self
return self
end
-- Gets a created Renderer from name
function Constructor.GetLoader(name: string): typeof(Constructor.new())
return chunkLoaders[name]
end
-- Returns true if position is within renderDistance
function Renderer:IsPositionWithinRenderDistance(position: Vector3)
local distance = player:DistanceFromCharacter(position)
return distance < self.renderDistance*self.chunkSize
end
-- Add more instances to the dontUnrender list
function Renderer:DontUnrender(instances: Instance | {Instance})
if typeof(instances) ~= "table" then
table.insert(self.dontUnrender, instances)
else
for _, inst in instances do
table.insert(self.dontUnrender, inst)
end
end
end
-- Sets a new renderOrigin
function Renderer:SetRenderOrigin(newOrigin: BasePart|Camera)
self.renderOrigin = newOrigin
end
-- Filters anything with the specified tag from being unrendered
function Renderer:FilterTag(tag: string, value)
self.filteredTags[tag] = value
end
-- Binds a function to either the Unrendered or the Rendered event
function Renderer:BindFunction(event: "Unrendered"|"Rendered", func: any)
self.bound[event] = self.bound[event] or {}
self.bound[event][func] = true
end
-- Gets the position of a chunk using the specified position
function Renderer:GetChunkPosition(position: Vector3)
local x = math.floor(position.X / self.chunkSize)
local z = math.floor(position.Z / self.chunkSize)
-- Calculate the corner position
local cornerX = x * self.chunkSize
local cornerZ = z * self.chunkSize
return Vector2.new(cornerX, cornerZ)
end
local function FireFunctions(self, name: string, ...)
for func, _ in self.bound[name] do
func(...)
end
end
local function Activate(self, chunkInfo)
if not chunkInfo then return end
for part: BasePart, _ in chunkInfo.parts do
chunkInfo.status = 1
part.LocalTransparencyModifier = 0
FireFunctions(self, "Rendered", part)
end
end
local function Deactivate(self)
for x, zTable in self.chunks do
for z, chunkInfo in zTable do
chunkInfo.status = 0
end
end
end
local function hasFilteredTags(self, tags)
for _, tag in tags do
if self.filteredTags[tag] then return true end
end
return false
end
local function Reload(self)
for x, zTable in self.chunks do
for z, chunkInfo in zTable do
if chunkInfo.status ~= 0 then continue end
for part: BasePart, _ in chunkInfo.parts do
if table.find(self.dontUnrender, part) or hasFilteredTags(self, part:GetTags()) then continue end
part.LocalTransparencyModifier = 1
FireFunctions(self, "Unrendered", part)
end
end
end
end
-- Updates all chunks
function Renderer:UpdateChunks()
Deactivate(self)
local originChunk = self:GetChunkPosition(self.renderOrigin.CFrame.Position)
local renderDistance = self.renderDistance
for dz = -renderDistance, renderDistance do
for dx = -renderDistance, renderDistance do
local x = originChunk.X + (dx * self.chunkSize)
local z = originChunk.Y + (dz * self.chunkSize)
local chunkInfo = self.chunks[x][z]
Activate(self, chunkInfo)
RunService.RenderStepped:Wait()
end
RunService.RenderStepped:Wait()
end
Reload(self)
end
-- Fills cache to be rendered or unrendered, can be called multiple times
function Renderer:FillCache(radius: number)
local originChunk = self:GetChunkPosition(self.renderOrigin.CFrame.Position)
local chunkHeight = 2000
local overlapParams = OverlapParams.new()
overlapParams.FilterType = Enum.RaycastFilterType.Exclude
overlapParams:AddToFilter(self.dontUnrender)
local playerCharacterFilter = {}
-- Gather all player characters once and add to the filter
for _, player in pairs(Players:GetPlayers()) do
if player.Character then
table.insert(playerCharacterFilter, player.Character:GetDescendants())
end
end
overlapParams:AddToFilter(playerCharacterFilter)
local size = Vector3.new(self.chunkSize, chunkHeight, self.chunkSize)
local function setValues(x, z)
self.chunks[x] = self.chunks[x] or {}
self.chunks[x][z] = self.chunks[x][z] or {}
self.chunks[x][z].status = self.chunks[x][z].status or 0
self.chunks[x][z].parts = self.chunks[x][z].parts or {}
end
for dz = -radius, radius do
for dx = -radius, radius do
local x = originChunk.X + (dx * self.chunkSize)
local z = originChunk.Y + (dz * self.chunkSize)
setValues(x, z)
local cframe = CFrame.new(x + self.chunkSize, 0, z + self.chunkSize)
local query = workspace:GetPartBoundsInBox(cframe, size, overlapParams)
if #query < 2 then continue end
for _, part: BasePart in query do
if self.chunks[x][z].parts[part] then continue end
self.chunks[x][z].parts[part] = true
end
RunService.RenderStepped:Wait()
end
RunService.RenderStepped:Wait() -- Extra wait after completing each row
end
end
-- Autoupdates chunks every renderstep
function Renderer:BindToRenderStepped()
local doing = false
self.loop = RunService.RenderStepped:Connect(function(dt)
local fps = 1/dt
if doing then return end
doing = true
self:UpdateChunks()
task.wait(10*(2-(fps/100)))
doing = false
end)
end
-- Autoadds any instances to the cache
function Renderer:CacheAddedDescendants()
game.DescendantAdded:Connect(function(descendant: BasePart|Model)
end)
end
-- Ends the Renderer process
function Renderer:EndProcess()
self:UnbindFromRenderStepped()
end
-- Unbind
function Renderer:UnbindFromRenderStepped()
self.loop:Disconnect()
end
return Constructor
Still confused?
The :FillCache() function fills the asset pool with chunks of a specified amount
around a specific Vector3 position. This means that anything outside of your :FillCache() range will not be considered by the ChunkLoader.
Hope this helps!
(Side note: If you want the chunk loader to keep working when the player resets, just set the ChunkLoader.renderOrigin to a new renderOrigin. Doesnt apply if you use a Camera object as the renderOrigin)
(Credits to SaintImmor for the improved post!)