Creating a DECENT inventory system

( accidently posted this too early, so im editing it )

Hi everybody. I’m working on a little side project.

It takes place in a mine… you can work with a team or alone, yadda yadda.

I need an inventory system. Problem? My brain is fried from lack of mental stimulus and working around in Blender and Roblox have been driving me to an impotent rage. I cannot think of a way to do this.

What I need:

  • A guide to making a decent(ly) working inventory system.
  • A guide to making a modular inventory system.

I got this little item manager (i am using OOP)

local Item = require(script.Parent.Item)

local ItemManager = {}
ItemManager.Items = {}

-- [[ HELPER ]] --
local function deepCopy(original)
	local copy = {}
	for k, v in pairs(original) do
		if type(v) == "table" then
			copy[k] = deepCopy(v)
		else
			copy[k] = v
		end
	end
	return copy
end

--[[
Creates a new item class.
]]
function ItemManager:CreateItem(name: string, model: Instance, data: Item.ItemData)
	local newItem = Item.new(name, model, deepCopy(data))
	table.insert(self.Items, newItem)
	return newItem
end

--[[
Creates and spawns a new item class.
]]
function ItemManager:SpawnItem(name: string, model: Instance, data: Item.ItemData, Cframe: CFrame, owner: Player?)
	local item = self:CreateItem(name, model, deepCopy(data))
	item:Spawn(Cframe, owner)
	return item
end

--[[
Uses an existing item class to spawn.
]]
function ItemManager:SpawnExisting(item, Cframe: CFrame, owner: Player)
	item:Spawn(Cframe, owner)
	return item
end

--[[
If the item has a special interaction, trigger it.
Else, the object is picked up.
]]
function ItemManager:Interact(item, player)
	if not item.Interaction then
		self:PickUp(item, player)
	else
		item:Interact(player)
	end
end

--[[
Picks up the item, and adds it to the player inventory.
]]
function ItemManager:PickUp(item, player: Player, amount: number)
	if not item or not item.Data then
		warn("Attempted pickup on nil item/data!")
		return
	end

	if not item.Instance then
		warn("Tried to pick up an item with no active instance!")
		return
	end
	
	local newStack = math.clamp(item:GetStack() - amount, 0, math.huge)
	item:TweakData("Stack", newStack)

	if item:GetStack() <= 0 then
		self:DestroyItem(item)
	end
end

--[[
Destroys the item class.
]]
function ItemManager:DestroyItem(item)
	for i, v in ipairs(self.Items) do
		if v == item then
			item:Destroy()
			table.remove(self.Items, i)
			break
		end
	end
end

--[[
Looks up the item name in the item list, returning the first value.
]]
function ItemManager:LookupByName(name: string)
	for _, item in ipairs(self.Items) do
		if item.Name == name then
			return item
		end
	end
	return nil
end

--[[
Looks up the item UUID in the item list, returning a match.
]]
function ItemManager:LookupByUUID(uuid: string)
	for _, item in ipairs(self.Items) do
		local itemUUID = item.Instance:GetAttribute("UUID")
		if itemUUID == uuid then
			return item
		end
	end
	warn("LookupByUUID failed for", uuid)
	return nil
end

--[[
Looks up the table for matching instances.
]]
function ItemManager:LookupByInstance(instance: Instance)
	for _, item in ipairs(self.Items) do
		if item.Instance == instance then
			return item
		end
	end
	warn("LookupByInstance failed for", instance:GetFullName())
	return nil
end

return ItemManager

So, I need to somehow figure out a way to make items that are picked up - appear in the inventory depending on their type. (Item, apparel).

Tags, I’d like to be made applicable (chat tags) if the player has access to them, they appear in the specified tab.

Additionally, here’s the Item class:

local Item = {}
Item.__index = Item

export type ItemData = {
	Description: string,
	Stack: number,
	Weight: number,
	Rarity: string,
	Value: number,
	Interaction: (item: Instance, player: Player) -> (),
	OnSpawn: (item: Instance, owner: Player) -> (),
	Hooks: {Key}
}

--[[
Data:
<code>Description</code> -- Description of the item
<code>Stack</code> -- how large of a stack to form.
<code>Weight</code> -- how heavy one item is
<code>Value</code> -- how much one item is worth
<code>Type</code> -- what type of item the item is.
<code>Rarity</code> -- how rare an item is
<code>Interaction</code> -- what happens when it's interacted with (self, player)
<code>OnSpawn</code> -- what happens when it's spawned (self, owner/player)
<code>Hooks</code> -- a table of keys that listen for changes with the item's data.
]]


-- [[ CONSTRUCTOR ]] --
function Item.new(name: string, model: Instance, data: ItemData)
	local self = setmetatable({}, Item)
	self.Name = name
	self.Model = model
	
	self.Data = data
	
	self.Data.Name = name or "Placeholder"
	self.Interaction = data.Interaction
	self.OnSpawn = data.OnSpawn
	
	self.Data.Hooks = data.Hooks or {}
	self.Hooks = self.Data.Hooks
	self.Instance = nil
	return self
end

-- [[ META AND HOOKS ]] --
local GlobalHooks = {
	Weight = function(item, value)
		if item.Instance then
			Item.Instance:SetAttribute("Weight", item:GetWeight())
			item.Instance:SetAttribute("StackWeight", item:GetStackWeight())
		end
	end,
	Stack = function(item, value)
		if item.Instance then
			item.Instance:SetAttribute("Stack", item:GetStack())
			item.Instance:SetAttribute("StackWeight", item:GetStackWeight())
			item.Instance:SetAttribute("StackValue", item:GetStackValue())
		end
	end,
}

