How many actors per module? The total of 24 or something? Two different Modules running work would run on the Same parallel phase, but on different threads if they have their own actors…? Right.
yeah, the default is 24, in the module there’s a DEFAULT_MAX_WORKERS variable at the begining of the module, currently set to 24 for the client and 48 for the server
They do run on their own threads, but then roblox distributes those threads onto your cpu threads (so like lets say you have 24 actors doing work, on a 4 thread cpu, roblox will distribute the work of those actors to the 4 cpu threads). This is why lowering that value can be beneficial if you have multiple modules doing work in parallel, to reduce overhead
I don’t know exactly how the Roblox serial and parallel system works, but there can be multiple parallel phases/serial phases during a frame. I would guess that after a serial phase, it starts a parallel phase with whatever was told to run in parallel, then serial, back to parallel if needed…
I am 90% this is due to this update Deferred Engine Events: Rollout Update but now if you schedule a bunch of work using :ScheduleWork, and then immediately use :Work() this module breaks. I think is because the actors are created on the fly, but they don’t have their :ConnectParallel work until the frame after, and thus the first :ScheduleWork will break since the event is just nonexistant. Every :ScheduleWork after the first broken one works however since it’s just reusing the Actors from the previous attempt which have existed for longer than a frame and do have their :ConnectParallel set. If you also put a task.wait() it also fixes itself.
I edited the module script myself to make it generate all the actors as soon as it is required() (if you require and instantly schedulework it might still break, but in my use case that never happens, so I don’t care)
This is one of the weirdest bugs I’ve ever seen. Every four times you use this function, it yields. No matter if you unroll the loop or not. Really strange. It has something to do with ResultEvent getting it’s :Wait() interrupted one frame late, but why I have no idea
Edit: if you yield the script yourself using something like task.wait() the script not longer yields suggesting that there is a limit to the amount of times you can wait on the bindable (in a single frmae) before it forces the script to yield. Very strange indeed. Maybe someone could get around this by using multiple bindables however for me this doesn’t matter cause I’m not gonna be using this more than once a frame, it just so happens that when I was testing performance, I was calling the parallel scheduler multiple times per frame causing this chain of events (pun unintended).
That is definitely odd, I’ll have to look into it
I GOT A FIX :D
I really did not expect the fix to be this easy, the scripts not initializing before :Work() was one issue, but then, for some reason, the shared table with the results was empty (something I didn’t get to investigate before I found the easy fix)
It was indeed due to Deferred mode… Um, I think deferred mode also makes the initialization of scripts deferred, meaning the scripts don’t initialize in time, but only after the thread (that called :ScheduleWork()) finishes
As you have said. However, it breaks because the Work event is fired before the scripts connects to the event. :ConnectParallel(). It connects way before the next frame, and actually starts working right after, so using task.defer(coroutine.running())
fixes it, no need to wait a full frame (though, then the shared table for the results is empty…)
So the fix
Deferring WorkEvent:Fire()
, like this
(This is the equivalent of
task.defer(function() WorkEvent:Fire() end)
)
This seems to fix everything, and the results are in the shared tabled. (I thought maybe the issue for that was that I put task.defer(coroutine.running())
before the place where the shared table is cleared, but putting it after doesn’t work either…)
Thank you for pointing this out. I’m somewhat surprised that I didn’t notice this before, seems like none of the games I’ve used this in are in deferred mode
The test place and the model have been updated
I’m too lazy to test that, but can you tell me if you are still running into the weird bug you are having? (it yielding every four times)
It might have to do with task.synchronize and task.desynchronize only being able to be used 4 times per frame, idk. I thought there was no limit for that
Hello, great module youve made!
Im curious if you could give an example of you using it on the server as opposed to just the client
Im not quite proficient enough with lua to interpret this as easily as id like to, and im currently trying to impliment parallel lua into my game which has a pretty heavy physics load. Id like to transfer this load to multiple threads/workers for better performance but am at a road block in the implementation.
Thanks in advance!
It works the same way on the server
What is the “physics load” in question? If you are talking about the roblox physics engine, you cannot make that parallel, roblox would have to do it themselves by adding a :SetThreadOwnership(), kind of like :SetNetworkOwnership()
Anyway, here is an example where I use it on the server, to deserialize a table that can get pretty big
local GameDatabase = Games[Task.TaskIndex]
if not GameDatabase then
local Data = DatastoreFunctions:GetGamesData(Task.TaskIndex) or tostring(GamesSerializer:GetLastestVersion())
Data = string.split(Data,"_")
local _version = tonumber(table.remove(Data,1))
local DataLenght = GamesSerializer:GetDataLenghtFromVersion(_version)
local GamesAmount = table.maxn(Data)/DataLenght
local Index = 0
for i = 0, Settings.ServerThreads -1 do
local Tasks = math.floor(GamesAmount/(Settings.ServerThreads-i))
GamesAmount -= Tasks
local a = Index + 1
local b = Index + Tasks*DataLenght
Index += Tasks*DataLenght
SerDeserModules.Games.Deser:ScheduleWork(table.concat(Data,"_",a,b),_version)
end
local FinalData = {}
Data = SerDeserModules.Games.Deser:Work()
for i, v in ipairs(Data) do
table.move(v,1,table.maxn(v),table.maxn(FinalData)+1,FinalData)
end
Data = nil -- TODO -- what the hell is happening here, why is this needed
local DataSize = table.maxn(FinalData)
Games[Task.TaskIndex] = {
Data = FinalData,
DataSize = DataSize,
Index = math.random(1,math.max(DataSize,1)),
}
GameDatabase = Games[Task.TaskIndex]
end
Here is the Deser module
local Settings = require(game.ReplicatedStorage.Settings)
local TasksPerThreads = Settings.SponsorsPerDatastore/Settings.ServerThreads
if TasksPerThreads ~= math.round(TasksPerThreads) then error("Invalid settings, cannot spread tasks evenly)") end
local Serializer = require(game.ServerScriptService.ServerTasks.TaskScript.Sponsors.SponsorsSerializer)
return function(String, _version, TaskIndex : number)
return Serializer:DecompressArray(String, _version)
end
the DecompressArray method is for decompressing multiple elements at once. I made my code to make it specifically Schedule work for every thread available (aka the number of actors, which is set in the settings for the module), instead of doing ScheduleWork for every element separately and having the ParallelScheduler merge them. It’s better performance wise to do it like this. It does complicate things a bit though
It is much simpler when not merging tasks together, though if you are getting into the territory of maybe 300-500 smallish tasks or more, you should merge them
Much appreciated!
Unfortunately, looking deeper into my Micro Profiler, most of my lag is from defualt roblox physics and only about 4-6ms is from my computations.
Ive confirmed the Micro Profiler Physics report looks exactly the same in other physics based games
Any idea why roblox is such an un-performant platform?
looking at all other platforms, doing something as simple as what im doing would never cause any lag, but on roblox everything screams at the mere sight of physics and parts needing to interact with the world around them.
It seems like most of it is coming from aerodynamics, try disabling that to see if things improve. I don’t know why roblox is so unperformant, even reading and especially modifying properties of parts is quite slow. It can’t be the C/lua boundary for physics as physics are fully written in C++ I’m pretty sure
When I use LoadModule, it requires two parameters, self and then the modulescript? What does this mean? Fyi, Im calling the scheduler inside a module script to call another script that uses runservice heartbeat. Im inexperienced with parallel luau, sorry! (ps, this is in serverscriptservices)
self is syntactic sugar in lua when using the :
notation
function Table:Method()
print(self) --> The contents of table will be printed
end
Table:Method()
self is a hidden first argument in this case. We can see that by using .
notation instead
local Table = {}
function Table.Method(self)
print(self)
end
Table:Method() -- Table is passed implicitly when using : notation, it's the same as doing Table.Method(Table)
Table.Method(Table)
All you have to do is this, to use LoadModule (if the module script is a child of script). If you use the : notation, you don’t have to pass self. You can rely on the autocomplete to figure out what you have to pass to the functions. Every function uses the :
notation
To make a function run with the Parallel Scheduler, you need to have a module (the one you pass to LoadModule) return a function, like shown in this figure (at the bottom, where it says Module Script)
The script calling the ParallelScheduler can be a LocalScript, or a ServerScript, doesn’t matter
Great Module! Running into one issue with it, though. I’m getting this error:
Which is linked to this line right here
Based off some debugging I attempted, it seems that after the RemainingTasks hits 2 (red circle), something isn’t cleared and the script still assumes there are 2 tasks and tries to assign them (blue circle). The WorkParmeters for that WorkerId doesn’t exist though so it just errors.
This error will contine to popup for subsequent :Work() requests.
Everything still will work as intended though so I can kinda just ignore it.
Well that’s dumb lol. I never considered the case where there are more actors than tasks to run XD
This happens when you Schedule and run more tasks (for example 4), then less tasks (but lower than DEFAULT_MAX_WORKERS, ex. 2), 4 actors were created previously, but only 2 have tasks to run, causing the error
The fix is simply to return if there are no params (aka no tasks) for the actor. This is what was (implicitly) happening when you encountered the error, which explains why the module kept working as expected
There might be another, more performant, fix to prevent the excess actors from running in the first place, I might look into it some day
This is also why I didn’t bother to really test this fix thoroughly cuz I’m lazy, so let me know if there are other errors
The roblox Model and the Place have been updated with this fix
Alrighty, thanks for the fix! I’ll keep you updated
Awesome work man
Responding to your question in the other thread, the reason why this module did not work for me is the yielding. I’m likely using the module wrong, but when I’m trying to calculate cframe data for hundreds of voxels at a time, and I’m constantly scheduling work to be done through a loop, it ends up looking like this:
And for reference this is what it looks like without parallel scheduler:
Can you show the code that uses Parallel Scheduler?
Make sure you are scheduling all the tasks before running Work(), though it seems like it is yielding to the next frame (and not freezing), which is, odd…
(Could be that there is a maximum amount of parallel phases in a frame?)
Send the code that uses :ScheduleTask() and :Work(), as well as the function inside the module passed to :LoadModule()
my division algorithm script:
local OctreeDivision = {}
local Types = require(script.Parent:WaitForChild("Types"))
local Settings = require(script.Parent:WaitForChild("Settings"))
local Constructor = require(script.Parent:WaitForChild("PartConstructor"))
local Scheduler = require(script.ParallelScheduler)
local ModTable = Scheduler:LoadModule(script.ModuleScript)
local function partCanSubdivide(part : Part) --Checks if part is rectangular.
local Threshold = 1.5 -- How much of a difference there can be between the largest axis and the smallest axis
local largest = math.max(part.Size.X, part.Size.Y, part.Size.Z) --Largest Axis
local smallest = math.min(part.Size.X,part.Size.Y, part.Size.Z) -- Smallest Axis
if smallest == part.Size.X then
smallest = math.min(part.Size.Y, part.Size.Z)
elseif smallest == part.Size.Y then
smallest = math.min(part.Size.X, part.Size.Z)
elseif smallest == part.Size.Z then
smallest = math.min(part.Size.X, part.Size.Y)
end
return largest >= Threshold * smallest
--Returns true if part is rectangular.
--Part is rectangular if the largest axis is at least 1.5x bigger than the smallest axis
end
function IsBoxWithinPart(data:BasePart, part:Types.VoxelInfo)
local partPosition = part.CFrame.Position
local partSize = part.Size
local halfPartSize = partSize / 2
local dataMin = data.CFrame.Position - (data.Size / 2)
local dataMax = data.CFrame.Position + (data.Size / 2)
local partMin = partPosition - halfPartSize
local partMax = partPosition + halfPartSize
return dataMin.X <= partMax.X and dataMax.X >= partMin.X and
dataMin.Y <= partMax.Y and dataMax.Y >= partMin.Y and
dataMin.Z <= partMax.Z and dataMax.Z >= partMin.Z
end
local function CheckForNewVoxelsInHitbox(Voxels:Types.VoxelInfoTable, hitbox:BasePart) -- Vector3,Instance
local partsInHitbox:Types.VoxelInfoTable = {}
for i,v in Voxels do
if IsBoxWithinPart(hitbox,v) then
table.insert(partsInHitbox,v)
end
end
return partsInHitbox
end
local function CheckForNewVoxelsNotInHitbox(Voxels:Types.VoxelInfoTable, hitbox:BasePart) -- Vector3,Instance
local partsInHitbox:Types.VoxelInfoTable = {}
for i,v in Voxels do
if not IsBoxWithinPart(hitbox,v) then
table.insert(partsInHitbox,v)
end
end
return partsInHitbox
end
local function getLargestAxis(part : Part) --Returns Largest Axis of Part size
return math.max(part.Size.X, part.Size.Y, part.Size.Z)
end
local function CutPartinHalf(block : Types.VoxelInfo, TimeToReset : number) --Cuts part into two evenly shaped pieces.
local partTable:Types.VoxelInfoTable = {} --Table of parts to be returned
local bipolarVectorSet = {} --Offset on where to place halves
local X = block.Size.X
local Y = block.Size.Y
local Z = block.Size.Z
if getLargestAxis(block) == X then --Changes offset vectors depending on what the largest axis is.
X /= 2
bipolarVectorSet = {
Vector3.new(1,0,0),
Vector3.new(-1,0,0),
}
elseif getLargestAxis(block) == Y then
Y/=2
bipolarVectorSet = {
Vector3.new(0,1,0),
Vector3.new(0,-1,0),
}
elseif getLargestAxis(block) == Z then
Z/=2
bipolarVectorSet = {
Vector3.new(0,0,1),
Vector3.new(0,0,-1),
}
end
local halfSize = Vector3.new(X,Y,Z)
for _, offsetVector in pairs(bipolarVectorSet) do
ModTable:ScheduleWork(block.CFrame,halfSize,offsetVector)
local info:Types.VoxelInfo = {
Size = halfSize,
CFrame = ModTable:Work()[1],
Parent = block.Parent,
CanCollide = block.CanCollide,
Transparency = block.Transparency,
Material = block.Material,
Anchored = block.Anchored,
Color = block.Color,
AlreadyExistsInWorkspace = false,
AlreadyDivided = false,
ResetTime = TimeToReset,
OriginalPart = block.OriginalPart
}
print(info)
table.insert(partTable,info)
end
return partTable -- Returns a table containing the two halves
end
local function DivideBlock(voxInfo : Types.VoxelInfoTable, MinimumVoxelSize : number, TimeToReset : number, Hitbox:BasePart) --Divides part into evenly shaped cubes.
--MinimumvVoxelSize Parameter is used to describe the minimum possible size that the parts can be divided. To avoid confusion, this is not the size that the parts will be divided into, but rather the minimum allowed
--You CANNOT change the size of the resulting parts. They are dependent on the size of the original part.
--if Hitbox == nil then
-- return voxInfo
--end
local partTable:Types.VoxelInfoTable = {} -- Table of parts to be returned
local minimum = MinimumVoxelSize or Settings.DefaultMinimumVoxelSize
local inHitbox
local NotInHitbox
if Hitbox then
inHitbox = CheckForNewVoxelsInHitbox(voxInfo,Hitbox)
NotInHitbox = CheckForNewVoxelsNotInHitbox(voxInfo,Hitbox)
else
inHitbox = voxInfo
--NotInHitbox = voxInfo
end
for i,block in inHitbox do
if (block.Size.X > minimum or block.Size.Y > minimum or block.Size.Z > minimum) then
if partCanSubdivide(block) then --If part is rectangular then it is cut in half, otherwise it is divided into cubes.
partTable = CutPartinHalf(block,TimeToReset)
else
local Threshold = 1.5 -- How much of a difference there can be between the largest axis and the smallest axis
local largest = math.max(block.Size.X, block.Size.Y, block.Size.Z) --Largest Axis
local smallest = math.min(block.Size.X,block.Size.Y, block.Size.Z) -- Smallest Axis
if smallest == block.Size.Y and smallest * Threshold <= largest then
local bipolarVectorSet = {}
local X = block.Size.X
local Y = block.Size.Y
local Z = block.Size.Z
X /= 2
Z /= 2
bipolarVectorSet = { --Offset Vectors
Vector3.new(-1,0,1),
Vector3.new(1,0,-1),
Vector3.new(1,0,1),
Vector3.new(-1,0,-1),
}
local halfSize = Vector3.new(X,Y,Z)
for _, offsetVector in pairs(bipolarVectorSet) do
ModTable:ScheduleWork(block.CFrame,halfSize,offsetVector)
local info:Types.VoxelInfo = {
Size = halfSize,
CFrame = ModTable:Work()[1],
Parent = block.Parent,
CanCollide = block.CanCollide,
Transparency = block.Transparency,
Material = block.Material,
Anchored = block.Anchored,
Color = block.Color,
AlreadyExistsInWorkspace = false,
AlreadyDivided = false,
ResetTime = TimeToReset,
OriginalPart = block.OriginalPart
}
print(info)
table.insert(partTable,info)
end
elseif smallest == block.Size.X and smallest * Threshold <= largest then
local bipolarVectorSet = {}
local X = block.Size.X
local Y = block.Size.Y
local Z = block.Size.Z
Y /= 2
Z /= 2
bipolarVectorSet = { --Offset Vectors
Vector3.new(0,-1,1),
Vector3.new(0,1,1),
Vector3.new(0,-1,-1),
Vector3.new(0,1,-1),
}
local halfSize = Vector3.new(X,Y,Z)
for _, offsetVector in pairs(bipolarVectorSet) do
ModTable:ScheduleWork(block.CFrame,halfSize,offsetVector)
local info:Types.VoxelInfo = {
Size = halfSize,
CFrame = ModTable:Work()[1],
Parent = block.Parent,
CanCollide = block.CanCollide,
Transparency = block.Transparency,
Material = block.Material,
Anchored = block.Anchored,
Color = block.Color,
AlreadyExistsInWorkspace = false,
AlreadyDivided = false,
ResetTime = TimeToReset,
OriginalPart = block.OriginalPart
}
print(info)
table.insert(partTable,info)
end
elseif smallest == block.Size.Z and smallest * Threshold <= largest then
local bipolarVectorSet = {}
local X = block.Size.X
local Y = block.Size.Y
local Z = block.Size.Z
X /= 2
Y /= 2
bipolarVectorSet = { --Offset Vectors
Vector3.new(1,-1,0),
Vector3.new(1,1,0),
Vector3.new(-1,-1,0),
Vector3.new(-1,1,0),
}
local halfSize = Vector3.new(X,Y,Z)
for _, offsetVector in pairs(bipolarVectorSet) do
ModTable:ScheduleWork(block.CFrame,halfSize,offsetVector)
local info:Types.VoxelInfo = {
Size = halfSize,
CFrame = ModTable:Work()[1],
Parent = block.Parent,
CanCollide = block.CanCollide,
Transparency = block.Transparency,
Material = block.Material,
Anchored = block.Anchored,
Color = block.Color,
AlreadyExistsInWorkspace = false,
AlreadyDivided = false,
ResetTime = TimeToReset,
OriginalPart = block.OriginalPart
}
print(info)
table.insert(partTable,info)
end
else
local bipolarVectorSet = { --Offset Vectors
Vector3.new(1,1,1),
Vector3.new(1,1,-1),
Vector3.new(1,-1,1),
Vector3.new(1,-1,-1),
Vector3.new(-1,1,1),
Vector3.new(-1,1,-1),
Vector3.new(-1,-1,1),
Vector3.new(-1,-1,-1),
}
local halfSize = block.Size / 2.0
for _, offsetVector in pairs(bipolarVectorSet) do
ModTable:ScheduleWork(block.CFrame,halfSize,offsetVector)
local info:Types.VoxelInfo = {
Size = halfSize,
CFrame = ModTable:Work()[1],
Parent = block.Parent,
CanCollide = block.CanCollide,
Transparency = block.Transparency,
Material = block.Material,
Anchored = block.Anchored,
Color = block.Color,
AlreadyExistsInWorkspace = false,
AlreadyDivided = false,
ResetTime = TimeToReset,
OriginalPart = block.OriginalPart
}
print(info)
table.insert(partTable,info)
end
end
end
end
end
local check = false
for i, v in pairs(partTable) do
if math.floor(v.Size.X) > minimum or math.floor(v.Size.Y) > minimum or math.floor(v.Size.Z) > minimum then
check = true
end
end
if NotInHitbox then
for i,v in NotInHitbox do
table.insert(partTable,v)
end
end
if check == true then
for i,partInfo in pairs(partTable) do
if partInfo.AlreadyDivided then
table.remove(partTable,i)
end
end
return DivideBlock(partTable,MinimumVoxelSize,TimeToReset,Hitbox)
else
return partTable --Returns resulting parts
end
end
function OctreeDivision.DivideBlock(Parts:Types.PartTable,Minimum:number|string,timeToReset:number,Hitbox:BasePart):Types.VoxelInfoTable
local voxelTable:Types.VoxelInfoTable = {}
local minimum = Minimum or Settings.DefaultMinimumVoxelSize
for i,v in Parts do
if v:HasTag(Settings.TagName) then
v:RemoveTag(Settings.TagName)
--if Settings.UseCache then
-- Constructor:ReturnPart(v)
--else
-- v:Destroy()
--end
end
local temp:Types.VoxelInfo = {
Parent = v.Parent,
Size = v.Size,
CFrame = v.CFrame,
Material = v.Material,
CanCollide = v.CanCollide,
Transparency = v.Transparency,
Color = v.Color,
Anchored = v.Anchored,
AlreadyDivided = true,
AlreadyExistsInWorkspace = true,
ResetTime = timeToReset,
OriginalPart = v,
}
table.insert(voxelTable,temp)
end
return DivideBlock(voxelTable,minimum,timeToReset,Hitbox)
end
return OctreeDivision
And the function passed through LoadModule:
return function(blockCF,halfSize,offsetVector)
local cf = blockCF + blockCF:VectorToWorldSpace((halfSize / 2.0) * offsetVector)
print(cf)
return cf
end
I know it’s kind of hard to read lol
Not too bad when you paste it into studio :P
Here is the issue
You are calling :Work() right after Scheduling a task, meaning there is only a single task scheduled to run, and it is thus unable to assign tasks to multiple threads. You need to Schedule multiple tasks, before calling :Work()
I will also point out that the work you are doing (the CFrame calculation) is not very computationally intensive alone, and it’ll only become computationally intensive from the high number of voxels.
However, when doing a single voxel per task, this increases the amount of tasks substancially, and the overhead (especially with the arguments being sent, sending arguments to different parallel threads is costly).
I would suggests coding the function in the module to take a list of voxels, and return the calculated CFrame for those voxels (bonus point if you can reduce the amount of data being sent), and have the main script schedule a set amount of tasks (maybe like 24), and divide all the voxels evenly (or the closest to evenly) between those 24 tasks
However, it is important to measure the performance (either through the micro profiler, or maybe with os.clock(), the script performance tab is inaccurate) to figure out if you actually save up performance by doing this. Parallel lua on roblox has very limited use cases still, and often, using parallel lua takes up more time because of the overhead. It is also very possible that other segments of the code (such as moving parts, resizing them, etc) take up a significant amount of time while CFrame calculations aren’t very significant, and those cannot be ran in parallel or optimized
Anyway, here is an example on how you could implement it:
local halfSize = Vector3.new(X,Y,Z)
for _, offsetVector in pairs(bipolarVectorSet) do
ModTable:ScheduleWork(block.CFrame,halfSize,offsetVector)
end
local ResultTable = ModTable:Work()
for i, offsetVector in pairs(bipolarVectorSet) do
local info:Types.VoxelInfo = {
Size = halfSize,
CFrame = ResultTable[i],
Parent = block.Parent,
CanCollide = block.CanCollide,
Transparency = block.Transparency,
Material = block.Material,
Anchored = block.Anchored,
Color = block.Color,
AlreadyExistsInWorkspace = false,
AlreadyDivided = false,
ResetTime = TimeToReset,
OriginalPart = block.OriginalPart
}
print(info)
table.insert(partTable,info)
end
Here, the loop is ran twice, once to schedule all the tasks, and then to set up the tables when the work is done. The results are returned in the same order they were scheduled, so you can simply get them using the index of the for loop
(I also noticed that the loop only runs 2-8 times throughout your code, which is not a lot, and for each, only somewhat basic CFrame calculations are done. It is highly likely that the work is not computationally intensive enough to benefit from parallel lua)
Something else to note is that if you batch tasks together, you can send block.CFrame and halfSize only once for the 2-8 tasks, reducing the amount of data being sent. My example doesn’t include this though
Under the “Performance Tips” drop down, you can read more about performance when using this module or parallel lua in general
Hope this helps
I don’t know if I’m using the module incorrectly or what, but it doesn’t work for me.
I’m using your module for my ‘Global Illumination’ module, so it runs faster.
Code for main module:
-- Service(s)
local Lighting = game:GetService("Lighting")
local HTTPService = game:GetService("HttpService")
-- Variable(s)
local Classes = script.Classes
local Methods = script.Methods
local External = script.External
local ParallelFunctions = script.ParallelFunctions
local ParallelScheduler = require(External.ParallelScheduler)
-- Function(s)
local function InstanceChacker(instName, className, parent)
if parent:FindFirstChild(instName) then
return parent:FindFirstChild(instName)
else
local inst = Instance.new(className)
inst.Parent = parent
inst.Name = instName
return inst
end
end
-- Main
local globalIllum = {totalGrid = 0}
local PreviousStorage = nil
function globalIllum:GloballyIlluminate(baseBrightness: NumberSequence?, divisions: number?, color: ColorSequence?, castShadow: boolean?)
baseBrightness = baseBrightness or NumberSequence.new(1)
divisions = divisions or 60
castShadow = castShadow or false
color = color or ColorSequence.new(require(Methods.Color).Mix(Color3.new(1,1,1), Lighting.Ambient))
local GlobalLights = InstanceChacker("GlobalLights", "Folder", workspace)
local Storage = InstanceChacker(HTTPService:GenerateGUID(), "Folder", GlobalLights)
PreviousStorage = Storage
local workspaceSize = require(Methods.WorkspaceSize).Get()
local yOffset = workspaceSize.Y
local yOffsetRaycast = workspaceSize.Y / 8
local divisionSizeX = workspaceSize.X / divisions
local divisionSizeZ = workspaceSize.Z / divisions
local lightDirection = require(Methods.LightDirection).Get()
local Brightness = Lighting.Brightness
local totalParts = divisions^2
local totalTime = divisions - 1
require(Classes.GridPart).CastShadow = castShadow
local gridSize = math.max(divisionSizeX, divisionSizeZ) / 2
local ModuleScript = ParallelScheduler:LoadModule(ParallelFunctions.IllluminationLoop)
--coroutine.wrap(function()
-- for i = 0, divisions - 1 do
-- coroutine.wrap(function()
-- for j = 0, divisions - 1 do
-- local interpFactor = (i + j) / (2 * totalTime)
-- local brightness = require(Methods.Number).Interpolate(baseBrightness, interpFactor, totalParts)
-- local gridColor = require(Methods.Color).Interpolate(color, interpFactor)
-- require(Classes.GridPart).Brightness = brightness
-- require(Classes.GridPart).BaseColor = gridColor
-- require(Classes.GridPart).new(i, j, gridColor, castShadow, brightness, workspaceSize, Vector3.new(divisionSizeX, 0, divisionSizeZ), yOffset, gridSize, lightDirection, yOffsetRaycast, Storage)
-- end
-- end)()
-- end
--end)()
ModuleScript:ScheduleWork(divisions, totalTime, totalParts, baseBrightness, color, castShadow, workspaceSize, divisionSizeX, divisionSizeZ, yOffset, gridSize, lightDirection, yOffsetRaycast, PreviousStorage)
ModuleScript:Work()
globalIllum.totalGrid = totalParts
end
function globalIllum:ManipulateGrid(gridX: number, gridY: number, color: Color3?, brightness: number?, castShadow: boolean?)
local Grid = PreviousStorage:FindFirstChild(tostring("X_" .. gridX .. "-Y_" .. gridY))
if Grid then
if color or brightness or castShadow then
Grid.PointLight.Color = color or Grid.PointLight.Color
Grid.PointLight.Brightness = brightness or Grid.PointLight.Brightness
Grid.PointLight.Shadows = castShadow or Grid.PointLight.Shadows
end
else
warn("Invalid grid position.")
end
end
return globalIllum
Loop module:
return function(divisions, totalTime, totalParts, baseBrightness, color, castShadow, workspaceSize, divisionSizeX, divisionSizeY, yOffset, gridSize, lightDirection, yOffsetRaycast, Storage, TaskIndex)
local Methods = script.Parent.Parent.Methods
local Classes = script.Parent.Parent.Classes
for i = 0, divisions - 1 do
for j = 0, divisions - 1 do
local interpFactor = (i + j) / (2 * totalTime)
local brightness = require(Methods.Number).Interpolate(baseBrightness, interpFactor, totalParts)
local gridColor = require(Methods.Color).Interpolate(color, interpFactor)
require(Classes.GridPart).Brightness = brightness
require(Classes.GridPart).BaseColor = gridColor
require(Classes.GridPart).new(i, j, gridColor, castShadow, brightness, workspaceSize, Vector3.new(divisionSizeX, 0, divisionSizeZ), yOffset, gridSize, lightDirection, yOffsetRaycast, Storage)
end
end
return
end
Error in question: