Grid Based Inventory Logic

Does anyone have an idea on how someone would go about making a grid based inventory? I’m not asking for any code or anything just some kind of vision here as to how to do it. I came up with a concept:
[lua]
function pl:SetInventoryItem(item,pos) //Basically I have it so the position should be a table based off of a {x axis, y axis}
self.Inventory[ItemToKey(item)] = pos --Item to key function is for my gamemode, it just returns the items key
end // When I think about it, it can’t work because if a player has multiple amounts of this item it won’t save them both because it is retrieved by it’s key

function pl:InitializeInventory()
self.Inventory[“SIZE”] = {8,8}
end
[/lua]

It’s probably right in front of my eyes but I’ve been working all day and my brain is tired haha, if anyone could give me some insight I’d appreciate it.

Are you looking for DIconLayout?

Pretty sure he means something like this:

Mmmmmmm, Path of Exile.

[img_thumb]http://indiefortress.files.wordpress.com/2012/01/sorting-inventory.png[/img_thumb]

are you thinking in menus? because i made one that utilizes the drag-n-drop system added in gmod 13

Ok, are you ready for this?
Alright.

So every item has a position in the backpack, and every item has a size in that backpack.
To start, you’ll need a table.
We’ll call it ply.Inv

[lua]
local ply = LocalPlayer()
ply.Inv = {}
[/lua]

ply.Inv will contain both the backpack AND the information about their inventory.

Let’s create a few subtables and values in ply.Inv
[lua]
ply.Inv.Backpack = {} --The actual backpack.
ply.Inv.Equipped = {} --This is the equipped items, assuming it’s going to be Diablo-Style.
ply.Inv.Weight = 0
[/lua]
(You can create whatever values you will need for your system. Weight might not be necessary, etc.)

Next we’re going to make the backpack table take the form of a 2-Dimensional table, with rows being the first key and each row containing columns. This would look something like:
[lua]
for i=1,8 do --8 being the width of the backpack.
ply.Inv.Backpack* = {}
end
for k,v in pairs(ply.Inv.Backpack)do
for i=1,4 do --4 being the height of the backpack.
ply.Inv.Backpack[k]* = false --False is a placeholder here. We’ll overwrite that later.
end
end
[/lua]
This will create an 8-wide, 4-tall 2D table.
So to get the item at coordinate 2,4 you would do ply.Inv.Backpack[2][4]
Simple enough.

But it’s always simpler with a convenience function:
[lua]
local plymeta = FindMetaTable(“Player”)
function plymeta:GetInvItem(x,y)
return self.Inv.Backpack[y]
end
[/lua]

Ok, now to explain the basis of the system.
The idea is this:
We’re going to create a new vgui element. This element will be a single square on the grid. With this we can have them shine and whatnot. When we finish designing this element we can replace the false’s we set earlier.
When we put an item in a place on the grid, we get the size of the item (width and height). We then tell all the squares in that area that there is now an item on them. We aren’t going to root the item to all the squares; just to the one which is in the top left of the item. The one which tells all the other squares. This will be the item’s parent square.
So let’s make these grid elements.

[lua]
local PANEL = {}

AccessorFunc(PANEL, “m_ItemPanel”, “ItemPanel”)

function PANEL:Init()
self.m_Coords = {x=0,y=0}
self:SetSize(30,30)
self:SetItem(false)
self:SetColor(Color(100,100,100))
self:SetItemPanel(false)

self:Receiver("invitem", function(pnl, item, drop, i, x, y) --Drag-drop functionality
if drop then
	item = item[1]
	local x1,y1 = pnl:GetPos()
	local x2,y2 = item:GetPos()
	if math.Dist(x1,y1,x2,y2) <= 30 then --Find the top left slot.
		if not pnl:GetItemPanel() then
			local itm = item:GetItem()
			local x,y = pnl:GetCoords()
			local itmw, itmh = itm:GetSize() --GetSize needs to be a function in your items system.
			local full = false
			for i1=x, (x+itmw)-1 do
				if full then break end
				for i2=y, (y+itmh)-1 do
					if LocalPlayer():GetInvItem(i1,i2):GetItemPanel() then --check if the panels in the area are full.
						full = true
						break
					end
				end
			end
			if not full then --If none of them are full then
				for i1=x, (x+itmw)-1 do
					for i2=y, (y+itmh)-1 do
						LocalPlayer():GetInvItem(i1,i2):SetItemPanel(item) -- Tell all the things below it that they are now full of this item.
					end
				end
				item:SetRoot(pnl) --like a parent, but not a parent.
				item:SetPos(pnl:GetPos()) --move the item.
			end
		end
	end
else
	--Something about coloring of hovered slots.
end

end, {})
end

