Garry's Mod Wiki

Revision Difference

Grid-Based_Inventory_System#511643

<cat>Dev.UI</cat>⤶ ## Prerequesites ⤶ ⤶ 1. Lua knowledge.⤶ 1. An item system. Each item must have two functions:⤶ 1. * ITEM:GetIcon() -- Must return a Material() object.⤶ 1. * 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(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⤶ ```⤶ ⤶ 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(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)⤶ 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 ⤶ ⤶ ⤶ ⤶ -- 00:03, 13 December 2014 (UTC)⤶ ⤶ ⤶ ⤶