Structuring an inventory system based on slots, similar to Minecraft

I’m attempting to create a 9x3 inventory system, based on slots.

The issue is that I can’t seem to figure out what way would be best to structure this, as going back and changing it later on may be an extreme hassle that I’d like to avoid if possible.

I’ve thought about using metatables, but I don’t truly understand them and they don’t seem like the right thing to do in this context. (They might be and I just don’t see it, correct me if I’m wrong!)

My current idea is to have a table on the server with the data about each of the slots and when the table is changed, the clients table who was changed is replicated to that client.

Is this the best way to go about it? Any help is appreciated.

14 Likes

Metatables won’t help all that much. Simplicity is better than cleverness, remember!

This is a good idea, and I believe that Minecraft does this also. The server manages everyone’s inventory, and clients keep a copy for themselves. When the server changes the contents of their inventory, the client is notified and their copy gets updated to the latest version. I have nothing to add :ok_hand:t2:

This is what I thought, until you actually start writing it out.

It would get very long very quickly because each player has 27 frames in their inventory + 9 on their hotbar.

Ontop of that, each frame has to be a table which stores the itemName, and the amount.

It just seems like there has to be a better way, hence why I made this post.

If it helps at all, here’s some sample code showing how you could do this. It’s not optimised in any way so be careful of performance issues - I built it to be easy to understand, not performant.

Server:

local playerInventories = {}
local inventorySize = 36 -- inventory of size 36 = 9 hotbar slots + 9*3 inventory slots

function ReplicatedStorage.GetInventoryCopy.OnServerInvoke(p)
    return playerInventories[p] -- return a copy of the calling player's inventory
end

local function onPlayerJoin(p)
     -- when a player joins, set up their inventory
    playerInventories[p] = {}
end

local function onPlayerLeave(p)
    -- when a player leaves, remove their inventory
    playerInventories[p] = nil
end

local function giveItem(p, item)
    -- put an item in the first free slot of a player's inventory
    -- returns true if successful, or false if the inventory is full
    local inventory = playerInventories[p]
    for i=1, inventorySize do -- iterate over every inventory slot in order
        if inventory[i] == nil then -- if this slot is free
            inventory[i] = item -- set slot to contain item
            ReplicatedStorage.ItemAddedToInventory:FireClient(p, item, i) -- notify client
            return true -- we finished
        end
    end
    return false -- we didn't find a free spot
end

Client:

-- get a copy of the inventory
local inventory = ReplicatedStorage.GetInventoryCopy:InvokeServer()

ReplicatedStorage.ItemAddedToInventory.OnClientEvent:Connect(function(item, slot)
    inventory[slot] = item -- keep our copy updated when an item is added
end)

This should give you an idea of how it may be implemented. Feel free to expand it to support removing items, stacking, metadata, whatever you like!

Hint: Minecraft doesn’t store items directly in these tables; Minecraft has these things internally called ItemStacks, which is basically just another mini table storing the type of item, how much of it is there, and any extra metadata.

12 Likes

While the OP doesn’t understand MetaTables (therefor it may be best he doesn’t use them) I would have to disagree with this. One of the main uses of a metatable is when you’re going to be indexing a class, in this case an Inventory should be unique to each user and if you want to make your life easy then you could set up a MetaTable like I did in this example.

local Inventory = {}
local InventoryClass = {}
local Inventories = {}

function InventoryClass:AddItem(Item, Amount)
	local itemName = Item.Name
	if not self.Contents[itemName] then
		self.Contents[itemName] = {
			Amount = Amount,
			Tool = Item
		}
	else
		self.Contents[itemName] = self.Contents[itemName].Amount + Amount
	end
end

function InventoryClass:GetContents()
	return self.Contents
end

function Inventory:GetInventory(Player)
	return Inventories[Player]
end

function Inventory.New(Player, Table)
	local InventoryMeta = setmetatable({
		Contents = Table,
		Ownership = Player,
	}, {
		__index = InventoryClass
	})
	Inventories[Player] = InventoryMeta
	return InventoryMeta
end

return Inventory

Anyways, that’s besides the point.

A good thing to keep in mind is that when making an inventory system you don’t need to worry about what slots items are in, as long as they’re in the inventory table.

Example: I have a table that contains two pieces of wood, that’s in my fourth slot, I iterate through the inventory table and check to see if my inventory contains two pieces of wood (this is to prevent exploiting) if it does then the user can use that to craft, etc…

Now if for some reason you truly need to store the slot, you could do this by using slot ids, as long as all of the slots don’t change names (like the image label) then you can store a value inside of your inventory table of each item.

With this is mind let’s get scripting something that doesn’t use MetaTables.

I would strongly recommend making this all in a ModuleScript.

We first need to define our Inventories, this will be a table that contains every single users inventory.

local inventoryHandler = {}
local inventories = {} -- This will be a dictionary with the player being the key

return inventoryHandler 

Now when a player joins there inventory will be empty, I’m not going to go over how to datastore an inventory as that’s a completely separate tutorial. We’ll simply be initializing a blank table when a player joins the game.

local inventoryHandler = {}
local inventories = {} -- This will be a dictionary with the player being the key

function inventoryHandler:CreateInventory(Player, Inventory) -- Inventory could be a datastored table, etc..
   if not Inventory then -- Checking to make sure that Inventory was defined
      Inventory = {} -- Inventory wasn't defined therefor we'll make a blank table
   end
   inventories[Player] = Inventory -- Setting up the player's inventory inside of our inventories table
   return inventories[Player] -- Returning the inventory incase it needs to be used later on
end

return inventoryHandler 

Awesome, now our inventory is created!
Let’s now add some items, it’s pretty easy! Keep in mind that this is where we will define things such as the slot an item is in (or any item data)

We will name our item’s data itemData. This is an example of what the table will look like.

{
   ["Name"] = "Stone",
   ["Amount"] = 15,
   ["Slot"] = 5,
}

Here’s a basic function we can use to add items.

function inventoryHandler:AddItem(Player, itemName, itemData)
   if not inventories[Player] then
      inventories[Player] = {}
   end
   local inventory = inventories[Player]
   if not inventory[itemName] then
      inventory[itemName] = itemData
   else
      inventory[itemName].Amount = inventory[itemName].Amount + itemData.Amount
   end
   return true
end

Removing items is the same as adding items just with a few changes, so I won’t be showing that.
Be sure to remove the players inventory table when they leave, etc…!

If you want to display the items now based on the slot then you can simply get the inventory (via invoke, etc…) and then display it based on the Slot value.

43 Likes

The use case of metatables you showed is indeed very common if you’re looking for an object oriented style. I use it a lot, but in general if you don’t understand it, don’t use it. That’s why simplicity is better than cleverness, because understanding your code is much more important than shaving seconds or being convenient. Abandoning simplicity for things you don’t understand is where bugs come from that you have no idea how to fix. KISS.

This will not work well with maximum stack sizes, or really anything to do with splitting items across slots. It’s not impossible, but again, KISS.

To not end on a negative, module scripts are awesome and more people should use them. It makes your code much more modular and re-usable!

2 Likes

He said that I shouldn’t use them, and you just reinstated that? Don’t see the point in that but thanks for the help!

4 Likes

It was more in response to his counter argument for “simplicity is better than cleverness” than anything :stuck_out_tongue:

3 Likes

On my experience with creating inventories. I tried both ways, and module scripts will be very good and clean when inventory is just grid layout with items. Without draggable slots, and etc (project delta like). For draggable slots better your way.