Lesson 4.11: Load Item Data From JSON File

We currently define all of our game data in code. We did this for simplicity and to learn the coding concepts without worrying about data management. If we want to add a new GameItem, we need to code that into the ItemFactory. But as we grow our game, we really need to move all of the game data to files that can be updated without making code changes. In the next few lessons, we’re going to redesign our factory classes to load information from data files, rather than define them in code.

First, our file format of choice will be JSON (JavaScript Object Notation). JSON has become the default message format between web applications and services. There’s support in every major platform for reading and writing (serializing) objects into and from JSON format. Since our future plans include getting game data from external web services, JSON data files are a nice step in that direction.

Note: XML is still a viable format. Many older services and libraries still support XML, but since we’re learning new technologies like Blazor and Azure Functions, it makes sense to use the new message format as well. Many of the concepts and code will work for XML serialization as well, only the file reading and conversion would be different.

Data Transfer Objects

The JsonSerializer in .NET likes to directly read and create objects of a particular type. Sometimes there is a mismatch of attributes that we want to save or read, so we don’t always want to expose all of the properties on GameItem in order to support loading the data. And with object composition concepts, like Actions, there may be additional data required or references from other data files. In order to deal with some of these issues, we will use the Data Transfer Object design pattern.

A Data Transfer Object (commonly known as a DTO) is usually an instance of a simple C# class used as a container to encapsulate data and pass data from one layer of an application to another. We would typically find DTOs being used in the service layer to return data back to the presentation layer. The biggest advantage of using DTOs is decoupling clients from our internal data structures. They are also useful in simplifying object serialization without affecting our business object design and game logic.

We will define a simple Data Transfer Object, ItemTemplate, to use to load item data from a JSON file. Let’s create the ItemTemplate class in the SimpleRPG.Game.Engine project and under the new Factories\DTO folder.

using SimpleRPG.Game.Engine.Models;

namespace SimpleRPG.Game.Engine.Factories.DTO
{
    public class ItemTemplate
    {
        public int Id { get; set; }

        public GameItem.ItemCategory Category { get; set; }

        public string Name { get; set; } = string.Empty;

        public int Price { get; set; }

        public string Damage { get; set; } = string.Empty;

        public int Heals { get; set; }
    }
}

As we can see, this is a very simple class that consists only of properties and their public getters and setters. There is no logic in this class at all. Also, this class has some optional attributes that are used to construct particular actions for the item, based on the ItemCategory. For example, weapons need a Damage roll, consumables require a Heals amount, and miscellaneous items don’t use either.

This class simplifies our serialization and matches directly what we will load from our JSON items file.

Next, we need to define the items.json file which we will hold our item data. We will make this data file an embedded resource in our project, so that it builds and publishes side-by-side with our code assemblies.

  1. Create a new folder in SimpleRPG.Game.Engine project called Data.
  2. Right click the folder and pick Add > New Item menu items.
  3. In the New Item dialog select JSON file and name it items.json.
Fig 1 – Add New Json File
  1. Once the file is created in the solution explorer, select the items.json file. Look at its properties. For this file’s Build Action, select the “Embedded resource” option.
Fig 2 – items.json File as Embedded Resource

Then, we will add all of our items and their attributes to this file:

[
  {
    "Id": 1001, "Category": 1, "Name": "Pointy stick", "Price": 1, "Damage": "1d2"
  },
  {
    "Id": 1002, "Category": 1, "Name": "Rusty sword", "Price": 5, "Damage": "1d3"
  },
  {
    "Id": 1501, "Category": 1, "Name": "Snake fang", "Price": 0, "Damage": "1d2"
  },
  {
    "Id": 1502, "Category": 1, "Name": "Rat claw", "Price": 0, "Damage": "1d2"
  },
  {
    "Id": 1503, "Category": 1, "Name": "Spider fang", "Price": 0, "Damage": "1d4"
  },
  {
    "Id": 2001, "Category": 2, "Name": "Granola bar", "Price": 5, "Heals": 2
  },
  {
    "Id": 3001, "Category": 0, "Name": "Oats", "Price": 1
  },
  {
    "Id": 3002, "Category": 0, "Name": "Honey", "Price": 2
  },
  {
    "Id": 3003, "Category": 0, "Name": "Raisins", "Price": 2
  },
  {
    "Id": 9001, "Category": 0, "Name": "Snake fang", "Price": 1
  },
  {
    "Id": 9002, "Category": 0, "Name": "Snakeskin", "Price": 2
  },
  {
    "Id": 9003, "Category": 0, "Name": "Rat tail", "Price": 1
  },
  {
    "Id": 9004, "Category": 0, "Name": "Rat fur", "Price": 2
  },
  {
    "Id": 9005, "Category": 0, "Name": "Spider fang", "Price": 1
  },
  {
    "Id": 9006, "Category": 0, "Name": "Spider silk", "Price": 2
  }
]

