Inventory System (or: How to Store Data in a Useful Way)

A place for Ren'Py tutorials and reusable Ren'Py code.
Forum rules
Do not post questions here!

This forum is for example code you want to show other people. Ren'Py questions should be asked in the Ren'Py Questions and Announcements forum.
Post Reply
Message
Author
User avatar
Milkymalk
Miko-Class Veteran
Posts: 659
Joined: Wed Nov 23, 2011 5:30 pm
Completed: Don't Look (AGS game)
Projects: KANPEKI! ★Perfect Play★
Organization: Crappy White Wings
Location: Germany
Contact:

Inventory System (or: How to Store Data in a Useful Way)

#1 Post by Milkymalk » 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:
  • a. How to store the data
    b. How to use the data
    c. How to present the data
So let's take making an inventory system as a general example for data storage and processing.

DISCLAIMERS:
I add 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

python:
    class Item(object):
        def __init__(self, name):
            self.name = name
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:

Code: Select all

$ inventory = Item('ball')
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!

Code: Select all

python:
    class Container(object):
        def __init__(self):
            self.inventory = []
If we now say

Code: Select all

$ backpack = Container()
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:

Code: Select all

japanese = {'god': 'kami', 'hair': 'kami', 'cat': 'neko'}
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:

Code: Select all

python:
    class Container(object):
        def __init__(self):
            self.inventory = []

        def add_item(self, item):
            self.inventory.append(item)
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.

Code: Select all

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))
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:

Code: Select all

$ ball = Item('ball', 5)
$ backpack = Container(100)
$ backpack.add_item(ball)
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:

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 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 of 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:

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')
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:

Code: Select all

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')
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:

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)
This method is very simular 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:

Code: Select all

        def finditem(self, item):
            return(self.inventory[[i.item for i in self.inventory].index(item)])
and exchange all instances of this term with this method call:

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)])
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.

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')
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.
Last edited by Milkymalk on Sun Dec 17, 2017 12:35 pm, edited 10 times in total.
Crappy White Wings (currently quite inactive)
Working on: KANPEKI!
(On Hold: New Eden, Imperial Sea, Pure Light)

User avatar
Milkymalk
Miko-Class Veteran
Posts: 659
Joined: Wed Nov 23, 2011 5:30 pm
Completed: Don't Look (AGS game)
Projects: KANPEKI! ★Perfect Play★
Organization: Crappy White Wings
Location: Germany
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#2 Post by Milkymalk » Tue Jul 04, 2017 9:06 pm

(placeholder for part 2)
Last edited by Milkymalk on Tue Jul 04, 2017 10:41 pm, edited 1 time in total.
Crappy White Wings (currently quite inactive)
Working on: KANPEKI!
(On Hold: New Eden, Imperial Sea, Pure Light)

User avatar
Milkymalk
Miko-Class Veteran
Posts: 659
Joined: Wed Nov 23, 2011 5:30 pm
Completed: Don't Look (AGS game)
Projects: KANPEKI! ★Perfect Play★
Organization: Crappy White Wings
Location: Germany
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#3 Post by Milkymalk » Tue Jul 04, 2017 9:07 pm

(placeholder for part 3)
Last edited by Milkymalk on Tue Jul 04, 2017 10:41 pm, edited 1 time in total.
Crappy White Wings (currently quite inactive)
Working on: KANPEKI!
(On Hold: New Eden, Imperial Sea, Pure Light)

User avatar
Milkymalk
Miko-Class Veteran
Posts: 659
Joined: Wed Nov 23, 2011 5:30 pm
Completed: Don't Look (AGS game)
Projects: KANPEKI! ★Perfect Play★
Organization: Crappy White Wings
Location: Germany
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#4 Post by Milkymalk » Tue Jul 04, 2017 9:08 pm

(placeholder for part 4)
Crappy White Wings (currently quite inactive)
Working on: KANPEKI!
(On Hold: New Eden, Imperial Sea, Pure Light)

User avatar
Enchant00
Regular
Posts: 135
Joined: Tue Jan 12, 2016 1:17 am
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#5 Post by Enchant00 » Thu Jul 06, 2017 3:39 pm

Wow this is really clear cut and easy to understand XD I'm an experienced python programmer so even without the explanations I could easily understand and reconstruct what you did but if a beginner were to see this then even without much programming experience they would be able to do it. Your explanations are really simple with not much jargon so kudos and keep it up! XD when you start with the screen language tutorial that's when people will be able to see the magic ;)

User avatar
DannyGMaster
Regular
Posts: 113
Joined: Fri Sep 02, 2016 11:07 am
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#6 Post by DannyGMaster » Sat Jul 08, 2017 8:46 pm

