Dynamic Menus and Dialog In RenPy

This is part 2 of a 4 article series on some useful techniques for making a RenPy game. We’ll show you how to implement a simple inventory system and build an intelligent menu for a shop/vending machine that reacts and responds appropriately to player inventory and stats.

The other articles are listed below, links will be added as the articles come out.

Building a Dynamic Menu

At it’s core, building a dynamic starts with a simple “if” statement after a menu choice. i.e.

label vending_machine:
    menu:
        "A reshing beverage" if money.level >= 1:
            "You purchase the bevage, it looks delicious."
            $money.dec()
            $beverage = resource(1)
        "You don't have enough money" if money.level < 1:
            "You walk away sadly."
            "Maybe you should get a job?"

Different options will be displayed if the player has certain items or stats (in this case, ‘money’), or if certain flags have been triggered in the game. 

  But what if we want to enhance this menu so that our vending machine/shop has an manageable inventory?

There’s multiple ways to do this: we’ll start with the simple solution that should work for 90% of projects out there and then more into a more advanced solution that is powerful and infinitely extensible, but somewhat more complex.

A Simple Method for Shop Inventory

To give the vending machine its own inventory we first need to use some Python to declare what the vending machine can sell:

$ vending_inventory = ['sword','pizza','potion']

 Now we just need to modify our menu options so that only items not purchased yet show up in the menu:

label vending_machine:
    menu:
        "A sword, you're not really sure why it's in the vending machine." if 'sword' in vending_inventory:
            $vending_inventory.remove('sword')
            "The sword clanks out of the vending machine."
        "A pizza...it has pineapple on it." if 'pizza' in vending_inventory:
            $vending_inventory.remove('pizza')
            "The machine yields the pizza. Hopefully you won't regret this."
        "A potion, it might be too strong for you to handle." if 'potion' in vending_inventory:
            $vending_inventory.remove('potion')
            "The potion rolls out of the vending machine. Cherry flavored, alright!"
        "It seems to be empty.You bought everything!" if len(vending_inventory)==0:
            jump some_other_menu

As each option is ‘purchased’ by the player, it disappears from the menu. When the menu is empty a special option to leave the vending machine is shown.

This allows us to re-use the same menu each time the player visits the vending machine.

Since the option to leave the vending machine is revealed only when the player has purchased everything, this will force the player to purchase everything before leaving the machine. If you do not want this behavior, simply remove the “if len(vending_inventory)==0” from the last option and the player will be able to walk away at any time.

This method is good for very simple shop/inventory dynamics, but it becomes difficult to maintain. If you put in the code for purchasing items (deducting ‘money’) and adding them to the player inventory, it gets to be rather long and difficult to read.

At that point if you decide to change something, you’re now hunting through lines and lines of messy code just to change it!

If your game is large, this could be exhausting.

A Power Method for Inventory and Stats

We can make our life’s easier in the long run if we’re willing to put in a little bit of work upfront. 

In the last tutorial we declared some Python code at the start of our sript.rpy file. We’re going to do that again, but with some modifications:

###custom python stuff
init python:#declare a python black so we can write python code

    class Player:# a generic class to represent the player
        def __init__(self):
            self.stats = {'love':0,'money':0}
            self.inventory = {'cookie':1}
        def change_stat(self,stat,modifier):
            self.stats[stat] = self.stats[stat] + modifier
        def change_inventory(self,item,amount):
            if item in self.inventory:
                self.inventory[item] = self.inventory[item]+amount
            else:
                 self.inventory[item] = amount
           
    player = Player()#a player object

    class vending_machine(self):#generic vending machine class
        def __init__(self):#declare default vending machine inventory
            self.inventory = {'cookie':3,'rusty sword':1}
            self.costs = {'cookie':1,'rusty sword':5}

        def player_can_afford(self,item_name):
            if self.costs[item_name] > player.stats['money']: return false
            return true

        def purchase(self,item,amount=1):
            player.change_stat('money',-self.costs[item_name])
            player.change_inventory(item,amount)
            self.inventory[item] = self.inventory[item] - amount

        def has(self,item):
            return self.inventory[item] > 0

This time, we’ve consolidated stats and inventory into a Player class which has 3 methods:

