Issues with custom classes

I’m working on my game, and I decided to write a custom (pseudo) class for my monsters, called Monster. It has a .new method, and several other methods relating to the functionality of the monster model.

In the constructor method, .new, I clone the monster model, assign it a unique EnemyId, and add it to the Workspace. I also add the new Monster to a table outside of the class called monsters. This keeps track of all the monsters added to the game.

Code:

table.insert(monsters, self);

I also have a method that’s outside the actual class that is used to lookup a monster object with its EnemyId.

function Monster.LookUp(enemyId: string): MonsterType
	print(monsters);
	for i,v in pairs(monsters) do
		if v.EnemyId == enemyId then
			return v;
		end
	end
end

I use this in hit detection to find the Monster object whose model was hit and use the Monster:Damage() method on it to handle what happens when a monster gets damaged. The thing is, this function always returns nil, and the table always prints to nil.

How should I fix this?

1 Like

Can you show how your class is set up (class table, metamethods, constructor)

1 Like

Here’s the code.

local Monster = {}
Monster.__index = Monster

function Monster.new(name: string, region: string)
	local self = setmetatable({}, Monster)
	
	self.Name = name
	self.EnemyId = nil;
	self.Health = settings.Gameplay.EnemyHP[self.Name]
	self.PlayerFoundEvent = Instance.new("BindableEvent")
	self.PlayerFound = self.PlayerFoundEvent.Event
	self.Model = nil
	
	local enemies = {}	
	
	for i,v in pairs(EnemyStorage:GetChildren()) do
		if v.Name == self.Name then
			self.Model = v:Clone();
			
			for i,v in pairs(Enemies:GetChildren()) do
				if v.Name == name then
					table.insert(enemies, v)
				end
			end

			local idStringVal = self.Model.EnemyId

			idStringVal.Value = `{self.Name}{(#enemies)+1}`
			
			self.EnemyId = idStringVal.Value
		end
	end
	
	local reg = settings.Gameplay.Regions.MonsterSpawns[region]
	
	self.Model:PivotTo(Utils:RandomPositionInReg3(reg))	

	self.Model.Parent = Enemies
	
	local human: Humanoid = self.Model:FindFirstChild("Enemy");
	
	table.insert(monsters, self);
	
	human.Died:Connect(function() self:Damage(100) end)
	
	coroutine.resume(coroutine.create(function()
		self:Wander();
	end))
	
	return self
end

In the monster table is there an __index metamethod pointing back at the monster table (Monster.__index = Monster), and in the monster table is there a damage method?

And in your original post, when you say the table always prints to nil, are you talking about when printing the monster table, it prints nil? Or do you mean Monster.Lookup returns nil? Or something else?

In the Monster.Lookup method, I tell it to print the monsters table so I can make sure the table is populated. Printing the monsters table returns nil, and the Lookup function returns nil also.

I assume this is happening because you require the module from 2 different scripts. Basically, 2 instances of the module are created, that have a different copy of monsters. A simple solution is to make monsters a global variable and mention it this way:

_G.monsters = _G.monsters or {} --if monsters doesn't exist, create it
local monsters = _G.monsters --just a reference

Also I assume the table is empty not nil, because if the function returns nil it means it can enter the for loop, else it would throw an error.

1 Like

Just to clarify, printing monsters it outputs nil, not an unpopulated table? Calling pairs with no arguments should give you an error saying argument 1 missing or nil.

I see in your code above that you aren’t ever declaring a monsters table also. But then again calling table.insert with no arguments should give you an error. Is that above? Can you try printing the monsters table directly after inserting self into it?

1 Like

Modules cache so the script’s environment stays the same across every require call

Well didn’t know that, last time I used modules I had issues similar to this due to modules acting weird each time they were required by a different script.

Also I think the issue is that on each require, the thing reruns, so the table gets its original/default value again, which is {}.

It doesn’t rerun. It only gets run on the first require call assuming it’s required from the same environment (from the command line, from server scripts, client scripts…)

image

1 Like

I’ll try this. Give me a second

My bad, it prints out an unpopulated table.

Thanks. If you print monsters right after appending self to it, does it output any new entries?

I switched all of the references to the local monsters variable to _G.monsters and it started working. Also, yes it did

1 Like

I’m going to offer another solution since the current solution to this is very unsatisfying.

Reckless use of _G is dangerous, and can get very unorganized when used so unsparingly. The best solution is to set up the table of monsters inside the monster class itself.

When simulating classes in Lua, you can use the dot syntax to simulate both your constructor and static methods (a lot of writing in Lua is about using conventions to convey intended usage in spite of the rather loose syntactic capabilities).

local monsters = {}

local Monster = {}
-- your monster class

-- I recommend providing ways to interact with the monster list rather than always using the list itself. Nevertheless:
function Monster.getList()
    return monsters
end

function Monster.find(identifier)
    return monsters[identifier]
end

This way, you can access your monsters list anywhere you’ve required the Monster module. Please refrain from using _G so recklessly. It gets very risky/dangerous the bigger your game gets.

Edit: Support on why you should limit using _G.

Very briefly, it makes program state unpredictable.

if a method in one of the objects triggers a side effect which changes the value of the shared global state, then you no longer know what the starting state is when you execute a method in the other object. You can now no longer predict what output you’ll get when you execute the method

Further, global state hurts the readability of your code. If your code has an external dependency that isn’t explicitly introduced into the code then whoever gets the job of maintaining your code will have to go looking for it to figure out where it came from.

3 Likes

Thanks for pointing this out, I’m not very familiar with OOP in Lua but this seems like a better solution than mine(btw it reminds me of the way objects are done in Java).

@FreakingHulk If the above reply is working, it should be marked a solution instead.

1 Like

Yep. Since Lua doesn’t offer native class functionality, we can’t exactly write a class like we normally would in Java, C#, C++, etc. There are quite a few ways to simulate this in Lua, but I think the easiest and most efficient way is to use metatables to allow for small, specific tables (your objects) to index/access information from big, general tables (your classes).

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.