function PANEL:SetCoords(x,y)
self.m_Coords = x
self.m_Coords[y] = y
end

function PANEL:GetCoords()
return self.m_Coords, self.m_Coords[y]
end

local col
function PANEL:Paint(w,h)
draw.NoTexture()
col = self:GetColor()
surface.SetDrawColor(col.r,col.g,col.b,255)
surface.DrawRect(0,0,w-2,h-2) --main square
surface.SetDrawColor(220,220,220,255)
surface.DrawRect(w-2,0,h,2) --borders
surface.DrawRect(0,h-2,2,w) – ^
end
vgui.Register(“InvSlot”, PANEL, “DPanel”)
[/lua]

Done. Now we add the whole functionality.
We have to assume that the item system involves unique items, not just arbitrary class id’s, to identify each item.
The best way to do this would be to link each item with an entity index. This would work if the items are all entities at some point.
When you pick an item up it removes the entity version (saving its ent index) and when you drop it it creates a new entity of the same kind with the same info.
This is all just concept. I’m not about to write an item system too.

So we would start by creating a slot panel for each inventory slot.

[lua]
for k,v in pairs(ply.Inv.Backpack)do
for i=1,4 do --4 being the height of the backpack.
ply.Inv.Backpack[k]* = vgui.Create(“InvSlot”)
ply.Inv.Backpack[k]:SetPos(k30,i30) --The icon is 30x30.
ply.Inv.Backpack[k]
:SetCoords(k,i)
end
end
[/lua]
Icons. Icon’s everywhere.

Next let’s make another vgui element. This will represent an item in your backpack.

[lua]
local PANEL = {}

AccessorFunc(PANEL, “m_Item”, “Item”)
AccessorFunc(PANEL, “m_Root”, “Root”)

function PANEL:Init()
self:SetSize(30,30)
self:SetItem(false) --false means no item.
self:SetColor(Color(100,100,100))
self:SetDroppable(“invitem”)
end

function PANEL:PaintOver(w,h)
draw.NoTexture()
if self:GetItem() then
surface.SetMaterial(self:GetItem():GetIcon()) --Your items must have a :GetIcon function.
surface.DrawTexturedRect(0,0,w,h)
end
end

local col
function PANEL:Paint(w,h)
draw.NoTexture()
col = self:GetColor()
surface.SetDrawColor(col.r,col.g,col.b,180)
surface.DrawRect(0,0,w,h) --background square
end
vgui.Register(“InvItem”, PANEL, “DPanel”)
[/lua]

Ok, so now we have the actual items.
Remember we’re only discussing the basics of a backpack system. You can add more if you want later.
Next, let’s create a function which finds out whether there is room in the backpack for a given item.
If there is room it will return the panel to root to. If not then it will return false.
[lua]
function IsRoomFor(item) --note that item must be an actual item, not a InvItem panel.
for k,v in ipairs(LocalPlayer().Inv.Backpack) do
for k2, pnl in ipairs(LocalPlayer().Inv.Backpack[v])do
if not pnl:GetItemPanel() then
local x,y = pnl:GetCoords()
local itmw, itmh = item:GetSize() --GetSize needs to be a function in your items system.
local full = false
for i1=x, (x+itmw)-1 do
if full then break end
for i2=y, (y+itmh)-1 do
if LocalPlayer():GetInvItem(i1,i2):GetItemPanel() then --check if the panels in the area are full.
full = true
break
end
end
end
if full then
return pnl --If there’s room then return the open panel.
end
end
end
end
return false --if not, then return false.
end
[/lua]
Most of this code is copied from the drag-n-drop functionality from earlier.
Next, let’s make items get picked up.
[lua]
function PickupItem(item)
if not CLIENT then return end --clientside only, sorry. We’re working with panels here.
local place = IsRoomFor(item)
if place then

	local itm = vgui.Create("InvItem") --create a new item panel.
	itm:SetItem(item)
	itm:SetRoot(place)
	itm:SetPos(place:GetPos())
	
	local x,y = place:GetCoords()
	local itmw, itmh = item:GetSize() --GetSize needs to be a function in your items system.
	for i1=x, (x+itmw)-1 do
		for i2=y, (y+itmh)-1 do
			LocalPlayer():GetInvItem(i1,i2):SetItemPanel(itm) -- Tell all the things below it that they are now full of this item.
		end
	end
	
	return true --successfully picked item up.
	
