Introduction
For the sake of my sanity, this tutorial will only go over the server side
Whenever I log on to the Developer Forums to see what new creations are being released I almost always see a notification pop up, what is it? It’s a heart on one of my posts about how I create inventories. I did a few quick searches to find out that nobody has a real tutorial on how to make a secure and easily manageable inventory.
This tutorial will be broken in to two sections, a intermediate level as well as a hard level. The intermediate level will be using intermediate level scripting, you won’t be required to use any fancy metamethods, etc… But as usual this does cost some convenience. The hard level will use an Object Orientated Style of inventory, I highly recommend actually looking in to the code and reading it, if you don’t understand it then you should take some time to figure out what everything does beyond the documentation that I put. Please don’t just copy and paste the code because that doesn’t help anyone.
Security
Not only will this tutorial be focusing on how you can make an inventory, but how you can secure it. The last thing you want as a developer is your games economy being stripped down and becoming useless because of an exploiter who can manipulate his/her inventory.
I know what you’re thinking, you can just add sanity checks on the server! While this is true, and sanity checks are what we’ll be focusing on, you need to also keep in mind as to how you’re giving players items. For example, if a user does a quest and then in return they earn 5 gold, you should also make sure that the quest can’t be manipulated in any way to become farmable via exploits. At the bottom of this page is a list of do’s and dont’s, I highly recommend looking in to them and making sure you’re being as secure as possible.
Intermediate Level
The intermediate level will require knowledge of the following.
- Tables
- Modules (Optional)
For the sake of this tutorial not being a book, we’ll be using one item for testing, the item will be referred to as an axe.
Let’s hop right in to how you can make your first ever inventory! The first thing we’ll want to do is figure out how we want our inventory to function. For our case I’ll be using a module script with two tables, one table will store our cached inventories, the other will store the inventory functions.
local Inventories = {} -- Our stored/cached inventories.
local Inventory = {} -- The table with our inventory functions
return Inventory -- Module script, return functions
Boom! Now we have two tables and they’ll work as they should in our module script.
Creating an inventory
Next up we need to implement a way to actually create an inventory. We’ll make a new function called “new”, this will create a new inventory!
We want to make sure that the arguments passed to our function are valid, to do this we add a check to make sure they’re not nil.
function Inventory.new(player, contents) -- Hey! Our new function, it takes a player and contents.
if not player then
return -- The player that we specified doesn't exist.
end
end
Cool! We have a function set up to create an inventory, but truth be told… It does nothing right now! So, how do we fix that? Well the way we have it set up right now is that it’ll accept an argument called contents. In a perfect world “contents” would always be a table. However this isn’t a perfect world. Assuming you’re sending something from a datastore and contents is nil, we’ll need to make a new inventory for the user.
So what do we do? We check to see if contents is a nil value or not, if it is then make the user a new, empty, inventory. Or in our case give the user one axe. For this tutorial the number next to the item name will be the count of how many of that item a user has.
function Inventory.new(player, contents) -- Hey! Our new function, it takes a player and contents.
if not player then
return -- The player that we specified doesn't exist.
end
-- alternatively you could do "contents = contents or {}", but for the sake of this tutorial we won't be doing that.
if not contents then -- We have no contents, let's make some.
contents = { -- Setting our contents to a table
["Axe"] = 1, -- Hey! The user has one axe. If you don't want the user to have any items by default then simply remove this line.
}
end
end
Believe it or not the hard part is pretty much over. We now have contents of an inventory as well as the player. From here on out its just manipulating the users contents to give or remove items from them! Simple right?
So from here we’ll put our users inventory inside of our cached inventories (we don’t want to make an inventory each time so we do this) Then we’ll return the inventory.
Inventories[player] = contents
return Inventories[player]
Boom! We’ve successfully created a new inventory with whatever contents it is that you need. For our purposes I won’t be going over using DataStores so your players save items. There is a really good tutorial which can be found here about that.
So? Our entire function looks like this.
function Inventory.new(player, contents) -- Hey! Our new function, it takes a player and contents.
if not player then
return -- The player that we specified doesn't exist.
end
if not contents then -- We have no contents, let's make some.
contents = { -- Setting our contents to a table
["Axe"] = 1, -- Hey! The user has one axe.
}
end
Inventories[player] = contents -- Assign the users contents to our cache
return Inventories[player] -- return the inventory, (semi useless b/c its just content but still)
end
Getting our players inventory
We know how to make an inventory which is great, but what if we want to get the contents of the inventory (we could easily change values this way or display the items, etc…) Well it’s simple. We just make a function called get… and return the users cached table.
function Inventory:Get(player)
if not player then
return -- Not going over this because I already did in the "new" function
end
return Inventories[player] -- Return the cached inventory!
end
This will either return nil, if the inventory doesn’t exist, or it’ll return the contents of the inventory (in a table form)
Adding items to our inventory
Now… Creating inventories is really helpful, but you can’t do much without being able to add items to an inventory, etc… How do we add an item to an inventory? It’s simple, we get the users inventory and then we manipulate the index.
We’ll want the player, the name of the item to add, and the amount of that item we’re adding. Because we love being fancy we’ll make the amount argument optional.
The first thing to do is create the function and get the users inventory, which can be seen below. We also want to make sure the user has an inventory to add an item to, if they don’t then return.
function Inventory:Add(player, name, amount)
if not player or not name then
return -- Not going over this because I already did in the "new" function
end
local inventory = Inventory:Get(player)
if not inventory then
return -- The user doesn't have an inventory, we can't add an item
end
end
Now that we’ve recieved the inventory we want to add an item to it, it’s not as easy as it seems but it’s still rather easy. We need to check first if the user already has whatever item you’re trying to give them. If they do then simply add one to the count, if not then we’ll put the item in the users inventory.
if inventory[name] then -- Check if the user has the item you're giving
inventory[name] = inventory[name] + amount or 1 -- They do, just add the amount or 1
else
inventory[name] = amount or 1 -- They don't, give them the amount of items or 1
end
And just like that we have a function which will successfully add an item to the users inventory. Simple! The entire function is as follows.
function Inventory:Add(player, name, amount)
if not player or not name then
return -- Not going over this because I already did in the "new" function
end
local inventory = Inventory:Get(player)
if not inventory then
return -- The user doesn't have an inventory, we can't add an item
end
if inventory[name] then -- Check if the user has the item you're giving
inventory[name] = inventory[name] + amount or 1 -- They do, just add the amount or 1
else
inventory[name] = amount or 1 -- They don't, give them the amount of items or 1
end
end
Removing items from our inventory
I’m going to make this short and simple as a lot of it is the same as the adding. The only difference is that we want to make sure we don’t remove items in to the negatives. How do we do that? We add a check to make sure that if we subtract the amount given it won’t be less then 0, if it is then we remove the item from the inventory completely.
if inventory[name] then -- Check if the user has the item you're giving
if inventory[name] - (amount or 1) < 0 then -- Check if the amount your removing is more then the user has
inventory[name] = nil -- Delete the item completely
else
inventory[name] = inventory[name] - amount or 1 -- Remove the amount or one item.
end
end
The entire code snippet can be found here.
function Inventory:Remove(player, name, amount)
if not player or not name then
return -- Not going over this because I already did in the "new" function
end
local inventory = Inventory:Get(player)
if not inventory then
return -- The user doesn't have an inventory, we can't add an item
end
if inventory[name] then -- Check if the user has the item you're giving
if inventory[name] - (amount or 1) < 0 then -- Check if the amount your removing is more then the user has
inventory[name] = nil -- Delete the item completely
else
inventory[name] = inventory[name] - amount or 1 -- Remove the amount or one item.
end
end
end
Usage
Congratulations you’ve now made a pretty simple inventory system. But how do you use it? I scripted mine in a modulescript, therefor you’ll need to require that module script and then use go from there.
Here are some code samples below.
local inventory_module = require(script.Inventory)
local Players = game:GetService("Players")
function addItem(player, name, amount)
inventory_module:Add(player, name, amount)
end
function removeItem(player, name, amounnt)
inventory_module:Remove(player, name, amounnt)
end
Players.PlayerAdded:Connect(function(player)
inventory_module.new(player)
wait(3)
addItem(player, "Axe", 2) -- or addItem(player, "Axe")
wait(3)
removeItem(player, "Axe", 1)
print(inventory_module:Get(player).Axe) -- Prints 1
end)
Entire Intermediate Product
local Inventories = {}
local Inventory = {}
function Inventory:Remove(player, name, amount)
if not player or not name then
return -- Not going over this because I already did in the "new" function
end
local inventory = Inventory:Get(player)
if not inventory then
return -- The user doesn't have an inventory, we can't add an item
end
if inventory[name] then -- Check if the user has the item you're giving
if inventory[name] - (amount or 1) < 0 then -- Check if the amount your removing is more then the user has
inventory[name] = nil -- Delete the item completely
else
inventory[name] = inventory[name] - amount or 1 -- Remove the amount or one item.
end
end
end
function Inventory:Add(player, name, amount)
if not player or not name then
return -- Not going over this because I already did in the "new" function
end
local inventory = Inventory:Get(player)
if not inventory then
return -- The user doesn't have an inventory, we can't add an item
end
if inventory[name] then -- Check if the user has the item you're giving
inventory[name] = inventory[name] + amount or 1 -- They do, just add the amount or 1
else
inventory[name] = amount or 1 -- They don't, give them the amount of items or 1
end
end
function Inventory:Get(player)
if not player then
return -- Not going over this because I already did in the "new" function
end
return Inventories[player] -- Return the cached inventory!
end
function Inventory.new(player, contents) -- Hey! Our new function, it takes a player and contents.
if not player then
return -- The player that we specified doesn't exist.
end
if not contents then -- We have no contents, let's make some.
contents = { -- Setting our contents to a table
["Axe"] = 1, -- Hey! The user has one axe.
}
end
Inventories[player] = contents -- Assign the users contents to our cache
return Inventories[player] -- return the inventory, (semi useless b/c its just content but still)
end
return Inventory
Hard Level
The intermediate level will require knowledge of the following.
- Tables
- Modules
- Metatables
For the sake of this tutorial not restricting you, you’ll be required to figure out how you want your items stored.
Now I say this is difficult, but it’s really not, you just need to take it slow and problem solve it. If you don’t understand the required knowledge then don’t get frustrated, I recommend using the resources available to you and learn it.
With that being said, I’m not going to do baby steps and explain everything, this is meant to be a tutorial but you also are meant to have a good knowledge of the list above!
Alright, so this will be scripted in a modulescript. I would recommend making a Framework for the best use however how you handle it is up to you. We’ll start off by creating a function which will create a new inventory.
Our setup will be as follows.
local Inventory = {}
function Inventory.new()
end
return Inventory
Inside of we’ll make a new metatable and return it, this will be the backbones of our entire inventory and will handle it. We want to use the index function for an Object Orientated type system. The reason that I don’t set the index to Inventory, (ie; __index = inventory) is so we can manipulate the function to run a piece of code each time we access the table. This is good if you want to fire an event each time you update the inventory, rather then posting a line of code to fire the client at the bottom of each function you can just do it once in the __index.
function Inventory.new(contents)
return setmetatable({
Contents = contents, -- Store contents of the inventory
}, {
__index = function(self, index)
-- Check if the index called on our inventory exists
if Inventory[index] then
-- Check if the inventories index is a function, if so return
-- the function
if type(Inventory[index] == "function") then
return function(self, ...)
-- The reason that we do a constructor like this is so
-- you can fire an event after each function to do things such
-- as update the inventory, etc...
return Inventory[index](self, ...)
end
else
-- Return the index
return Inventory[index]
end
end
end
})
end
Adding an item
This code sample is going to be pretty much exactly like the beginner version, the only difference is that we’ll be referencing self. If you don’t know what “self” is then please follow the beginner tutorial and learn how metatables work before revisiting here.
function Inventory:Add(item, amount)
-- Check the item argument and make sure it's specified
if not item then
return -- It's not specified, return
end
-- Check if the user has the item, if they do then add
-- the amount to it, if not then define it to the amount or 1
if not self.Contents[item] then
-- Defining the item to the amount or 1
self.Contents[item] = amount or 1
else
-- They have the item, adding a count to it.
self.Contents[item] = self.Contents[item] + amount or 1
end
end
Removing an item
Once again, this code sample will be really similar to the beginner version as well as the adding of an item.
function Inventory:Remove(item, amount)
-- Ensure that the item you want to remove exists as
-- well as that the user has the item.
if not item or not self.Contents[item] then
return
end
-- Check if removing the said amount would result in
-- a negative, if it does then remove the item completely
if self.Contents[item] - (amount or 1) < 0 then
self.Contents[item] = nil -- Remove the item
else
-- Remove the amount specified from the items count
self.Contents[item] = self.Contents[item] - amount or 1
end
end
Completed Version
local Inventory = {}
function Inventory:Remove(item, amount)
-- Ensure that the item you want to remove exists as
-- well as that the user has the item.
if not item or not self.Contents[item] then
return
end
-- Check if removing the said amount would result in
-- a negative, if it does then remove the item completely
if self.Contents[item] - (amount or 1) < 0 then
self.Contents[item] = nil -- Remove the item
else
-- Remove the amount specified from the items count
self.Contents[item] = self.Contents[item] - amount or 1
end
end
function Inventory:Add(item, amount)
-- Check the item argument and make sure it's specified
if not item then
return -- It's not specified, return
end
-- Check if the user has the item, if they do then add
-- the amount to it, if not then define it to the amount or 1
if not self.Contents[item] then
-- Defining the item to the amount or 1
self.Contents[item] = amount or 1
else
-- They have the item, adding a count to it.
self.Contents[item] = self.Contents[item] + amount or 1
end
end
function Inventory.new(contents)
return setmetatable({
Contents = contents, -- Store contents of the inventory
}, {
__index = function(self, index)
-- Check if the index called on our inventory exists
if Inventory[index] then
-- Check if the inventories index is a function, if so return
-- the function
if type(Inventory[index] == "function") then
return function(self, ...)
-- The reason that we do a constructor like this is so
-- you can fire an event after each function to do things such
-- as update the inventory, etc...
return Inventory[index](self, ...)
end
else
-- Return the index
return Inventory[index]
end
end
end
})
end
return Inventory
Disclaimer
Yes, the “hard” version was rushed, it was meant to be. I don’t want beginners copying and pasting the code and not learning from it. It being somewhat incomplete and not having usage examples allows me to make sure that people at least are doing some research about it.
Security
Existance
When letting a user pick up an item from the client you want to make sure that the item they're picking up actually exists. How do you do this? You can send a model or a unique id of the item.
When the user picks up an item fire the event with the arguments being the item they’re picking up, on the server make sure that it exists and is a valid item.
local Event = game:GetService("ReplicatedStorage"):WaitForChild("PickupItem")
local Items_Folder = game:GetService("Workspace"):WaitForCild("GroundItems")
Event.OnServerEvent:Connect(function(player, item)
if not item or not Items_Folder:FindFirstChild(item) then
return
end
-- Add a radius check, (as seen below) and then give the user the item.
end)
Radius Checks
When making an inventory and allowing users to pick up items from the client one of the most important safety check is making sure that the user is within range to pick up an item, otherwise they can potentially pick up an item half way across the map (using exploits)
local distance = (HumanoidRootPart.Position - Object.Position).Magnitude
if distance > 5 then
return
end
-- Give the user an item here, they're in range.
Realism
This one is the easiest to program. Make sure that the item the user is giving themselves is realistic. If the user is trying to pick up 10,000,000 Diamonds then you can most likely assume that it's a form of cheat. Keep in mind that doing this sort of check could result in some unfortunate loss, (ie; if a user truly does have 10,000,000 diamonds and you remove them from the inventory)