Revision Difference
Grid-Based_Inventory_System#527908
<cat>Dev.UI</cat>
## Prerequesites
* Lua knowledge.
* An item system. Each item must have two functions:
* ITEM:GetIcon() -- Must return a Material() object.
* ITEM:GetSize() -- Must return a width and height in cells.
A system like this can be found at the <page>Object Oriented Lua</page> tutorial.
Note that this tutorial only discusses clientside handling of an inventory.
I'm too lazy for serverside networking...
## Let's Get Started
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
```
local ply = LocalPlayer()
ply.Inv = {}
```
ply.Inv will contain both the backpack AND the information about their inventory.
Let's create a few subtables and values in 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
```
(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:
```
for i=1,8 do --8 being the width of the backpack.
ply.Inv.Backpack[i] = {}
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][i] = false --False is a placeholder here. We'll overwrite that later.
end
end
```
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:
```
local plymeta = FindMetaTable("Player")
function plymeta:GetInvItem(x,y)
return self.Inv.Backpack[x][y]
end
```
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.
```
local PANEL = {}
AccessorFunc(PANEL, "m_ItemPanel", "ItemPanel")
AccessorFunc(PANEL, "m_Color", "Color")
function PANEL:Init()
self.m_Coords = {x=0,y=0}
self:SetSize(30,30)
self:SetColor(Color(200,200,200))
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 = x
self.m_Coords.y = y
end
function PANEL:GetCoords()
return self.m_Coords.x, 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(70,70,70,255)
surface.DrawRect(w-2,0,h,2) --borders
surface.DrawRect(0,h-2,2,w) -- ^
end
vgui.Register("InvSlot", PANEL, "DPanel")
```
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 writing an item system in this tutorial.
So we would start by creating a slot panel for each inventory slot.
```
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][i] = vgui.Create("InvSlot")
ply.Inv.Backpack[k][i]:SetPos(k*30,i*30) --The icon is 30x30.
ply.Inv.Backpack[k][i]:SetCoords(k,i)
end
end
```
Icons. Icon's everywhere.
Next, let's make another vgui element. This will `represent` an item in your backpack.
```
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 method.
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")
```
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.
```
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(v)do
for k2, pnl in ipairs(LocalPlayer().Inv.Backpack[k]) 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
```
Most of this code is copied from the drag-n-drop functionality from earlier.
Next, let's make items get picked up.
```
function PickupItem(item)
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
```
## Final Product
```
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[i] = {}
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][i] = 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[x][y]
end
local PANEL = {}
AccessorFunc(PANEL, "m_ItemPanel", "ItemPanel")
AccessorFunc(PANEL, "m_Color", "Color")
function PANEL:Init()
self.m_Coords = {x=0,y=0}
self:SetSize(30,30)
self:SetColor(Color(200,200,200))
self:SetItemPanel(false)
self:Receiver("invitem", function(pnl, item, drop, i, x, y) --Drag-drop
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 = x
self.m_Coords.y = y
end
function PANEL:GetCoords()
return self.m_Coords.x, 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(70,70,70,255)
surface.DrawRect(w-2,0,2,h) --borders
surface.DrawRect(0,h-2,w,2) -- ^
end
vgui.Register("InvSlot", PANEL, "DPanel")
local dfram = vgui.Create("DFrame")
dfram:SetSize(ScrW()/2, ScrH()/2)
dfram:Center()
dfram:MakePopup()
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][i] = vgui.Create("InvSlot", dfram)
ply.Inv.Backpack[k][i]:SetPos((k*30)+100,(i*30)+100) --The icon is 30x30.
ply.Inv.Backpack[k][i]: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(v) do
for k2, pnl in ipairs(LocalPlayer().Inv.Backpack[k]) 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)
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
```
## Test Results
<image src="grid_example.jpg"/>