As we can see, the JSON format is expressive and easy to read. Each JSON attribute name exactly matches the ItemTemplate property. There are type differences between integers and strings, and how they are defined. And, this JSON file has a list of items which is denoted by the [ ] symbols that start and end the file.

We wrote the JSON more compactly by putting each object’s properties on a single line of text. In more complex objects, we will typically see the text expanded out with each property on its own line. But the JSON parser is flexible in how it can read these files.

Next, we implement the JsonSerializationHelper class and DeserializeResourceStream method in the SimpleRPG.Game.Engine project and Factories folder.

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
using System.Text.Json;

namespace D20Tek.Common.Helpers
{
    public static class JsonSerializationHelper
    {
        /// <summary>
        /// Deserializes the specified type and returns a list of items.
        /// Reads the json file from the assembly's resource manifest.
        /// </summary>
        /// <typeparam name="T">Type of entity to deserialize.</typeparam>
        /// <param name="resourceNamespace">Full namespace path to the resource file.</param>
        /// <returns>List of entities deserialized.</returns>
        public static IList<T> DeserializeResourceStream<T>(string resourceNamespace)
        {
            try
            {
                var assembly = typeof(T).GetTypeInfo().Assembly;
                var resourceStream = assembly.GetManifestResourceStream(resourceNamespace);
                StreamReader reader;
                using (reader = new StreamReader(resourceStream, Encoding.UTF8))
                {
                    var json = reader.ReadToEnd();
                    var elements = JsonSerializer.Deserialize<IList<T>>(json);
                    return elements;
                }
            }
            catch (JsonException)
            {
                throw;
            }
            catch (Exception ex)
            {
                throw new InvalidOperationException(
                    $"Error trying to load embedded resource for: {resourceNamespace}.", ex);
            }
        }
    }
}

The DeserializeResourceStream method is a static helper that encapsulates how embedded resource files are loaded, read into memory, and then converted from a JSON string into the desired object list. We put this code into a helper method because it will be shared by all of our factories to load embedded data files.

  • DeserializeResourceStream<T> is a generic method, so its caller defines the type T that is used and returned by the method. (We will see an example of how this is called shortly.) We use a generic method because the bulk of this code is type agnostic. Only the JsonSerializer and the method return type cares about which class is used.
  • The method takes a parameter to the fully-qualified name to the embedded resource.
  • We use the resourceNamespace in the Assembly.GetManifestResourceStream method. This loads the resource element as a Stream.
  • Then, we read the Stream and get out its data in text format (UTF8 to be precise).
  • Up to this point, we could use this code to read any embedded resource text file (including an XML file).
  • Then we call JsonSerializer.Deserialize<IList<T>> on the JSON text string. This method will return a list of our objects. If the data file only had a single root element defined and not an array, this conversion would fail.
  • If there are JSON parsing and conversion exceptions, we would surface those to the callers.
  • If there are file loading or resource location errors, we wrap those in a generic InvalidOperationException… passing the original exception as our inner exception.

This small snippet of code is pretty powerful in what it does. We use the System.Text.Json implementation of JsonSerializer rather than using external components, like NewtonSoft.Json package. They are comparable in functionality, but NewtonSoft.Json supports more advanced scenarios (feature comparison). But we wanted to learn the .NET Core implementation.

Loading the JSON File in ItemFactory

We already have a creation factory in our code that encapsulates how GameItems are created and retrieved. This layer of abstraction helps us now as we change how these items are created. Rather than being created in code, they will be loaded from file. But their retrieval via the ItemFactory.CreateGameItem method remains the same. And doesn’t affect how other parts of our code use GameItems.

Let’s make these changes to the ItemFactory class:

