Hi! With the full release of Parallel Lua, I decided to convert a Unit/NPC implementation to using parallel lua. I wanted to get some feedback to ensure that I am using this feature correctly and efficiently in my implementation.
The original implementation had a single client sided ModuleScript class act as the unit object. I would call unit:Start(), unit:Stop(), unit:UpdateVisual(), ect… in order to replicate it’s position on the server.
![]()
My new implementation with parallel lua made me throw a bit of the design out of the window as I could not connect any parallel threads without actor instances. It now looks something like this:

The UnitReplicator receives updates from the server that a unit has been created and that a client sided ones now needs to be made. It clones the UnitActor instance and calls Updater:Load(...) to set up the unit/attributes with the correct properties. The UnitActor instance has attributes attached to it to allow me to edit properties on without some sort of scope issue due to the actor.
Let me know if more info is needed to understand what I’m doing. Here are relevant snippets from my implementation:
“UnitActor” attributes

Here is the relevant “UnitReplicator” code:
UnitReplication.OnClientEvent:Connect(function(action, args)
if action == "update" then
local unit = UnitReplicator.Units[args.id]
if not unit then
local newUnitActor = UnitActor:Clone()
newUnitActor.Parent = Actors
local unitUpdater = require(newUnitActor.Updater)
unitUpdater:Load(args)
UnitReplicator.Units[args.id] = {
actor = newUnitActor,
updater = unitUpdater
}
unit = UnitReplicator.Units[args.id]
end
unit.actor:SetAttribute("Active", true)
elseif ... then
...
end
Here is the “Updater” module code:
if script.Parent.Parent:IsA("ModuleScript") then return end
local Actor = script.Parent
local UnitObject = require(Actor.Parent.Parent.Unit)
local activeSignal = Actor:GetAttributeChangedSignal("Active")
local visibleSignal = Actor:GetAttributeChangedSignal("Visible")
local healthSignal = Actor:GetAttributeChangedSignal("Health")
local startTimeSignal = Actor:GetAttributeChangedSignal("StartTime")
local pauseTimeSignal = Actor:GetAttributeChangedSignal("PauseTime")
local Updater = {}
function Updater:Load(unitProperties)
Actor.Name = unitProperties.id
Actor:SetAttribute("StartTime", unitProperties.startTime)
Actor:SetAttribute("MaxHealth", 100)
Actor:SetAttribute("Health", Actor:GetAttribute("MaxHealth"))
self.unit = UnitObject.new(Actor, unitProperties)
self.active = false
self.visible = true
self.replicateConnection = nil
end
function Updater:Destroy()
if self.replicateConnection then self.replicateConnection:Disconnect() end
self.unit:Destroy()
end
function Updater:ActiveChanged()
self.active = Actor:GetAttribute("Active")
if self.replicateConnection then self.replicateConnection:Disconnect() end
if not self.active then return end
while self.active and self.visible do
task.synchronize()
self.unit:UpdateVisual()
task.desynchronize()
task.wait()
end
end
function Updater:PauseTimeChanged()
self.unit.pauseTime = Actor:GetAttribute("PauseTime")
end
function Updater:HealthChanged()
task.synchronize()
self.unit:SetHealth(Actor:GetAttribute("Health"))
task.desynchronize()
end
activeSignal:ConnectParallel(function()
Updater:ActiveChanged()
end)
pauseTimeSignal:ConnectParallel(function()
Updater:PauseTimeChanged()
end)
healthSignal:ConnectParallel(function()
Updater:HealthChanged()
end)
return Updater
Here is the “Unit” code:
local RunService = game:GetService("RunService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local SharedObjects = ReplicatedStorage:WaitForChild("SharedObjects")
local Path = require(SharedObjects:WaitForChild("Path"))
local RepAssets = ReplicatedStorage:WaitForChild("Assets")
local EnemyAssets = RepAssets:WaitForChild("Enemies")
local BillboardAssets = RepAssets:WaitForChild("Billboards")
local function processUnitModel(model)
local humanoid = model:FindFirstChildWhichIsA("Humanoid")
if humanoid then
humanoid:SetStateEnabled(Enum.HumanoidStateType.FallingDown, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Running, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.RunningNoPhysics, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Climbing, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.StrafingNoPhysics, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Ragdoll, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.GettingUp, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Jumping, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Landed, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Flying, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Freefall, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Seated, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.PlatformStanding, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Dead, false)
humanoid:SetStateEnabled(Enum.HumanoidStateType.Swimming, false)
humanoid:ChangeState(Enum.HumanoidStateType.Physics)
end
for _,object in model:GetDescendants() do
if object:IsA("BasePart") then
object.CanCollide = false
object.CanTouch = false
object.CanQuery = false
end
end
end
local Unit = {}
Unit.__index = Unit
function Unit.new(actor, unitProperties)
local newUnit = {}
newUnit.actor = actor
newUnit.id = unitProperties.id
newUnit.path = Path.new(unitProperties.pathFolder)
newUnit.maxHealth = actor:GetAttribute("MaxHealth")
newUnit.health = actor:GetAttribute("Health")
newUnit.startTime = actor:GetAttribute("StartTime")
newUnit.pauseTime = actor:GetAttribute("PauseTime")
newUnit.model = EnemyAssets.Enemy:Clone()
newUnit.model.Parent = workspace.Enemies
processUnitModel(newUnit.model)
newUnit.healthBar = BillboardAssets["HealthBar"]:Clone()
newUnit.healthBar.Frame.NameFrame.Label.Text = "Enemy_"..math.random(1,500)
newUnit.healthBar.Parent = newUnit.model.PrimaryPart
setmetatable(newUnit, Unit)
return newUnit
end
function Unit:SetHealth(health)
if self.healthBar.Parent ~= nil then
self.health = math.max(0, health)
self.healthBar.Frame.HealthFrame.Container.Bar.Size = UDim2.fromScale(self.health / self.maxHealth, 1)
end
end
function Unit:UpdateVisual()
local cframe = self:GetCFrame()
self.model:PivotTo(cframe)
end
function Unit:Destroy()
self.model:Destroy()
end
function Unit:GetCFrame()
local travelTime = (workspace:GetServerTimeNow() - self.startTime) - self.pauseTime
local nextNode, currentPosition = self.path:GetLocationFromTime(travelTime, 5, self.currentNode)
self.currentNode = nextNode - 1
return CFrame.new(currentPosition, self.path.nodes[nextNode].cframe.Position)
end
function Unit:Hide()
self.model:PivotTo(CFrame.new(100000, 100000, 0))
end
return Unit
Edit:
I did some tests with the parallel version vs the unparallel version. Weirdly enough the unparallel version performs a lot better. Both get some moderate lag when spawning a unit every 0.01 seconds as you can see here:
Non-Parallel:
Parallel:
I also did an edited version of the parallel implementation that separated the unit’s cframe and PivotTo(…) functionality so I could do the cframe calculations in parallel without having to synchronize for the PivotTo(…):

However this was even worse performance wise. I am definitely doing something wrong here.

