OOP Inventory System

Let us talk about OOP usage in a Player Inventory. If you do not already know the four fundamental principles of Object Oriented Programing, please look at my article explaining OOP in detail. Also note, if you do not have the experience to grasp these core concepts as of this moment, you can always come back with a deeper understanding of Lua. This tutorial is built using Advanced Lua concepts; it is understandable if you need to learn more material before attempting.

However, if you already understand these critical fundamental principles and how OOP/Metatables work, let us continue.

Let us start with the basic system that we will be making. The key components that are needed include:

  • Saving and Loading Data
  • Dealing with Adding and Removing Items
  • Verifying the integrity of Items inside of Inventory

With these goals, we can now move on to the core code to make this system run.

To start, I will be using three modules, as seen in the image listed.

image

The loader will be inheriting the methods from the Classes folder. This will allow us to have clean and efficient code. Please note when doing this type of scripting, you may run into C-Stack errors if you attempt to index a table that is indexing itself.

For example:

local Table = {}
Table.__index = function(T, Index)
return T[Index]
end

This snippet of code will result in a C-Stack error due to repeated indexes to the table. To offset this, you can make a copy of the table.

As seen here:

local Table = {}
local Table_Copy = {}
Table.__index = function(T, Index)
return Table_Copy [Index]
end

However, I would not suggest doing this as it is overcomplicated and messy; Using Table.__index = function() is only useful for inheritance purposes.

The loader module that will be using inheritance will have to be set up so that we don’t have to require each model in our script. Instead, we can require all of the classes through the loader and return the inherited objects directly to the script controlling the system.

To achieve this functionality, we must do the following steps:

--[[
Sam
2/16/2021
Loads classes for use
]]

local Loader = newproxy(true)
local LoaderMT = getmetatable(Loader)
LoaderMT.__index = LoaderMT

local Classes = script.Enums:GetChildren()

for Index, Value in next, Classes do
	local Name = Value.Name
	local EnumData = require(Value)
	LoaderMT[Name] = EnumData
end


setmetatable(LoaderMT, {__index = {}})
return Loader

This will allow us to simply do the following code to require any class in our system.

local Loader = require(game.ServerStorage.InvSystem.Loader)
print(Loader.DataClass) -- {}, we found our class.

With this, we can now set up our important methods for each class.

Our DataClass will contain all the methods we will need to deal with saving and loading data. It will look like this:

--[[
Sam
2/16/2021
Deals with data saving and loading for player Inv
]]

local DataClass = {}

local DS = game:GetService("DataStoreService")
local Store = DS:GetDataStore("Store")

DataClass.__index = DataClass -- we dont use .__index = function here due to it being messy.


function DataClass.New(Player)
	return setmetatable({
	Player = Player;
	Data = {};	
	}, DataClass)
end

function DataClass:LoadData() -- make load data method
	local Data = Store:GetAsync(self.Player.UserId, {}) -- attempt to load data from store
	print(Data)
	if not Data then -- Set data inside private method
		self.Data = {
			["Sword"] = 1;
			["Gold"] = 500;
		}
	
	else
		self.Data = Data -- we set the data inside of the private method
	end
end


function DataClass:SaveData() -- make save data method
	Store:UpdateAsync(self.Player.UserId, function(Old) -- we save data
		return self.Data
	end)
end

return DataClass

You can save data however you want; I used this method to save time.

We can also create or InvClass; This class will deal with all Inv methods, adding items, removing items, and verifying items exist. This class will also inherit functions from the DataClass. This will look like this:

--[[
Sam
2/16/2021
Deals with inv functionality
]]

local InvClass = {}
InvClass.__index = InvClass 

function InvClass.New(Player, DataClass) -- constructor function.
	local MT = setmetatable({}, InvClass)
	DataClass:LoadData()
	setmetatable(MT, {__index = DataClass}) -- This will wrap the new MT we created.
