Inventory System
Given the small size of the game and the amount of items considered (a few keys), it makes sense that an inventory system should be very simple. I created an inventory system based solely around the storage of generic items that can be added without code.
Essentially a list of item names and counts, the system dynamically creates entries at the moment of use. This means that an item can be defined anywhere in the game and still be stored/retrieved on use. However, this has a major caveat due to the nature of the system, typos. Storing a "tedkey" will add "tedkey:1" and asking for a "redkey" will return "redkey:0", this could introduce bugs that can only be found with extra play testing.
Inventory
The Inventory
is a serializable wrapper class around a List<InventoryItem>
that handles adding and removing from the collection. All interaction methods query the collection for an existing InventoryItem
and will create a new instance if one does not exist. This means that merely querying the system for "OrangeShoe" for example will create an "OrangeShoe" with a count of 0 in the container.
[Serializable]
public partial class Inventory {
[SerializeField, ReadOnly]
private List<InventoryItem> _inventoryItems;
public Inventory() {
_inventoryItems = new List<InventoryItem>();
}
/// <summary>
/// Add an item to the inventory
/// </summary>
/// <param name="item"></param>
/// <returns>true if an item was added</returns>
public bool AddItem(Item item) {
return AddItems(item, 1) > 0;
}
/// <summary>
/// Adds an amount of an item
/// </summary>
/// <param name="item">The item to add</param>
/// <param name="amount">The amount of Items to add</param>
/// <returns>The amount of items added</returns>
public int AddItems(Item item, int amount) {
if (amount < 0) {
Debug.Log("Cannot add a negative amount of items");
}
return GetInventoryItem(item)
.Add(amount);
}
/// <summary>
/// Removes one item from the inventory and passes it to the dropMethod
/// </summary>
/// <param name="dropMethod">Method to pass the dropped item to</param>
/// <param name="item">Item matching the item to remove</param>
/// <returns>True if an item was removed</returns>
public bool DropItem(Action<Item, int> dropMethod, Item item) {
return DropItems(dropMethod, item, 1) > 0;
}
/// <summary>
/// Drop an item
/// </summary>
/// <param name="dropMethod">Action to invoke with dropped item</param>
/// <param name="item">Item to drop</param>
/// <param name="amount">Number of items to drop</param>
/// <returns></returns>
public int DropItems(Action<Item, int> dropMethod, Item item, int amount) {
var dropped = RemoveItems(item, amount);
if (dropMethod != null) {
dropMethod.Invoke(item, dropped);
}
return dropped;
}
/// <summary>
/// Gets the amount of Items the inventory holds
/// </summary>
/// <param name="item">Item to count</param>
/// <returns>The amount of items</returns>
public int GetItemCount(Item item) {
return GetInventoryItem(item)
.Count;
}
/// <summary>
/// Removes an item from the Inventory.
/// </summary>
/// <param name="item">Item matching the item to remove</param>
/// <returns>True if an item was removed</returns>
public bool RemoveItem(Item item) {
return RemoveItems(item, 1) > 0;
}
/// <summary>
/// Removes an item from the Inventory.
/// </summary>
/// <param name="item">Item matching the item to remove</param>
/// <param name="amount">The amount of items to remove</param>
/// <returns>True if an item was removed</returns>
public int RemoveItems(Item item, int amount) {
if (amount < 0) {
Debug.Log("Cannot remove a negative amount of items");
}
return GetInventoryItem(item)
.Remove(amount);
}
/// <summary>
/// Gets or creates an Inventory item for the Item
/// </summary>
/// <param name="item">The Item to get</param>
/// <returns>An InventoryItem that exists in the collection</returns>
private InventoryItem GetInventoryItem(Item item) {
if (HasItem(item)) {
return _inventoryItems.First(inventoryItem => inventoryItem.Equals(item));
}
var i = new InventoryItem(item, 0);
_inventoryItems.Add(i);
return i;
}
/// <summary>
/// Checks if the inventory contains the item
/// </summary>
/// <param name="item">The item to check for</param>
/// <returns>true if it exists</returns>
private bool HasItem(Item item) {
foreach (var x in _inventoryItems) {
if (x.Item == item) {
return true;
}
}
return false;
}
}
Inventory.Item
An InventoryItem
is another simple serializable class joining the item name with a counter. It controls access to its Count
property with Add/Remove methods and it implements IEquatable<Item>
to allow the use of equality comparisons between itself and the Item
class inventoryItem1 == item2
.
public partial class Inventory {
/// <summary>
/// A single Item type in the inventory
/// </summary>
[Serializable]
public class InventoryItem : IEquatable<Item> {
[SerializeField]
public Item Item;
[SerializeField]
private int _count;
public InventoryItem(Item item, int count) {
Item = item;
Count = count;
}
/// <summary>
/// The amount of items stored
/// </summary>
public int Count {
get { return _count; }
private set {
_count = value;
if (Count < 0) {
_count = 0;
}
}
}
/// <summary>
/// Add an amount of items
/// </summary>
/// <param name="amount">An amount of Items to add</param>
/// <returns>the amount that was added</returns>
public int Add(int amount) {
// TODO: Add max Item cap in Item class
var min = Mathf.Min(amount + Count, int.MaxValue);
var added = min - Count;
Count += amount;
return added;
}
public bool Equals(Item other) {
return Item.Equals(other);
}
/// <summary>
/// Remove an amount of items
/// </summary>
/// <param name="amount">An amount of items to remove</param>
/// <returns>The amount removed</returns>
public int Remove(int amount) {
var removable = Mathf.Min(amount, Count);
Count -= amount;
return removable;
}
}
}
Item
Item
is the most basic representation of a thing, while similar behaviour could be achieved with a simple string, this yields advantages if functionality additions need to be made to the base item. The class implements IEquatable<Item>
so that we can do item1 == item2
and get the desired result, items with the same name are the same item.
/// <summary>
/// Basic representation of an item
/// </summary>
[Serializable]
public class Item : IEquatable<Item> {
/// <summary>
/// Name/Identifier of the item
/// </summary>
[SerializeField]
public string Name;
public Item(string name) {
Name = name;
}
public static bool operator ==(Item i1, Item i2) {
if (ReferenceEquals(i1, null) ? ReferenceEquals(i2, null) : i1.Equals(i2)) {
return true;
}
return false;
}
public static bool operator !=(Item i1, Item i2) {
return !(i1 == i2);
}
/// <inheritdoc cref="IEquatable{T}.Equals(T)" />
public override bool Equals(object obj) {
if (ReferenceEquals(null, obj)) {
return false;
}
if (ReferenceEquals(this, obj)) {
return true;
}
if (obj.GetType() != GetType()) {
return false;
}
return Equals((Item) obj);
}
/// <inheritdoc cref="IEquatable{T}.Equals(T)" />
public bool Equals(Item other) {
return (other != null) && (Name == other.Name);
}
/// <inheritdoc cref="object.GetHashCode" />
public override int GetHashCode() {
return Name != null ? Name.GetHashCode() : 0;
}
}
Base Actor
An inventory wouldnt be complete without a class that uses it, the Actor
class is a MonoBehaviour
container/wrapper for an the inventory system. The player inherits from this class, however, an alternate implementation is possible using the component model in which Actor
is attached to and manipulated by a script on a GameObject
/// <summary>
/// Base Actor
/// </summary>
public class Actor : MonoBehaviour {
public Inventory Inventory;
/// <summary>
/// Give the Actor an item
/// </summary>
/// <param name="item">Item to give</param>
/// <returns>true if an item was added</returns>
public bool GiveItem(Item item) {
return Inventory.AddItem(item);
}
/// <summary>
/// Remove an Item from the Actor
/// </summary>
/// <param name="item">Item to remove</param>
/// <returns>true if an item was removed</returns>
public bool RemoveItem(Item item) {
return Inventory.RemoveItem(item);
}
}