else
	return false --no room.
end

end
[/lua]

I think that’s basically it.
If you have any other questions, PM me.
I hope you enjoyed this. I haven’t tested it myself yet.

THE COMPLETED CODE:
[lua]

local ply = LocalPlayer()
ply.Inv = {}
ply.Inv.Backpack = {} --The actual backpack.
ply.Inv.Equipped = {} --This is the equipped items, assuming it’s going to be Diablo-Style.
ply.Inv.Weight = 0

for i=1,8 do --8 being the width of the backpack.
ply.Inv.Backpack* = {}
end
for k,v in pairs(ply.Inv.Backpack)do
for i=1,4 do --4 being the height of the backpack.
ply.Inv.Backpack[k]* = false --False is a placeholder here. We’ll overwrite that later.
end
end

local plymeta = FindMetaTable(“Player”)
function plymeta:GetInvItem(x,y)
return self.Inv.Backpack[y]
end

local PANEL = {}

AccessorFunc(PANEL, “m_ItemPanel”, “ItemPanel”)

function PANEL:Init()
self.m_Coords = {x=0,y=0}
self:SetSize(30,30)
self:SetItem(false)
self:SetColor(Color(100,100,100))
self:SetItemPanel(false)

self:Receiver("invitem", function(pnl, item, drop, i, x, y) --Drag-drop functionality
if drop then
	item = item[1]
	local x1,y1 = pnl:GetPos()
	local x2,y2 = item:GetPos()
	if math.Dist(x1,y1,x2,y2) <= 30 then --Find the top left slot.
		if not pnl:GetItemPanel() then
			local itm = item:GetItem()
			local x,y = pnl:GetCoords()
			local itmw, itmh = itm:GetSize() --GetSize needs to be a function in your items system.
			local full = false
			for i1=x, (x+itmw)-1 do
				if full then break end
				for i2=y, (y+itmh)-1 do
					if LocalPlayer():GetInvItem(i1,i2):GetItemPanel() then --check if the panels in the area are full.
						full = true
						break
					end
				end
			end
			if not full then --If none of them are full then
				for i1=x, (x+itmw)-1 do
					for i2=y, (y+itmh)-1 do
						LocalPlayer():GetInvItem(i1,i2):SetItemPanel(item) -- Tell all the things below it that they are now full of this item.
					end
				end
				item:SetRoot(pnl) --like a parent, but not a parent.
				item:SetPos(pnl:GetPos()) --move the item.
			end
		end
	end
else
	--Something about coloring of hovered slots.
end

end, {})
end

function PANEL:SetCoords(x,y)
self.m_Coords = x
self.m_Coords[y] = y
end

function PANEL:GetCoords()
return self.m_Coords, self.m_Coords[y]
end

local col
function PANEL:Paint(w,h)
draw.NoTexture()
col = self:GetColor()
surface.SetDrawColor(col.r,col.g,col.b,255)
surface.DrawRect(0,0,w-2,h-2) --main square
surface.SetDrawColor(220,220,220,255)
surface.DrawRect(w-2,0,h,2) --borders
surface.DrawRect(0,h-2,2,w) – ^
end
vgui.Register(“InvSlot”, PANEL, “DPanel”)

for k,v in pairs(ply.Inv.Backpack)do
for i=1,4 do --4 being the height of the backpack.
ply.Inv.Backpack[k]* = vgui.Create(“InvSlot”)
ply.Inv.Backpack[k]:SetPos(k30,i30) --The icon is 30x30.
ply.Inv.Backpack[k]
:SetCoords(k,i)
end
end

