How can I properly dispose of something (OOP)?

I’m trying to create a relatively simple particle system for a firework display, based on an object-oriented paradigm. The issue I’m having is trying to properly dispose of an object when it’s no longer needed.

I have three scripts:

  • Everything begins with a local script. Using Heartbeat, there’s a 10% chance I try to loosely create a firework display. This utilises the Firework module I have, which then in turn uses the Particle module.
local Firework = require(script.Firework)

local fireworks = {}

game:GetService("RunService").Heartbeat:Connect(function()
	
	if math.random() < 0.1 then
		table.insert(fireworks, Firework.new())
	end
	
	for i = 1, #fireworks do
		if fireworks[i] then
			fireworks[i].update()
			fireworks[i].show()
		else
			table.remove(fireworks, i)
		end
	end
	
end)
  • Here’s the Particle module, which Firework inherits (there’s no actual inheritance though, at least as far as I’m aware, I’m still coming to grabs with OOP). In brevity, it handles the creation, rendering and deletion of the actual particle instances. I’ve tried deleting the actual particle, looping through itself and setting values to nil, and then setting the entire class to nil after.
local Particle = {}
Particle.__index = Particle


function Particle.new(x, y)
	local self = {}
	
	self.pos = Vector2.new(x, y)
	self.vel = Vector2.new(0, math.random(-12, -8))
	self.acc = Vector2.new(0, 0)
	
	self.point = nil
	
	self.applyForce = function(force)
		self.acc = self.acc + force
	end
	
	self.update = function()
		self.vel = self.vel + self.acc
		self.pos = self.pos + self.vel
		self.acc = self.acc * 0
	end
	
	self.show = function()
		if not self.point then
			local p = Instance.new("Frame")
			p.Parent = script.Parent.Parent
			p.Size = UDim2.new(0, 5, 0, 5)
			p.BorderSizePixel = 0
			p.Position = UDim2.new(0, self.pos.x, 0, self.pos.y)
			
			self.point = p
		else
			self.point.Position = UDim2.new(0, self.pos.x, 0, self.pos.y)
		end
	end
	
	self.destroy = function()
		self.point:Destroy()
		for i, v in next, self do
			v = nil
		end
		self = nil
	end
	
	setmetatable(self, {__index = Particle})
	
	return self
end


return Particle
  • This is the Firework module, nothing spectacular, it just creates the particle.
local Firework = {}
Firework.__index = Firework

local Particle = require(script.Parent.Particle)


function Firework.new()
	local self = {}
	
	self.gravity = Vector2.new(0, 0.2)
	
	self.firework = Particle.new(math.random(script.Parent.Parent.AbsoluteSize.X), script.Parent.Parent.AbsoluteSize.Y)
	
	self.update = function()
		if self.firework then
			self.firework.applyForce(self.gravity)
			self.firework.update()
			
			if self.firework.vel.y >= 0 then
				self.firework.destroy()
			end
		else
			return
		end
	end
	
	self.show = function()
		self.firework.show()
	end
	
	setmetatable(self, {__index = Firework})
	
	return self
end


return Firework

I suspect that it might have something to do with the Firework object in the local script still being referenced, even when it doesn’t exist? I’ve looked a little at weak tables, and I’m not sure how to implement them or if it’ll even solve the issue.

You can see the particles accelerate upwards and then when it should (and it does, if it wasn’t for the error) fall back down with gravity, that’s when I delete it and get the issue.

I apologise in advance if I’m not doing this whole topic thing right, this is my first time.

1 Like

Probably neither a final nor nice solution, but worth a try:

  1. use pcall() function in all cases where error can occur regarding to nil object (at least you can do it to see if this is the only problem or there are some others)

  2. check the object with FindFirstChild() before you act, e.g. if self:FindFirstChild(“acc”) then …

  3. set a boolean variable just before you fire the :destroy() method, and you can check this bool value before you do anything with that object

If I wrapped it in a pcall to avoid the error, would that cause some kind of memory leak and progressively get worse relative to how many particles are being created? It’s trying to GC objects I no longer need anymore that’s getting to me.

To me it looks like your “stop condition” of when a firework instance is done, is never checked against.

The ‘if fireworks[i] then’ check will never result in a false condition, as you never change the value of the element in the array to a ‘non-true’ value.

Instead perhaps modify the Firework.update method, so it returns true when it is still active, and false when it is finished and can be deleted:

self.update = function()
	if self.firework then
		self.firework.applyForce(self.gravity)
		self.firework.update()
		
		if self.firework.vel.y < 0 then
			return true -- Still alive
		end
		
		self.firework.destroy()
		self.firework = nil -- Remember to "release / remove" the reference
	end
	return false -- Firework is finished, no need to update ever again
end

Then change the loop in your Heartbeat anonymous method, so it correctly checks what the Firework.update method returns:

	-- Start iterating from the back of the array, towards the front.
	-- Because we are also going to remove elements from the array, while iterating through it.
	for i = #fireworks, 1, -1 do
		local fireworkElement = fireworks[i]
		if fireworkElement.update() then
			fireworkElement.show()
		else
			-- Remove element from array.
			-- Note that this is why we need to iterate backwards through the array.
			table.remove(fireworks, i)
		end
	end
2 Likes