GDE Example: Shops

Posted September 10, 2015

Today I’m going to use GDE to manage Shops. I’ll show you how to manage a shop’s inventory and how to custimize shop inventory regeneration rates. If you would like to use these Schemas, download the Data File and import it into GDE or download and import the Google Spreadsheet.

Let’s say my game has Potions for sale (not the In App Purchase kind, the NPC kind). As you go through this example, keep in mind, this concept can apply to any item you sell in a shop. The ideas I’ll lay out can work for Power Ups, Weapons, Keys, even unlockable Characters.

I’ve created a simple schema called Potion. It has a name, and the amount of hit points it grants when consumed.

Shop Data

A shop needs to know what it sells (Potions). My shop owners (my fictitious NPC shop owners) have different ideas on how to do business, so they each have different buy back rates. The shops that sell really rare potions, acquire them more slowly. Their inventories don’t replenish as quickly as a shop that sells common potions. Oh, and some of my shop owners run sales.

Given all that, this is what my shop data looks like:

Stock Data

In the Shop inventory list, I could have directly linked Potions, but I wanted to add some meta data for each shop’s inventory. This allowed me to define the max of each Potion I can have available, track how many potions the shop currently has, and the cost.

In the Stock schema, I associated the Potion it is representing in the data field. When I load my shop’s data, its Stock data will be loaded, as well as the Potion data. When a Potion is purchased, managing the shop’s inventory can be done like this:

GDEPotionData Purchase(GDEShopData shop, int potion_index)
{
  GDEPotionData purchasedPotion = null;
  Stock potionStock = shop.inventory[potion_index];

  // First make sure we have this Potion in stock
  if (potionStock.num_available > 0)
  {
    // If we do, record the purchase
    potionStock.num_available -= 1;
    purchasedPotion = potionStock.data;
  }
  else
    purchasedPotion = null;

  return purchasedPotion;
}

I set up my purchase method to return the data of the Potion purchased. The calling method could then take that Potion and add it to the character’s personal inventory. Also, remember basic (non-list) properties automatically save. The next time a character enters this shop, the available inventory will remain depleted until replenished.

Replenishing Stock

My shops define a restock rate. To replenish my shop’s stock, I can create a small script that will iterate over all my shops and replenish their inventory according to their individual restock rates.

List<GDEShopData> shops;
void Update()
{
  // Assume I loaded the shops at startup
  for(int x=0;  x<shops.Count;  x++)
  {
    // Calculate the replenish rate using the shops restock rate
    float restockAmt = Time.deltaTime * shops[x].restock_rate;
    for(int i=0;  i<shops[x].inventory.Count;  i++)
    {
      // First increase the number available
      GDEStockData curStock = shops[x].inventory[i];
      curStock.num_available += restockAmt;

      // Cap the availability to the max amount
      curStock.num_available = Mathf.Min(curStock.num_available, curStock.max);
    }
  }
}

This will replenish all the shop’s stock at the rate the shop defines. If I want more fine grained stock regeneration, I can move the regeneration rate from the Shop schema, to the Stock schema. This enables me to control each Potion’s regeneration rate no matter which shop it appears. For example, super rare Potions can be made to regenerate very slowly, and common Potions can be made to regenerate quickly. Instead of using the restock_rate from the shop, the replenish calculation would use the restock_rate defined in the Stock.

List<GDEShopData> shops;
void Update()
{
  // Assume I loaded the shops at startup
  for(int x=0;  x<shops.Count;  x++)
  {
    for(int i=0;  i<shops[x].inventory.Count;  i++)
    {
      // Calculate the current Stock's replenish rate
      GDEStockData curStock = shops[x].inventory[i];
      float restockAmt = Time.deltaTime * curStock.restock_rate;

      // Then increase the number available
      curStock.num_available += restockAmt;

      // Cap the availability to the max amount
      curStock.num_available = Mathf.Min(curStock.num_available, curStock.max);
    }
  }
}

Buy Back Rates and Sales

Buy back rates and sales calculations are similar to the replenishing stock example above. The data hierarchy for this game can be thought of like this:

Game
  - Shop
     - Stock

Rates defined lower in the hierarchy allow for greater custimization. For example, a buy back rate defined in Shop will apply to all of that shop’s stock. If you move the buy back rate to Stock, then each Stock item controls its own buy back rate - giving you more control over buy back rates. The same idea applies to sale rates.

Here’s the fun part (and I’ll try not to blow your mind). My shop owners like to run huge sales. Everything 50% off, while supplies last!! To do this, I’ll define a sale rate in my Shop schema. In this case, calculating the cost of any Potion in my shop looks like this:

int GetCost(GDEShopData shop, int potion_index)
{
  Stock potionStock = shop.inventory[potion_index];

  // Use the shop sale rate to determine the cost
  // 0 is free, 1 is full cost, anything in between means
  // a sale is active
  return shop.sale_rate * potionStock.cost;
}

However, my shop owners also like to run sales on particular items. I could define a sale rate on Shop AND Stock schemas. When I calculate the current cost I can override the shop’s rate with the potion’s rate.

In my example, to indicate a sale, I define a float between 0 and 1.

  • 0 means Free
  • 1 is Full Cost (no sale)
  • 75% off is 0.25 (75% off leaves 25% of the cost, i.e. 1-0.75)

If a stock’s sale rate is equal to 1, I’ll use the shop’s sale rate instead. If the shop’s sale rate is 1, then there is no sale and the potion will be listed at full price. Here’s the updated cost method:

int GetCost(GDEShopData shop, int potion_index)
{
  Stock potionStock = shop.inventory[potion_index];

  // Start with the shop's sale rate
  float sale_rate = shop.sale_rate;

  // Determine if the Stock has overriden the shop's sale rate
  // If so, use that instead
  if (potionStock.sale_rate < 1)
    sale_rate = potionStock.sale_rate;

  return sale_rate * potionStock.cost;
}

The same logic can be used for buy back rates. Just replace any reference to sale_rate with buy_back_rate. I’ll show you:

int GetBuyBackAmount(GDEShopData shop, int potion_index)
{
  Stock potionStock = shop.inventory[potion_index];

  // Start with the shop's sale rate
  float buy_back_rate = shop.buy_back_rate;

  // Determine if the Stock has overriden
  // the shop's buy back rate
  // If so, use that instead
  if (potionStock.buy_back_rate < 1)
    buy_back_rate = potionStock.buy_back_rate;

  return buy_back_rate * potionStock.cost;
}

These examples illustrate one way to organize game data. Your game might require more Schemas and/or different structures. My goal is to get your creative juices flowing. Feel free to download this Data File and modify it to fit your needs. Or download and import the Google Spreadsheet.