local function CheckForHook(key: string, item): ((Item, any) -> ())?
	return item.Data.Hooks[key] or GlobalHooks[key]
end

local function CheckValueAttributeSupport(v)
	local t = typeof(v)
	if t == "function" or t == "table" then
		return false
	end
	return true
end


function Item:TweakData(key: string, value: any)
	if self.Data[key] == nil then
		warn(`[Item:TweakData] "{key}" not originally in data of "{self.Name}". Adding it dynamically.`)
	end

	self.Data[key] = value
	
	local hook = CheckForHook(key, self)
	if hook then
		hook(self, value)
	end

	if self.Instance then
		self.Instance:SetAttribute(key, value)
	end
end

function Item:BatchTweakData(tweaks: {[string]: any})
	for key, value in pairs(tweaks) do
		if self.Data[key] == nil then
			warn(`[Item:BatchTweakData] "{key}" not originally in data of "{self.Name}". Adding it dynamically.`)
		end
		
		self.Data[key] = value
		
		local hook = CheckForHook(key, self)
		if hook then
			hook(self, value)
		end

		if self.Instance then
			self.Instance:SetAttribute(key, value)
		end
	end
end

function Item:AddHook(key: string, callback: (item, value) -> ())
	self.Data.Hooks = self.Data.Hooks or {}
	self.Data.Hooks[key] = callback
end

function Item:RemoveHook(key: string)
	if self.Data.Hooks then
		self.Data.Hooks[key] = nil
	end
end

-- [[ GETTERS ]] --
function Item:GetStackWeight()
	local weight = self.Data.Weight or 1
	return weight * self.Data.Stack
end

function Item:GetStackValue()
	local value = self.Data.Value or 1
	return value * self.Data.Stack
end

function Item:GetDescription()
	return self.Data.Description
end

function Item:GetWeight()
	return self.Data.Weight
end

function Item:GetStack()
	return self.Data.Stack
end

function Item:GetType()
	return self.Data.Type
end

function Item:GetRarity()
	return self.Data.Rarity
end

function Item:GenerateItemUUID()
	return self.Name.."_"..HttpService:GenerateGUID(false)
end

-- [[ BASE FUNCTIONS ]] --
function Item:SetInteraction(func)
	self.Interaction = func
end

function Item:Interact(player: Player)
	if self.Interaction then
		self.Interaction(self, player)
	end
end

function Item:Spawn(cframe: CFrame, owner)
	assert(self.Model, "Model for object "..self.Name.." does not exist.")
	self.Instance = self.Model:Clone()
	self.Instance:PivotTo(cframe)
	print("Spawning item at", cframe)
	self.Instance:SetAttribute("OwnedBy", owner or "Neutral")

	self.Instance:SetAttribute("Stack", self:GetStack())
	self.Instance:SetAttribute("Weight", self:GetWeight())
	self.Instance:SetAttribute("StackWeight", self:GetStackWeight())
	self.Instance:SetAttribute("StackValue", self:GetStackValue())
	
	self.Instance:SetAttribute("UUID", self:GenerateItemUUID())

	for k, v in pairs(self.Data) do
		if k ~= "Weight" and k ~= "Stack" and CheckValueAttributeSupport(v) then
			self.Instance:SetAttribute(k, v)
		end
	end

	for _, part in self.Instance:GetDescendants() do
		if part:IsA("BasePart") then
			part.CollisionGroup = "Items"
		end
	end

	local safeParent = workspace:FindFirstChild("Game") and workspace.Game:FindFirstChild("LooseItems")
	self.Instance.Parent = safeParent or workspace

	if self.OnSpawn then
		self.OnSpawn(self, owner)
	end

	return self.Instance
end

function Item:Destroy()
	if self.Instance then
		self.Instance:Destroy()
		self.Instance = nil
	end
end

return Item

You need to use either UIGrid/UIListLayout for it to place icons accordingly and tobuse scrolling frame(depends) and knoding how to make them properly scale (would be the hardest part tbh).
And then you can just place icons inside inventory and their position will get snapped to proper position
(Oh you already did that)
You could also referance this icon in class of your item for more easier removal, if you are implementing swapping position system you could just change their ListLayoutOrder property (not sure if it is called this way)
And also yeah make sure to assign this property to all of items like in an array

Hi!

You could store the player’s inventory on the server, represented by a table. So, your item manager could have a key called “players” with indexes that correspond to a user’s id, then have the player send a request to grab your category’s data. So, for each category you could send a request for a different index like “combat”, “util”, etc

EDIT:

And as Yarik said, you can use UiGridLayout to automatically place your items into a grid. Although, to make the actual frame, just make a template frame that you clone and destroy, which you can use to freely create new inventory items.

1 Like

So store the player items in a table (dictionary in this case as there’s the weight/stack, etc.) - and then simply send an event to retrieve the data?

How would retrieving the data work? it sends out the player dictionary via remote?

Additionally - what would be the performance impact?

Sorry for the late reply.

Yes, you’d send a remote to request the data, and the server would send back all the data for that player’s inventory. How frequently you send the packet is up to you, as you could request all the data upon loading, and then have the server send updates to the client for any new items.

Performance impact wise, it’s very low. Unless your dictionaries contain megabytes of data, which I doubt, or if you’re planning to stores thousands of items, you should be perfectly fine storing them and sending them as dictionaries.

However, if you want to optimize the amount of data you send over the network, consider Zap or Bytenet as networking libraries.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.