[OOP] Property of class returning nil

Hello, I am attempting to incorporate OOP into my new game. While I am familiar with how OOP works in actual OOP-languages, I am not entirely familiar with how they work with meta tables, and I am getting some errors in the process. Below are the two module scripts, a regular script, and the output.

The concept: a SmallBoy is an class that belongs to the class Boat.

Boat Module Script
local Boat = {}
Boat.__index = Boat

function Boat.new(obj, config)
	
	return setmetatable({
			
		--Organization
		Obj = obj, --The actual instance of the boat (parent-most model).
		Name = config.Name, --USS_Boat_Name or CHINA_Boat_Name
		Faction = config.Faction, --USN/Hostile
		Unit = config.Unit, --CSGI/CSGIII/Hostile
		Alive = true, --True/False
		
		--Location
		Spawned = true, --By default, the boat has not spawned, but for testing it has.
		InPort = true, --By default, the boat will be set to nil, but for testing it is in port.
		
		--Statistics
		Kills = 0, --The number of boats sank by this boat.
		
		--Damage Specifics
		MaxHealth = config.MaxHealth, --Max HP of the boat.
		CurrentHealth = config.MaxHealth, --Current HP of the boat, just spawned, so maxHealth.
		Repairing = false, --Is the boat currently under repairs?
		
		--Propulsion Specifics
		EngineCount = config.EngineCount, --Number, how many engines.
		PropCount = config.PropCount, --Number, how many propellers.
		MaxSpeed = config.MaxSpeed, --Max speed the boat will go.
		CurrentSpeed = 0, --Boat is not moving when created.
		Acceleration = config.Acceleration, --How fast can the boat speed/stop?
		TurnSpeed = config.TurnSpeed, --How fast can the boat turn?
			
	}, Boat)
	
end

--Propulsion
function Boat:Start()
	print(self.Name, " starting up.")
end

--Damage Control
function Boat:Damage(hp)
	local newHp = self.CurrentHealth - hp
	
	if newHp < 0 then
		newHp = 0
		self.Alive = false
	else
		self.CurrentHealth = newHp
	end
	
	print(newHp)
end

function Boat:Repair(hp)
	local newHp = self.CurrentHealth + hp
	
	if newHp <= self.MaxHealth then
		self.CurrentHealth = newHp
	end
end

return Boat
SmallBoy Module Script
local ServerScriptService = game:GetService("ServerScriptService")
local Classes = ServerScriptService["Classes"]
local Boat = require(Classes["Boat"])

local SmallBoy = {}
SmallBoy.__index = SmallBoy
setmetatable(SmallBoy,{__index = Boat})

function SmallBoy.new(obj, config)	
	return setmetatable({
		
		self = Boat.new(obj, config)
		
	}, SmallBoy)
end

return SmallBoy
Initialize Script within a Boat
local ServerScriptService = game:GetService("ServerScriptService")

local Classes = ServerScriptService["Classes"]

local SmallBoy = require(Classes["SmallBoy"])

SmallBoy.new(script.Parent, require(script.Parent.Configuration))

SmallBoy:Start()