This is the Tutorial I wish I had when I started working with Python and Ren'Py, most of this I already know but I actually learned a few things I didn't and it served as a nice refresher, I'll probably come back here if I ever get stuck with my own inventory. You've made a great job putting this together, I'll be definitely looking forward to the next parts, keep up the good work!
The silent voice within one's heart whispers the most profound wisdom.

User avatar
Milkymalk
Miko-Class Veteran
Posts: 659
Joined: Wed Nov 23, 2011 5:30 pm
Completed: Don't Look (AGS game)
Projects: KANPEKI! ★Perfect Play★
Organization: Crappy White Wings
Location: Germany
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#7 Post by Milkymalk » Sat Jul 08, 2017 11:31 pm

Thank you, both! Hearing this motivates me to continue with this tutorial :) Though it's almost done unless I come up with more functions that I want to have. What's left then is only the inventory screen which I will have two varieties of.
If you have suggestions for features, let me know! But it will take a few days because I have some upcoming deadlines I have to keep.
Crappy White Wings (currently quite inactive)
Working on: KANPEKI!
(On Hold: New Eden, Imperial Sea, Pure Light)

User avatar
dreamfarmer
Regular
Posts: 25
Joined: Mon Feb 27, 2017 8:19 pm
Deviantart: exstarsis
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#8 Post by dreamfarmer » Tue Oct 24, 2017 8:04 pm

I wish there was more of this...

User avatar
Milkymalk
Miko-Class Veteran
Posts: 659
Joined: Wed Nov 23, 2011 5:30 pm
Completed: Don't Look (AGS game)
Projects: KANPEKI! ★Perfect Play★
Organization: Crappy White Wings
Location: Germany
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#9 Post by Milkymalk » Wed Nov 22, 2017 2:00 am

To be honest I ran out of ideas what to show. If you have wishes or ideas, tell me and I might actually include it.
Crappy White Wings (currently quite inactive)
Working on: KANPEKI!
(On Hold: New Eden, Imperial Sea, Pure Light)

Errilhl
Regular
Posts: 163
Joined: Wed Nov 08, 2017 4:32 pm
Projects: HSS
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#10 Post by Errilhl » Thu Nov 23, 2017 6:09 am

Would be nice to have a "display_all" function, perhaps, to simplify the displaying of the actual content?

Also... I get a consistent error that "global name 'finditem' is not defined":

Code: Select all

I'm sorry, but an uncaught exception occurred.

While running game code:
  File "game/script.rpy", line 327, in script
    if not backpack.has_item(schoolbooks):
  File "game/script.rpy", line 327, in <module>
    if not backpack.has_item(schoolbooks):
  File "game/class.rpy", line 32, in has_item
    return(False)
NameError: global name 'finditem' is not defined

-- Full Traceback ------------------------------------------------------------

Full traceback:
  File "game/script.rpy", line 327, in script
    if not backpack.has_item(schoolbooks):
  File "C:\Program Files (x86)\renpy-6.99.13-sdk\renpy\ast.py", line 1681, in execute
    if renpy.python.py_eval(condition):
  File "C:\Program Files (x86)\renpy-6.99.13-sdk\renpy\python.py", line 1794, in py_eval
    return py_eval_bytecode(code, globals, locals)
  File "C:\Program Files (x86)\renpy-6.99.13-sdk\renpy\python.py", line 1788, in py_eval_bytecode
    return eval(bytecode, globals, locals)
  File "game/script.rpy", line 327, in <module>
    if not backpack.has_item(schoolbooks):
  File "game/class.rpy", line 32, in has_item
    return(False)
NameError: global name 'finditem' is not defined

Windows-8-6.2.9200
Ren'Py 6.99.13.2919
High School Shenaningans 0.018-(Alpha)
Currently working on: Image

User avatar
Milkymalk
Miko-Class Veteran
Posts: 659
Joined: Wed Nov 23, 2011 5:30 pm
Completed: Don't Look (AGS game)
Projects: KANPEKI! ★Perfect Play★
Organization: Crappy White Wings
Location: Germany
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#11 Post by Milkymalk » Mon Dec 04, 2017 4:27 pm

Thanks for the heads up, Ren'py needs a self.-reference for all internal calls of finditem() (it worked without it in pure Python). I corrected the script.
Crappy White Wings (currently quite inactive)
Working on: KANPEKI!
(On Hold: New Eden, Imperial Sea, Pure Light)

User avatar
wyverngem
Miko-Class Veteran
Posts: 594
Joined: Mon Oct 03, 2011 7:27 pm
Completed: Simple as Snow, Lady Luck's Due,
Projects: Aether Skies, Of the Waterfall
Tumblr: casting-dreams
itch: castingdreams
Location: USA
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#12 Post by wyverngem » Mon Dec 11, 2017 12:59 pm