using D20Tek.Common.Helpers;
using SimpleRPG.Game.Engine.Actions;
using SimpleRPG.Game.Engine.Factories.DTO;
using SimpleRPG.Game.Engine.Models;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.Factories
{
    internal static class ItemFactory
    {
        private const string _resourceNamespace = "SimpleRPG.Game.Engine.Data.items.json";
        private static readonly List<GameItem> _standardGameItems = new List<GameItem>();

        static ItemFactory()
        {
            Load();
        }

        public static GameItem CreateGameItem(int itemTypeID)
        {
            var standardItem = _standardGameItems.First(i => i.ItemTypeID == itemTypeID);

            return standardItem.Clone();
        }

        public static string GetItemName(int itemTypeId)
        {
            return _standardGameItems.FirstOrDefault(i => i.ItemTypeID == itemTypeId)?.Name ?? "";
        }

        private static void Load()
        {
            var templates = JsonSerializationHelper.DeserializeResourceStream<ItemTemplate>(_resourceNamespace);
            foreach (var tmp in templates)
            {
                switch (tmp.Category)
                {
                    case GameItem.ItemCategory.Weapon:
                        BuildWeapon(tmp.Id, tmp.Name, tmp.Price, tmp.Damage);
                        break;
                    case GameItem.ItemCategory.Consumable:
                        BuildHealingItem(tmp.Id, tmp.Name, tmp.Price, tmp.Heals);
                        break;
                    default:
                        BuildMiscellaneousItem(tmp.Id, tmp.Name, tmp.Price);
                        break;
                }
            }
        }

        private static void BuildMiscellaneousItem(int id, string name, int price) =>
            _standardGameItems.Add(new GameItem(id, GameItem.ItemCategory.Miscellaneous, name, price));

        private static void BuildWeapon(int id, string name, int price, string damageDice)
        {
            var weapon = new GameItem(id, GameItem.ItemCategory.Weapon, name, price, true);
            weapon.SetAction(new Attack(weapon, damageDice));
            _standardGameItems.Add(weapon);
        }

        private static void BuildHealingItem(int id, string name, int price, int healPoints)
        {
            GameItem item = new GameItem(id, GameItem.ItemCategory.Consumable, name, price);
            item.SetAction(new Heal(item, healPoints));
            _standardGameItems.Add(item);
        }
    }
}
  • The first thing you will notice is the creation code in the constructor has been deleted, and replaced by a call to the Load method (line #17).
  • Then we define the _resourceNamespace constant (line #12) to be the fully qualified namespace to the items.json file. This includes the namespace to the file concatenated with the filename.
  • The Load method (lines #32-50) starts by calling our JsonSerializationHelper.DeserializeResourceStream method with the _resourceNamespace parameter. And it calls the generic method with the ItemTemplate type.
  • This returns a list of ItemTemplates that were loaded from our embedded resource file.
  • Then, we go through each ItemTemplate and depending on its ItemCategory, we build the appropriate GameItem and Action (if necessary).
  • This code still uses the BuildWeapon, BuildHealingItem, and BuildMiscellaneousItem methods that we already had in this class. Just in our object conversion code, rather than to hard-code each item.
  • Finally, there were changes to use the GameItem.SetAction method (lines #58 & 65) rather than the property setter (we will discuss that refactoring shortly).

The internals of this class radically changed, but its public methods and how other object consume GameItems didn’t have to change at all.

Refactoring GameItem

The code changes we made so far only affected the ItemFactory and some of our test code. But we are also going to take this opportunity to refactor the GameItem class. It is currently too permissive… all of our properties have public setters and a default constructor, but we really don’t want that. We want to force callers to use a GameItem constructor. I left these open earlier because I wasn’t sure which serialization strategy we would use in these lessons. But now that we’re moving forward with DTOs, we can make our logic classes more restrictive, because we don’t have to worry about the JsonSerializer requiring a setter on those properties.

So let’s refactor our GameItem as follows:

using SimpleRPG.Game.Engine.Actions;
using System;

namespace SimpleRPG.Game.Engine.Models
{
    public class GameItem
    {
        public enum ItemCategory
        {
            Miscellaneous,
            Weapon,
            Consumable
        }

        public GameItem(int itemTypeID, ItemCategory category, string name, int price, bool isUnique = false, IAction? action = null)
        {
            ItemTypeID = itemTypeID;
            Category = category;
            Name = name;
            Price = price;
            IsUnique = isUnique;
            Action = action;
        }

        public int ItemTypeID { get; }
        
        public ItemCategory Category { get; }

        public string Name { get; }

        public int Price { get; }

        public bool IsUnique { get; }

        public IAction? Action { get; private set; }

        public virtual GameItem Clone() =>
            new GameItem(ItemTypeID, Category, Name, Price, IsUnique, Action);

        internal void SetAction(IAction? action)
        {
            this.Action = action;
        }

        internal DisplayMessage PerformAction(LivingEntity actor, LivingEntity target)
        {
            if (Action is null)
            {
                throw new InvalidOperationException("CurrentWeapon.Action cannot be null");
            }

            return Action.Execute(actor, target);
        }
    }
}
  • We removed the static property for Empty GameItem.
  • We removed the default constructor for the GameItem.
  • We removed the setters from most of the properties (lines #25-33). Without the setter, these properties are read-only and can only be set in the class’s constructor.
  • We made the Action setter private (line #35). This hides it from other classes, but allows it to be set by this class.
  • Finally, we implemented the SetAction method (lines #40-43), so that when external classes need to set an item’s action, it is an explicit method call.

This code change causes quite a number of breaks in our test code, so we had to clean those all up and get our tests running again.

There was also a couple of ripples of this refactoring in our game engine code. We will remove the use of GameItem.Empty from the GroupedInventoryItem class.

namespace SimpleRPG.Game.Engine.Models
{
    public class GroupedInventoryItem
    {
        public GroupedInventoryItem(GameItem item, int quantity)
        {
            Item = item;
            Quantity = quantity;
        }
        public GameItem Item { get; }

        public int Quantity { get; set; }
    }
}

To address this change, we defined a constructor for the GroupedInventoryItem and removed the Item property setter.

Then there were a couple of changes to the Inventory class to use the GroupedInventoryItem constructor (lines #47 & 53):

using System;
using System.Collections.Generic;
using System.Linq;

namespace SimpleRPG.Game.Engine.Models
{
    public class Inventory
    {
        private readonly List<GameItem> _backingInventory = new List<GameItem>();
        private readonly List<GroupedInventoryItem> _backingGroupedInventory = new List<GroupedInventoryItem>();

        public Inventory(IEnumerable<GameItem> items)
        {
            if (items == null)
            {
                return;
            }

            foreach (GameItem item in items)
            {
                AddItem(item);
            }
        }

        public Inventory()
        {
        }

        public IReadOnlyList<GameItem> Items => _backingInventory.AsReadOnly();

        public IReadOnlyList<GroupedInventoryItem> GroupedItems => _backingGroupedInventory.AsReadOnly();

        public IList<GameItem> Weapons =>
            Items.Where(i => i.Category == GameItem.ItemCategory.Weapon).ToList();

        public List<GameItem> Consumables =>
            Items.Where(i => i.Category == GameItem.ItemCategory.Consumable).ToList();

        public void AddItem(GameItem item)
        {
            _ = item ?? throw new ArgumentNullException(nameof(item));

            _backingInventory.Add(item);

            if (item.IsUnique)
            {
                _backingGroupedInventory.Add(new GroupedInventoryItem(item, 1));
            }
            else
            {
                if (_backingGroupedInventory.All(gi => gi.Item.ItemTypeID != item.ItemTypeID))
                {
                    _backingGroupedInventory.Add(new GroupedInventoryItem(item, 0));
                }

                _backingGroupedInventory.First(gi => gi.Item.ItemTypeID == item.ItemTypeID).Quantity++;
            }
        }

        public void RemoveItem(GameItem item)
        {
            _ = item ?? throw new ArgumentNullException(nameof(item));

            _backingInventory.Remove(item);

            if (item.IsUnique == false)
            {
                GroupedInventoryItem groupedInventoryItemToRemove =
                    _backingGroupedInventory.FirstOrDefault(gi => gi.Item.ItemTypeID == item.ItemTypeID);

                if (groupedInventoryItemToRemove != null)
                {
                    if (groupedInventoryItemToRemove.Quantity == 1)
                    {
                        _backingGroupedInventory.Remove(groupedInventoryItemToRemove);
                    }
                    else
                    {
                        groupedInventoryItemToRemove.Quantity--;
                    }
                }
            }
        }

        public void RemoveItems(IList<ItemQuantity> itemQuantities)
        {
            _ = itemQuantities ?? throw new ArgumentNullException(nameof(itemQuantities));

            foreach (ItemQuantity itemQuantity in itemQuantities)
            {
                for (int i = 0; i < itemQuantity.Quantity; i++)
                {
                    RemoveItem(Items.First(item => item.ItemTypeID == itemQuantity.ItemId));
                }
            }
        }

        public bool HasAllTheseItems(IEnumerable<ItemQuantity> items)
        {
            return items.All(item => Items.Count(i => i.ItemTypeID == item.ItemId) >= item.Quantity);
        }
    }
}

Again there are some breaking changes to our tests, so we need to spend some time cleaning those up and using constructors where we were using object instantiation and properties before. But fixing the build breaks should get us to working tests. And the test behavior hasn’t changed, so they should all pass once the compiler errors are addressed.

Once all of the tests are building and running successfully, we are in a state to run our game again. Everything should work as before, but now our items data is loading from a file rather than defined in our source code.

We could make changes or add items to the embedded resource file and run the game again to see the changes or new items… without any additional coding changes. This makes our game engine more flexible and game data can be added by non-developers. This is a great design separation for our game engine, so we will continue moving hard-coded data into files over the next few lessons.

2 thoughts on “Lesson 4.11: Load Item Data From JSON File

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s