SmallBoy:Damage(5)
Config Module
return {

	["Name"] = "USS_SmallBoy",
	["Faction"] = "USN",
	["Unit"] = "CSGI",
	["MaxHealth"] = 1000,
	["EngineCount"] = 1,
	["PropCount"] = 2,
	["MaxSpeed"] = 70,
	["Acceleration"] = 0.5,
	["TurnSpeed"] = 0.02999999999999999889,
	
}
Output (with errors):
nil starting up.

  [21:39:00.309 - ServerScriptService.Classes.Boat:46: attempt to perform arithmetic (sub) on nil and number](rbxopenscript://www.dummy.com/dummy?scriptGuid=%7BA98737B7%2D8A42%2D4517%2D83CB%2DE85C5FE9E26C%7D&gst=3#46)

21:39:00.309 - Stack Begin

[21:39:00.310 - Script 'ServerScriptService.Classes.Boat', Line 46 - function Damage](rbxopenscript://www.dummy.com/dummy?scriptGuid=%7BA98737B7%2D8A42%2D4517%2D83CB%2DE85C5FE9E26C%7D&gst=3#46)

[21:39:00.310 - Script 'Workspace.USS_SmallBoy.Initilize', Line 8](rbxopenscript://www.dummy.com/dummy?scriptGuid=%7B17B4018E%2DF437%2D4FB0%2DAB5C%2D9A512F97A60E%7D&gst=3#8)

21:39:00.311 - Stack End

I have looked at many different videos, and I cannot figure out why the properties are not being set, or at least not being retrieved when using self. When I print out the inputs in the Boat Module Script, they are correct, so I know this is an issue with either storing or retrieving.

Any guidence would help! :smiley:

3 Likes

It would help if you could provide us with the “Config” module.

Edited with the Config module added.

1 Like

As I thought, it doesn’t look like you have CurrentHealth defined in the Config module and that is the reason it is returning as nil.

1 Like

As defined in the Boat Module, CurrentHealth is defined the same as MaxHealth.

--Damage Specifics
MaxHealth = config.MaxHealth, --Max HP of the boat.
CurrentHealth = config.MaxHealth,

Please note the first line, nil is starting. Even the name is not being found, yet when I print config.Name, I get a valid string.

2 Likes

It might be this part where you defined the boat’s constructor as self. See what happens if you remove self and don’t define it as a variable—just leave it as Boat.new(obj, config)

1 Like

Nothing changed on the output.

2 Likes

In that little sample, have you tried taking out the table part that Boat.new is sitting in?
a.k.a:

function SmallBoy.new(obj, config)	
	return setmetatable( Boat.new(obj, config), SmallBoy )
end
Edit: The biggest problem with this way of doing it is that you will have to assign fields to the table after constructing it:
local SomeOtherBoat = {}
SomeOtherBoat.__index = SomeOtherBoat
setmetatable(SomeOtherBoat, Boat)

function SomeOtherBoat.new(obj, config)
	local self = Boat.new(obj, config)

	-- this part right here
	self.NewProperty = "a new property"

	return setmetatable(self, SomeOtherBoat)
end

return SomeOtherBoat

By the way, I’m pretty sure you can simplify the part where you inherit SmallBoy from Boat to something simpler:

-- from
setmetatable(SmallBoy,{__index = Boat})

-- to
setmetatable(SmallBoy, Boat)
2 Likes

I made the changes you listed, and nothing changed. I also need SmallBoy to have additional properties, so I still need to be able to add data to the table.

2 Likes
  function SmallBoy.new(obj, config)	
      local NewBoy = Boat.new(obj, config)
      —// If you wanted to add new properties.
      NewBoy.Example = true
      return NewBoy
  end

I am somewhat sure you can add more properties to an object like this.

2 Likes

I just answered the second part in an edit, (already answered by @Alphexus though) :

You can also set the metatable beforehand if you want to call methods first:

function SomeOtherBoat.new(obj, config)
	local self = setmetatable(Boat.new(obj, config), Boat)
	
	self.NewProperty = "a new property"
	self:Start()
	
	return self
end

And you can also add new methods to your class that can override the superclass’ methods

function SpeedyBoat.new(obj, config)
	-- etc
end

-- new method
function SpeedyBoat:UseNitro(howMuch)
	-- example
end

-- override supermethod, take double damage
function SpeedyBoat:Damage(num)
	Boat.Damage(self, num * 2)
end

As for the first part of your problem, I don’t know what could be wrong, I just tested the method/supermethod theory in a script myself and it worked fine.

Edit: code I used for ^
local SuperClass = {}
SuperClass.__index = SuperClass

function SuperClass.new()
	local self = {
		SuperProp = 3
	}
	
	return setmetatable(self, SuperClass)
end

function SuperClass:SuperMethod()
	return "super method"
end

local Class = {}
Class.__index = Class
setmetatable(Class, SuperClass)

function Class.new()
	local self = SuperClass.new()
	
	self.Prop = 2
	
	return setmetatable(self, Class)
end

function Class:SuperMethod()
	return "overridden method"
end

function Class:Method()
	return "method"
end

local obj = Class.new()
print(obj.Prop) --> 2
print(obj.SuperProp) --> 3
print(obj:Method()) --> method
print(obj:SuperMethod()) --> overridden method

local superObj = SuperClass.new()
print(superObj:SuperMethod()) --> super method

Could you try printing self[1] and self.self in your Boat:Start method to see if there is still a nested table being used in the SmallBoy.new method?

Edit: You can even type out table.foreach(self, print) to print all the properties the table has.

2 Likes
Boat:Start()
function Boat:Start()

print(self[1])

print(self.self)

table.foreach(self, print)

print(self.Name, " starting up.")

end
New SmallBoy.new()
function SmallBoy.new(obj, config)	
	local self = setmetatable(Boat.new(obj, config), Boat)
	
	self.NewProperty = "a new property"
	self:Start()
	
	return self
end
Output

nil
nil
PropCount 2
Unit CSGI
TurnSpeed 0.03
Kills 0
Alive true
EngineCount 1
Spawned true
Obj USS_SmallBoy
NewProperty a new property
Acceleration 0.5
CurrentHealth 1000
CurrentSpeed 0
Name USS_SmallBoy
Faction USN
MaxSpeed 70
Repairing false
MaxHealth 1000
InPort true
USS_SmallBoy starting up.
nil
nil
__index table: 0x278c3f49c4bdf4e3
new function: 0xe4b8c114319bc193
nil starting up.

2 Likes

Ok, I figured it out. You have to make the Initialize Script store the value from SmallBoy.new in a variable, and use that to call the methods.
i.e:

-- the class
local SmallBoy = require(Class["SmallBoy"])

-- the new object, instantiated by the class
local smallBoyObject = SmallBoy.new(script.Parent, require(script.Parent.Configuration))

-- calling object methods
smallBoyObject:Start()

smallBoyObject:Damage(5)

hhhhhhhh

On a side note, you might want to rename the obj argument in your methods to something like instance so that you don’t get confused.

3 Likes

I got it to work! I do have another question:

function SmallBoy.new(obj, config)	
	local self = Boat.new(obj, config)

	-- this part right here
	self.NewProperty = "a new property"

	return setmetatable(self, SmallBoy)
end

Can the above be converted to a notation such as:

function Boat.new(obj, config)
	
	return setmetatable({
			
		--Organization
		Obj = obj, --The actual instance of the boat (parent-most model).
		Name = config.Name,

I just think it looks better to keep them the same, but if not I guess I will have to live with it lol.

1 Like

You may encounter some difficulty trying to convert a subclass inherting from a superclass to that same kind of notation, primarily because the superclass version returns a new table while the subclass one is getting a table from another constructor. It gets weird and muddled when you lose track of what you’re doing or what the purpose of something is.

You could construct a new Boat for the subclass to use, wrap that and set properties accordingly, but that would look horrid. It’s for this reason that you should stick to a consistent structure that you’re able to use for both the super and subs rather than using different styles. It helps for readability as well when your conventions are consistent between your super and subs. Where the super constructs a new table, the sub gets the object table from the super.

-- Sub constructor
function SmallBoy.new(obj, config)
    local self = Boat.new(obj, config)

    self.NewProperty = "a new property"

    return setmetatable(self, SmallBoy)
end

-- Super constructor
function Boat.new(obj, config)
    local self = {}

    self.obj = obj,
    -- etc.

    return setmetatable(self, Boat)
end
3 Likes