I really need to read through this as there seems to be a lot of good information here. The only thing is how to apply these variables to screen language. My first project, before I knew renpy, I set up a screen like this:

(Don't use this as a reference it's very old code, and abbreviated I had over 50 items at one time.)

Code: Select all

init:
    $ gherbs = 0
    $ gdes = "A plant used in healing recovery."
    $ rherbs = 0
    $ rdes = "A plant used in stamina recovery."
    $ pherbs = 0
    $ pdes = "A plant used in magic recovery."    

screen myinv:
    frame:
        background Solid("#c71585") xfill True yfill True
    vbox:
        if gherbs > 0:
            hbox:
                spacing 50
                text str("Green Herbs ")
                text str(gherbs)
                text str(gdes)
        else:
            null width 1
        if rherbs > 0:
            hbox:
                spacing 50
                text str("Red Herbs ")
                text str(rherbs)
                text str(rdes)
        else:
            null width 1
        if rherbs > 0:
            hbox:
                spacing 50
                text str("Purple Herbs ")
                text str(pherbs)
                text str(pdes)
        else:
            null width 1
Using your set up how would you display similar information? I feel that's the connection that I'm lacking is how Classes and screen language can work together.

User avatar
Milkymalk
Miko-Class Veteran
Posts: 659
Joined: Wed Nov 23, 2011 5:30 pm
Completed: Don't Look (AGS game)
Projects: KANPEKI! ★Perfect Play★
Organization: Crappy White Wings
Location: Germany
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#13 Post by Milkymalk » Tue Dec 12, 2017 9:09 pm

wyverngem wrote:
Mon Dec 11, 2017 12:59 pm
The only thing is how to apply these variables to screen language.
I was planning to do a part about screen language since that would be the "how to present the data" part I initially spoke of. I haven't gotten to actually do it until now, but I will give you a quick answer to this for now.

Basically, you need a loop that iterates through all elements of your Container class instance and displays them. Ideally, you put it in a hbox or vbox so the elements are neatly arranged next to each other or in a column. As the list in your Container.inventory only includes items you actually have, there is no need to check if there's more than 0 of it. However, you might want to run a routine that sorts the items by alphabet or whatever. Otherwise, if your first item is reduced to 0 (and thus removed from the list) and later added again, it will be the last item in the list. You could also include an ID in the core Item data so you always sort by that number.

Code: Select all

screen invdisplay:

    ## screen stuff here ##
    
    hbox:    ## horizontal box arranges all elements next to each other
        for i in backpack.inventory:    ## iterates through all inventory items
            vbox:   ## vertical box displays the data of each element in one column
                text i.item.name
                text "Weight: "+str(i.item.weight)   ## needs str() because i.item.weight is a number
                text "Number: "+str(i.amount)   ## same here - note that it's i.amount, not i.item.amount!
In short, just use the class instances like any other variable.

Code is untested, my screen language is prone to various fails.
If you expand the classes, you can also include filenames for icons and display those in the screen, and whatever you want. If you understand how the classes I used work and why they are set up that way, you can modify them to your needs.

I hope this helps.
Crappy White Wings (currently quite inactive)
Working on: KANPEKI!
(On Hold: New Eden, Imperial Sea, Pure Light)

olismith1
Newbie
Posts: 1
Joined: Wed Feb 07, 2018 5:15 pm
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#14 Post by olismith1 » Wed Feb 07, 2018 5:16 pm

i did this and it didn't work. What am i doing wrong:



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')

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)

def finditem(self, item):
return(self.inventory[[i.item for i in self.inventory].index(item)])

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')

def inv(self):

for i in backpack.inventory:
print(i.item.name)
print("Weight: "+str(i.item.weight)) ## needs str() because i.item.weight is a number
print("Number: "+str(i.amount)) ## same here - note that it's i.amount, not i.item.amount!


ball = Item('ball', 5)
backpack = Container(100)
backpack.add_item(ball)
backpack.remove_item(ball)
backpack.inv()

verysunshine
Veteran
Posts: 281
Joined: Wed Sep 24, 2014 5:03 pm
Organization: Wild Rose Interactive
Contact:

Re: Inventory System (or: How to Store Data in a Useful Way)

#15 Post by verysunshine » Tue Jan 22, 2019 5:57 pm

It seems like there might be a couple of issues with this.
  • The class-setting python block should be a "init python" block
    Defines set in a block won't cooperate properly with rollback and saving
Thanks to trooper6 for actually catching these mistakes.

Build the basics first, then add all the fun bits.

Please check out my games on my itch.io page!

Post Reply

Who is online

Users browsing this forum: No registered users