__init__(self): this method set our default stats and inventory for the player in this case, love and money are 0. Fortunately, our player starts with a cookie.

change_stat(self, stat, modifier): this method adjusts a stat by a given amount.

change_inventory(self,item,amount): basically the same as the above method, but modifies inventory instead.

This gives us an intuitive way of interacting with player stats and inventory. i.e.

$player.change_stat('love',2)#add two love
$player.change_inventory('used napkin',1)#gain one used napkin

Now we combine this with a vending_machine class which has the following 4 methods:

__init__(self): this method sets the default inventory and cost for a vending machine object. This is useful if you want all vending machines to have the same cost or carry the same inventory.

If not, you can always override the inventory and cost variables for your specific vending machine. If you’re comfortable with Python, you may even modify this method to take inventory and costs as an argument.

player_can_afford(self,item_name): this method gets the cost of the item_name, if it is more than the player’s current money it returns false otherwise it returns true. This will be useful in the next section when we put it together in a dialog menu.

purchase(self,item,amount): this method handles the transaction for us: deducting the items from vending_machine inventory, deducting the cost from the player, and adding the item to inventory.

has(self,item): a simple method that checks if the vending machine is sold out. Useful for cleaner code in dialog menus.

This technique allows us to declare our vending machine inventory and costs in one, single spot at the top of our file, making it trivial to locate and adjust it on the fly while keeping our dialog options uncluttered by cumbersome transaction code.

Putting it together

Now that we’ve created meaningful player and vending_machine classes, let’s put them to use in a smart dialog menu:

$shady_vend = vending_machine()#this is here for convenience, it should be declared in the initial Python block at the top of the file
label shady_vending:
    menu:
        "Cookie $[shady_vend.costs['cookie']]" if (shady_vend.player_can_afford('cookie') and shady_vend.has('cookie')):
            "You buy a delicious cookie".
            shady_vend.purchase('cookie')
            jump shady_vending
        "All out of cookies." if not shady_vend.has('cookie'):
            jump shady_vending
        "Cookie $[shady_vend.costs['cookie']]" if (shady_vend.has('cookie') and not (shady_vend.player_can_afford('cookie')):
            "You are too poor to afford this cookie =(
            jump shady_vending
        "Rusty Sword $[shady_vend.costs['rusty sword']]" if (shady_vend.player_can_afford('rusty sword') and shady_vend.has('rusty sword')):
            "You buy a delicious rusty sword."
            shady_vend.purchase('rusty sword')
            jump shady_vending
        "All out of rusty swords." if not shady_vend.has('rusty sword'):
            jump shady_vending
        "Rusty Sword $[shady_vend.costs['rusty sword']]" if (shady_vend.has('rusty sword') and not (shady_vend.player_can_afford('rusty sword')):
            "You are too poor to afford this rusty sword=(
            jump shady_vending
        "leave":
            "This machine's a little shady"
            jump get_outta_here

Thanks to our convenience code, our smart menu turned out fairly legible: an option is shown if the player can afford it and it is not out of stock. The player can leave the vending machine at any time by choosing the “leave” option.

It will look something like this:

>Cookie $1

>Rusty Sword $5

>leave

Once a player purchases an item, all the inventory and money code is handled generically with the ‘shady_vend.purchase’ method. We then jump the player back to the same menu which will now update with the new inventory.

Next Steps

There’s a lot of potential next steps for this technique. You may want to show the number of items available for purchase in the dialog menu using RenPy’s string substitution [] technique

You may want to take it a step farther and create a method that generates the menu option text dynamically based on if it’s sold out or if the player can afford it. Right now we are still using 3 dialog options for each possible eventuality:

  1. In stock, but the player can’t afford it
  2. Out of stock
  3. In stock, and the player can buy it

By adding in another convenience method, it is possible to condense these down to two functions that return the appropriate dialog and call the appropriate code.

 

Check out our next RenPy article on using music in your game!

Releated Post


Notice: Use of undefined constant ‘mc4wp_show_form’ - assumed '‘mc4wp_show_form’' in /var/www/html/wp-content/themes/eblog-lite/comments.php on line 26

Leave a Reply