--[[
Because we wrapped the new MT with a new MT we setup an index call to check MT and DataClass for functions. 
	return MT
end

function InvClass:AddItem(Item,Name, Amount) -- make the add item method
	if not Item or Name then return end
	
	if self.Data[Item] then 
		self.Data[Item] += Amount or 1
	else
		self.Data[Item] = Amount or 1
	end	
end

function InvClass:RemoveItem(Item,Name,Amount) -- remove item method
	if not Item or Name then return end
	if self.Data[Item] then 
		self.Data[Item] -= Amount or 1
	end	
end


return InvClass

With all of these main classes done we can now set up the script that will put them all together.

This script will look something like this:

--[[
Sam
2/16/2021
Deals directly with Inv system
]]


local Loader = require(game.ServerStorage.InvSystem.Loader)


shared.PlayerList = {}



game.Players.PlayerAdded:Connect(function(Player)
	shared.PlayerList[Player] = Loader.InvClass.New(Player,  Loader.DataClass.New(Player)) -- pass the player as well as the Data_Class through.
end)
game.Players.PlayerRemoving:Connect(function(Player)
shared.PlayerList[Player]:SaveData() -- we can call the save data function from the InvClass due to inheriting the Data class properties.
end)

This is our complete system, You can save data, load data, add items, and remove items. You can also add your own custom methods to alter functionality. But be aware if you do so you will need to use the “:” key instead of the default “.”. The key “:” is used to refer to self or the object that the method is being called from.Preformatted text

54 Likes

If there are any concerns regarding this topic or thread please let me know.

4 Likes

Yeah, I’ve got a few,

  1. I started making an oop inventory system a long time ago, then I stopped and I forgot why until now when I’m trying to make it again. How do I access objects across scripts?
1 Like

You would need to transfer the data somehow. This data transfer can be done with bindable events or global tables such as shared or _G.

1 Like

So I was looking at this and trying to get it to work in my own environment.

I keep running into an issue where


is printed every single time I try to use the AddItem function.

I cannot find out the reason why. I copied the code word for word in that section. (purely because I couldn’t figure out what was wrong)

if I remove this line of code, it gives me a different error.

setmetatable(MT, {__index = DataClass})

image

at least with this error it is finding the function AddItem but it can’t find “Cash” (this is the default data item I used in the DataClass) but I’m guessing that is because the DataClass was never inherited.

Any Ideas on how to fix this would be super helpful!

1 Like

Sorry for the late reply!

It appears that add item is not being recognized inside of the inventory class. This is likely caused by not using the __index metamethod when instantiating the object.

local InvClass = {}
InvClass.__index = InvClass 

I don’t get the benefits of OOP isn’t it just a more complicated version of coding?

OOP is a paradigm that prides itself on the reusability and extendability of code. It’s one of the less complex versions of coding once the foundation is understood. Nonetheless, it can be hard to understand its implementation if one has not studied the subject or implemented the paradigm in code.

For a concrete example without the complex additives of theoretical data and not using any abstract classes/methods here is an example of its usage.

Let’s say we have a player. This player holds data for the user which was retrieved from our database as well as their current session data like current health, stats, state, ect… We could represent this relationship as a stand-alone table with functions that act on the data which they are given rather than the player itself, this is called data-oriented programming or we can create a player object for each player. This allows us to add functions to the players in the game to manipulate and aggregate the given player datasets to generate the expected output. For example, I can add a method(functions are methods in oop) to the player object called spawn_character. This method will spawn the player. This method will also be usable for every player object in the entire game with the class definition. Now, why do we care about this? Well, let’s say we wrote an algorithm to spawn the player at a specific location and we want to update this algorithm. We could pass all the information into a regular function that is not a part of any class, or we can simply update the method part of the class. This will update the defined logic for every object with the signature of that method. This concept is actually, the reason we use OOP in the enterprise space.

1 Like

Ah okay I get it now. Thanks for the brief I allways thought it was a harder way of doing things for a challenge.

2 Likes