PANEL = {}

AccessorFunc(PANEL, “m_Item”, “Item”)
AccessorFunc(PANEL, “m_Root”, “Root”)

function PANEL:Init()
self:SetSize(30,30)
self:SetItem(false) --false means no item.
self:SetColor(Color(100,100,100))
self:SetDroppable(“invitem”)
end

function PANEL:PaintOver(w,h)
draw.NoTexture()
if self:GetItem() then
surface.SetMaterial(self:GetItem():GetIcon()) --Your items must have a :GetIcon function.
surface.DrawTexturedRect(0,0,w,h)
end
end

local col
function PANEL:Paint(w,h)
draw.NoTexture()
col = self:GetColor()
surface.SetDrawColor(col.r,col.g,col.b,180)
surface.DrawRect(0,0,w,h) --background square
end
vgui.Register(“InvItem”, PANEL, “DPanel”)

function IsRoomFor(item) --note that item must be an actual item, not a InvItem panel.
for k,v in ipairs(LocalPlayer().Inv.Backpack) do
for k2, pnl in ipairs(LocalPlayer().Inv.Backpack[v])do
if not pnl:GetItemPanel() then
local x,y = pnl:GetCoords()
local itmw, itmh = item:GetSize() --GetSize needs to be a function in your items system.
local full = false
for i1=x, (x+itmw)-1 do
if full then break end
for i2=y, (y+itmh)-1 do
if LocalPlayer():GetInvItem(i1,i2):GetItemPanel() then --check if the panels in the area are full.
full = true
break
end
end
end
if full then
return pnl --If there’s room then return the open panel.
end
end
end
end
return false --if not, then return false.
end

function PickupItem(item)
if not CLIENT then return end --clientside only, sorry. We’re working with panels here.
local place = IsRoomFor(item)
if place then

	local itm = vgui.Create("InvItem") --create a new item panel.
	itm:SetItem(item)
	itm:SetRoot(place)
	itm:SetPos(place:GetPos())
	
	local x,y = place:GetCoords()
	local itmw, itmh = item:GetSize() --GetSize needs to be a function in your items system.
	for i1=x, (x+itmw)-1 do
		for i2=y, (y+itmh)-1 do
			LocalPlayer():GetInvItem(i1,i2):SetItemPanel(itm) -- Tell all the things below it that they are now full of this item.
		end
	end
	
	return true --successfully picked item up.
	
else
	return false --no room.
end

end
[/lua]

You skipped the fun part; networking.

Well I didn’t want to take all the fun from the OP.

Also I fucking hate networking.

So long as the inventory table keeps only net-safe data types (for instance, not functions), the entire table can be transferred to the client upon joining. The inventory data could potentially get rather large with complexity, especially that of nested inventories, so you will want to send this table to the client with something like this.

You don’t want to be rocketing this data back and forth all the time. The client should of course have no direct control over the inventory. Anything the client does should be sent to the server to verify the validity of, and if proven valid, be sent back as a delta update to the client. Something like ‘change the values of these cells to this’, instead of sending the entire inventory. If the client finds that the server’s instructions don’t make sense (having somehow gone out of sync), the client requests a full copy like the one they got on joining.

You might also consider not storing the inventory on the player object, but instead doing a more global table of all players. AllInventories[player’s steam ID] = blah; ply.Inventory = AllInventories[ply:SteamID()]. This will let whichever means of saving you use save the inventory of players even after they disconnect. You won’t be saving this to file every time the player makes a change or constantly, you want to save them periodically. If having a lot of players saving at once causes a hiccup, you could save players one at a time over a length of time 5/numberofplayers minutes, ensuring that every player gets saved every five minutes, removing that player from AllInventories after saving if the player no longer is present.

This would make a fantastic tutorial on the wiki.

All of this makes a lot of sense.
I was only working in the clientside. I never even considered networking issues.

I’ll add it to the wiki when I test it.

[EDITLINE]today[/EDITLINE]

Thanks a ton bobblehead, I really appreciate it!