Inventory System (or: How to Store Data in a Useful Way)
Posted: Tue Jul 04, 2017 9:06 pm
So you want to make an inventory system for your game? Or maybe not, but this tutorial might be relevant to what you are planning to achieve anyway.
Whenever you want the computer to react to the player's input dynamically, i.e. not hard-coded but so the player has freedom of action, you have to deal with three aspects:
DISCLAIMERS:
I add init python: and $ because this tutorial is intended for Ren'Py, even though most of this works in plain Python. Make sure you understand the difference between Python code, Ren'py script language and Ren'Py screen language before you even attempt to do anything that mixes the three. Which an inventory does!
As per Python convention, classes will have uppercase first letters and variables will be all lowercase. A class is a template for a collection of data under a single name.
First, we need to decide what we want to store. Let's say that we want items, of course. So we need to store the item's name: A ball. Let's do this:
That's it! We now have a class (a "type" of data package) that contains a name.
The method __init__ (that's two underscores in front and behind) is automatically called whenever a new instance of this class is created. Keep in mind that every method (a function inside a class) of a class always takes self as its first argument, which is automatically filled during its call!
We can now do this:
This creates a variable inventory and makes it an instance of the class Item. We now have a ball in our inventory and can get the name by using inventory.name.
This is not very flexible because we can only have one item at a time. So let's make a container!
If we now say
we get a backpack that has an empty list of items, called inventory. Cool?
A few words about lists:
Lists are a type of data structure in Python and are the most intuitive type of "data collection" because they can be changed and have an order in which their elements appear. The first element is always number 0, so the first element of a list named inventory is inventory[0], the 5th element is inventory[4]. Lists are defined by using [ ]: [1, 2, 3] is a list with these three numbers in this order.
Other data structures are:
dictionaries - A dictionary is unsorted, meaning it has no order, and translates one piece of data into another. Dictionaries are defined by using { }. We won't use these for the inventory.
Example:
japanese['cat'] would return 'neko'. japanese['neko'] would result in an error. As you can see, while the keys (left part of an entry) have to be unique, the returned value can be the same for several keys, which is why a dictionary can only be used in one direction.
tuples - These are just like lists, but are write-protected. They are best used for data that obviously never changes, like the days of the week. They are defined by using ( ).
sets - Something is either part of a set or it isn't. Sets have no order (they are unsorted) and can't have duplicates. You can use this to keep track of which places the player has already visited, for example, or of he has met a specific character yet. Sets are defined using Set() and offer a variety of different methods.
Now, back to our inventory.
There wouldn't be much of a point in having an entire class if all it does is contain a list, because we could just use a simple list from the start. We will now add functionality:
append is a method for lists that does exactly that: append data. So whenever we now use backpack.add_item('ball'), we get another 'ball' in our list. If we make an Item object and use it instead of just a string for the name, the whole object will be added to the list.
We are now able to make any number of Containers, each representing a different inventory, with Items stored in a list.
I will do a few steps at once now.
What happened here?
First, Item now has another variable, weight, and the Container one called weight_max. Second, we have another class called InvItem that has two data fields: item and amount.
Of course we could include the amount in the data of the item itself, but there is an important reason why we don't - just be patient!
So let's test our code:
This first creates an Item with the name 'ball' and the weight 5 and saves it in the variable ball. Then it creates a Container called backpack and adds an InvItem containing ball with the amount 1 to it.
So far, the weight does nothing. We want the game to check whether we can carry the new item, so we change the method add_item:
Now we first check if the weight of the new item will make the total weight exceed our max_weight. If so, we return an error message. If not, we append the item as well as the given amount (1 if none given).
Now comes the reason why we didn't include the amount in the item data itself: We want to be able to compare the items in our inventory to the "base items" in our database. If we include the amount, 'ball' with weight 5 and amount 1 would be a different item than 'ball' with weight 5 and amount 2, which would mean that we wouldn't be able to check if we already have a ball or not.
We want to be able to compare items in order to check for duplicates:
Explanation of the two commented lines:
In the first line, you see list brackets, so this is obviously going to be a list, which is created by taking every single element of self.inventory, calling it i, and putting .item behind it. What we get is a list of the pure Item objects in our inventory which we can use to check if the new item already exists in our inventory.
The second line does exactly the same and uses this list of stripped-down InvItem objects (now Item objects) to find the location of the existing item by using [].index. This location is then used to increase that item's amount instead of creating a new item to add to the list.
The complete code so far:
For a complete inventory system, there are still three functions left to do: If the total amount of added items is too much, add as many as possible; be able to remove items again; and check if and if yes how many of a specific item are there.
Next, we will implement these functions (in reverse order, actually). After that, we will make a screen to display the data: An inventory screen!
First, we will implement a check if and how many of a specific item are already in our inventory. We add a method to our Container class:
This method is very similar to add_item(). It checks if the item exists in our inventory and in which amount. If the amount is at least the asked for amount (default: 1), the actual amount is returned (which counts as True). If there's not enough of that item or it's not there at all, False is returned (which is effectively a 0).
You can see that we have to use self.inventory[[i.item for i in self.inventory].index(item)] a lot. Let's make this a method of our class to make our code more readable:
and exchange all instances of this term with this method call:
Next is the method for removing items. We can already effortlessly find any item (or rather, its InvItem) in our inventory and determine its amount.
I think I should explain what inventory.pop(inventory.index(self.finditem(item))) does.
inventory.pop(a) removes the element at position a from the list inventory. So we want the position of our item as a.
This a is inventory.index(b), which returns the position of the first occurrence of the element b in the list inventory.
b finally is finditem(item), which returns the actual InvItem in our inventory which we can then look for.
So this method finds the item in our inventory, decreases its amount by the given amount (if none given, by 1) and then checks if there are 0 or fewer left. If so, it removes the item (by removing its InvItem) from our inventory. After all that, it returns whether no item was found, there are still more left, or it was removed entirely.
EDIT: Added self.-reference to all calls of finditem() and has_item(). It works without in vanilla Python, but Ren'py wants those.
EDIT: Changed "python:" to "init python:". Thanks for pointing that out.
Whenever you want the computer to react to the player's input dynamically, i.e. not hard-coded but so the player has freedom of action, you have to deal with three aspects:
- a. How to store the data
b. How to use the data
c. How to present the data
DISCLAIMERS:
I add init python: and $ because this tutorial is intended for Ren'Py, even though most of this works in plain Python. Make sure you understand the difference between Python code, Ren'py script language and Ren'Py screen language before you even attempt to do anything that mixes the three. Which an inventory does!
As per Python convention, classes will have uppercase first letters and variables will be all lowercase. A class is a template for a collection of data under a single name.
First, we need to decide what we want to store. Let's say that we want items, of course. So we need to store the item's name: A ball. Let's do this:
Code: Select all
init python:
class Item(object):
def __init__(self, name):
self.name = name
The method __init__ (that's two underscores in front and behind) is automatically called whenever a new instance of this class is created. Keep in mind that every method (a function inside a class) of a class always takes self as its first argument, which is automatically filled during its call!
We can now do this:
Code: Select all
$ inventory = Item('ball')
This is not very flexible because we can only have one item at a time. So let's make a container!
Code: Select all
init python:
class Container(object):
def __init__(self):
self.inventory = []
Code: Select all
$ backpack = Container()
A few words about lists:
Lists are a type of data structure in Python and are the most intuitive type of "data collection" because they can be changed and have an order in which their elements appear. The first element is always number 0, so the first element of a list named inventory is inventory[0], the 5th element is inventory[4]. Lists are defined by using [ ]: [1, 2, 3] is a list with these three numbers in this order.
Other data structures are:
dictionaries - A dictionary is unsorted, meaning it has no order, and translates one piece of data into another. Dictionaries are defined by using { }. We won't use these for the inventory.
Example:
Code: Select all
japanese = {'god': 'kami', 'hair': 'kami', 'cat': 'neko'}
tuples - These are just like lists, but are write-protected. They are best used for data that obviously never changes, like the days of the week. They are defined by using ( ).
sets - Something is either part of a set or it isn't. Sets have no order (they are unsorted) and can't have duplicates. You can use this to keep track of which places the player has already visited, for example, or of he has met a specific character yet. Sets are defined using Set() and offer a variety of different methods.
Now, back to our inventory.
There wouldn't be much of a point in having an entire class if all it does is contain a list, because we could just use a simple list from the start. We will now add functionality:
Code: Select all
init python:
class Container(object):
def __init__(self):
self.inventory = []
def add_item(self, item):
self.inventory.append(item)
We are now able to make any number of Containers, each representing a different inventory, with Items stored in a list.
I will do a few steps at once now.
Code: Select all
init python:
class Item(object):
def __init__(self, name, weight):
self.name = name
self.weight = weight
class InvItem(object):
def __init__(self, item, amount):
self.item = item
self.amount = amount
class Container(object):
def __init__(self, weight_max):
self.inventory = []
self.weight_max = weight_max
def add_item(self, item, amount=1):
self.inventory.append(InvItem(item, amount))
First, Item now has another variable, weight, and the Container one called weight_max. Second, we have another class called InvItem that has two data fields: item and amount.
Of course we could include the amount in the data of the item itself, but there is an important reason why we don't - just be patient!
So let's test our code:
Code: Select all
$ ball = Item('ball', 5)
$ backpack = Container(100)
$ backpack.add_item(ball)
So far, the weight does nothing. We want the game to check whether we can carry the new item, so we change the method add_item:
Code: Select all
def add_item(self, item, amount=1):
if item.weight * amount > self.weight_max - sum(i.item.weight * i.amount for i in self.inventory):
return('too heavy')
else:
self.inventory.append(InvItem(item), amount)
return('success')
Now comes the reason why we didn't include the amount in the item data itself: We want to be able to compare the items in our inventory to the "base items" in our database. If we include the amount, 'ball' with weight 5 and amount 1 would be a different item than 'ball' with weight 5 and amount 2, which would mean that we wouldn't be able to check if we already have a ball or not.
We want to be able to compare items in order to check for duplicates:
Code: Select all
def add_item(self, item, amount=1):
if item.weight * amount > self.weight_max - sum(i.item.weight * i.amount for i in self.inventory):
return('too heavy')
else:
if item in [i.item for i in self.inventory]: # I can't believe I got this line to work with my first try
self.inventory[[i.item for i in self.inventory].index(item)].amount += amount # oh god why
else:
self.inventory.append(InvItem(item, amount))
return('success')
In the first line, you see list brackets, so this is obviously going to be a list, which is created by taking every single element of self.inventory, calling it i, and putting .item behind it. What we get is a list of the pure Item objects in our inventory which we can use to check if the new item already exists in our inventory.
The second line does exactly the same and uses this list of stripped-down InvItem objects (now Item objects) to find the location of the existing item by using [].index. This location is then used to increase that item's amount instead of creating a new item to add to the list.
The complete code so far:
Code: Select all
init python:
class Item(object):
def __init__(self, name, weight):
self.name = name
self.weight = weight
class InvItem(object):
def __init__(self, item, amount):
self.item = item
self.amount = amount
class Container(object):
def __init__(self, weight_max):
self.inventory = []
self.weight_max = weight_max
def add_item(self, item, amount=1):
if item.weight * amount > self.weight_max - sum(i.item.weight * i.amount for i in self.inventory):
return('too heavy')
else:
if item in [i.item for i in self.inventory]: # I can't believe I got this line to work with my first try
self.inventory[[i.item for i in self.inventory].index(item)].amount += amount # oh god why
else:
self.inventory.append(InvItem(item, amount))
return('success')
Next, we will implement these functions (in reverse order, actually). After that, we will make a screen to display the data: An inventory screen!
First, we will implement a check if and how many of a specific item are already in our inventory. We add a method to our Container class:
Code: Select all
def has_item(self, item, amount=1):
if item in [i.item for i in self.inventory]:
if self.inventory[[i.item for i in self.inventory].index(item)].amount >= amount:
return(self.inventory[[i.item for i in self.inventory].index(item)].amount)
else:
return(False)
else:
return(False)
You can see that we have to use self.inventory[[i.item for i in self.inventory].index(item)] a lot. Let's make this a method of our class to make our code more readable:
Code: Select all
def finditem(self, item):
return(self.inventory[[i.item for i in self.inventory].index(item)])
Code: Select all
class Container(object):
def __init__(self, weight_max):
self.inventory = []
self.weight_max = weight_max
def add_item(self, item, amount=1):
if item.weight * amount > self.weight_max - sum(i.item.weight * i.amount for i in self.inventory):
return('too heavy')
else:
if item in [i.item for i in self.inventory]: # I can't believe I got this line to work with my first try
self.finditem(item).amount += amount # oh god why
else:
self.inventory.append(InvItem(item, amount))
return('success')
def has_item(self, item, amount=1):
if item in [i.item for i in self.inventory]:
if self.finditem(item).amount >= amount:
return(self.finditem(item).amount)
else:
return(False)
else:
return(False)
def finditem(self, item):
return(self.inventory[[i.item for i in self.inventory].index(item)])
Code: Select all
def remove_item(self, item, amount=1):
if self.has_item(item):
self.finditem(item).amount -= amount
if self.finditem(item).amount <= 0:
self.inventory.pop(self.inventory.index(self.finditem(item)))
return('gone')
else:
return('more left')
else:
return('not found')
inventory.pop(a) removes the element at position a from the list inventory. So we want the position of our item as a.
This a is inventory.index(b), which returns the position of the first occurrence of the element b in the list inventory.
b finally is finditem(item), which returns the actual InvItem in our inventory which we can then look for.
So this method finds the item in our inventory, decreases its amount by the given amount (if none given, by 1) and then checks if there are 0 or fewer left. If so, it removes the item (by removing its InvItem) from our inventory. After all that, it returns whether no item was found, there are still more left, or it was removed entirely.
EDIT: Added self.-reference to all calls of finditem() and has_item(). It works without in vanilla Python, but Ren'py wants those.
EDIT: Changed "python:" to "init python:". Thanks for pointing that out.