Grid-Based Inventory System

From Garry's Mod
Jump to: navigation, search

Contents

Prerequesites

  1. Lua knowledge.
  2. 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 Object Oriented Lua 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

grid example.jpg


--Bobblehead 00:03, 13 December 2014 (UTC)

Personal tools
Navigation