Alright, for my implementation, I wanted to store various item types, this included Weapons, Apparel, Items, GameEvents, LootTables, Abilities, etc. I didnt want to use a large amount of polymorphism, because I consider it to be a weakness in a large system, especially the one that my game has become. As you can imagine, this would make a database mechanism for non-polymorphic items very difficult to create.
What I ended up doing was having a static GameDatabase class, which is initialized at runtime, loaded from a directory, and used throughout the entire game engine without having to pass around references to the Database. The Database consists of one List of each item type, and an associated enumeration, which is combined with the index of the item to create a pseudo bitmask key. The key is a single integer. What this means is that now we can pass around an integer ID for everything in the game, instead of the item itself.
Here is an example of my GameDatabase with the file loading trimmed out. I am also using C# because I absolutely abhor Java.
using System;
using System.Collections.Generic;
using VoidwalkerEngine.Framework.Collections;
using VoidwalkerEngine.Framework.DataTypes;
using VoidwalkerEngine.Framework.Entities;
using VoidwalkerEngine.Framework.Game.Items;
using VoidwalkerEngine.Framework.Rendering.OpenGL.Modeling;
namespace VoidwalkerEngine.Framework.Game.Managers
{
public enum DatabaseLocation
{
Abilities,
Apparel,
Actors,
Classes,
Events,
Items,
LootTables,
LootTablePools,
Models,
Sounds,
StatusEffects,
Weapons
}
public static class GameDatabase
{
public static List<Ability> Abilities;
public static List<Actor> Actors;
public static List<Birthsign> Classes;
public static List<GameEvent> Events;
public static List<GameItem> Items;
public static List<Weapon> Weapons;
public static List<Apparel> Apparel;
public static List<LootTable> LootTables;
public static List<LootTablePool> LootPools;
public static List<VoidwalkerModel> Models;
public static List<GameSound> Sounds;
public static List<StatusEffect> StatusEffects;
public static void Create()
{
Abilities = new List<Ability>();
Actors = new List<Actor>();
Classes = new List<Birthsign>();
Events = new List<GameEvent>();
Items = new List<GameItem>();
Weapons = new List<Weapon>();
Apparel = new List<Apparel>();
LootTables = new List<LootTable>();
LootPools = new List<LootTablePool>();
Models = new List<VoidwalkerModel>();
Sounds = new List<GameSound>();
StatusEffects = new List<StatusEffect>();
}
public static void Initialize(int identifier)
{
Initialize(new DatabaseKey(identifier));
}
/// <summary>
/// Initializes the Database location with a new Object of that type.
/// The identifier for the object is automatically added.
/// </summary>
/// <param name="key"></param>
public static void Initialize(DatabaseKey key)
{
int identifier = key.Identifier;
int index = key.Index;
switch (key.Location)
{
case DatabaseLocation.Abilities:
Abilities[index] = new Ability(identifier);
break;
case DatabaseLocation.Apparel:
Apparel[index] = new Apparel(identifier);
break;
case DatabaseLocation.Actors:
Actors[index] = new Actor(identifier);
break;
case DatabaseLocation.Classes:
Classes[index] = new Birthsign(identifier);
break;
case DatabaseLocation.Events:
Events[index] = new GameEvent(identifier);
break;
case DatabaseLocation.Items:
Items[index] = new GameItem(identifier);
break;
case DatabaseLocation.LootTables:
LootTables[index] = new LootTable(identifier);
break;
case DatabaseLocation.LootTablePools:
LootPools[index] = new LootTablePool(identifier);
break;
case DatabaseLocation.Models:
Models[index] = new VoidwalkerModel(identifier);
break;
case DatabaseLocation.Sounds:
Sounds[index] = new GameSound(identifier);
break;
case DatabaseLocation.StatusEffects:
StatusEffects[index] = new StatusEffect(identifier);
break;
case DatabaseLocation.Weapons:
Weapons[index] = new Weapon(identifier);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public static object Query(int identifier)
{
return Query(new DatabaseKey(identifier));
}
public static object Query(DatabaseKey key)
{
int index = key.Index;
switch (key.Location)
{
case DatabaseLocation.Abilities:
return Abilities[index];
case DatabaseLocation.Apparel:
return Apparel[index];
case DatabaseLocation.Actors:
return Actors[index];
case DatabaseLocation.Classes:
return Classes[index];
case DatabaseLocation.Events:
return Events[index];
case DatabaseLocation.Items:
return Items[index];
case DatabaseLocation.LootTables:
return LootTables[index];
case DatabaseLocation.LootTablePools:
return LootPools[index];
case DatabaseLocation.Models:
return Models[index];
case DatabaseLocation.Sounds:
return Sounds[index];
case DatabaseLocation.StatusEffects:
return StatusEffects[index];
case DatabaseLocation.Weapons:
return Weapons[index];
default:
throw new ArgumentOutOfRangeException();
}
}
}
}
In order to access an item in the database, you need to use this class to create a key for said item:
using System;
using VoidwalkerEngine.Framework.Game.Managers;
namespace VoidwalkerEngine.Framework.DataTypes
{
public struct DatabaseKey : IEquatable<DatabaseKey>
{
#region Fields
/// <summary>
/// The Location within the Database
/// </summary>
public DatabaseLocation Location { get; }
/// <summary>
/// The Index of the Database List
/// </summary>
public int Index { get; }
/// <summary>
/// The Packed Identifier of this DatabaseKey
/// </summary>
public int Identifier
{
get
{
return Pack(Location, Index);
}
}
#endregion
#region Constants
/// <summary>
/// The Masking Multiplier. This is 10,000
/// which means the maximum number of Items,
/// Weapons,Apparel, etc are 9,999 for each
/// Database List. If this number is increased
/// it will also increase the maximum number of items.
/// MaskMultiplier = 100,000 > 99,999 maximum items.
/// </summary>
public const int MaskMultiplier = 10000;
public const int MinimumIndex = 0;
public const int MaximumIndex = MaskMultiplier - 1;
#endregion
#region Constructors
public DatabaseKey(string hexString) : this(Convert.ToInt32(hexString, 16)) { }
public DatabaseKey(int identifier) : this(UnpackLocation(identifier), UnpackIndex(identifier)) { }
public DatabaseKey(DatabaseKey other) : this(other.Location, other.Index) { }
public DatabaseKey(DatabaseLocation location, int index)
{
this.Location = location;
this.Index = index;
}
#endregion
#region Functions
/// <summary>
/// Unpacks the Location from a packed HexCode.
/// </summary>
/// <param name="hexCode"></param>
/// <returns></returns>
public static DatabaseLocation UnpackLocation(int hexCode)
{
return (DatabaseLocation)(hexCode / MaskMultiplier);
}
/// <summary>
/// Unpacks the Index within a packed HexCode.
/// </summary>
/// <param name="hexCode"></param>
/// <returns></returns>
public static int UnpackIndex(int hexCode)
{
return ((hexCode - ((hexCode / MaskMultiplier) * MaskMultiplier)) - 1);
}
/// <summary>
/// Packs a Location and Index into an Identifier.
/// </summary>
/// <param name="location"></param>
/// <param name="index"></param>
/// <returns></returns>
public static int Pack(DatabaseLocation location, int index)
{
return ((((int)location * MaskMultiplier) + index) + 1);
}
#endregion
#region Overrides
public bool Equals(DatabaseKey other)
{
return Identifier == other.Identifier;
}
public override bool Equals(object obj)
{
return obj is DatabaseKey reference && Equals(reference);
}
public override int GetHashCode()
{
return Identifier;
}
public override string ToString()
{
return "0x" + Identifier.ToString("X");
}
#endregion
}
}
The DatabaseKey is declared as an immutable struct. In order to create a key for an item you do something like:
DatabaseKey key = new DatabaseKey(DatabaseLocation.Items,24);
This results in an Integer identifier of: 50025
or in hex: 0xC369
. What that means is that basic game items are located within the range of 50000-59999. So you take 50,000 + array index. This gives us a single integer in which to map items. Passing items around internally would then use an ItemStack implementation; here is mine:
using System;
using VoidwalkerEngine.Framework.Maths;
namespace VoidwalkerEngine.Framework.DataTypes
{
[Serializable]
public class ItemStack
{
/// <summary>
/// The ID of the Associated Item.
/// </summary>
public int Identifier { get; set; }
private int _quantity;
/// <summary>
/// The Quantity of this ItemStack.
/// </summary>
public int Quantity
{
get
{
return _quantity;
}
set
{
this._quantity = VoidwalkerMath.Clamp(value, MinQuantity, MaxQuantity);
}
}
private int _maxQuantity = Int32.MaxValue;
/// <summary>
/// The Maximum Quantity of this Stack.
/// </summary>
public int MaxQuantity
{
get
{
return _maxQuantity;
}
set
{
this._maxQuantity = VoidwalkerMath.Clamp(value, MinQuantity, Int32.MaxValue);
this.Quantity = this.Quantity;
}
}
public const int MinQuantity = 0;
public bool IsStackable
{
get
{
return this.MaxQuantity != 1;
}
}
/// <summary>
///
/// </summary>
public bool IsEmpty
{
get
{
return this.Quantity == MinQuantity;
}
}
/// <summary>
///
/// </summary>
public bool IsFull
{
get
{
return this.Quantity == this.MaxQuantity;
}
}
public ItemStack()
{
}
public ItemStack(int identifier, int quantity = 1, int maxQuantity = Int32.MaxValue)
{
this.Identifier = identifier;
this.Quantity = quantity;
this.MaxQuantity = maxQuantity;
}
/// <summary>
/// Adds the specified quantity to this ItemStack. If
/// the ItemStack is already full or close to being full,
/// this ItemStack will overflow into a new ItemStack as the
/// return value.
/// </summary>
/// <param name="quantity"></param>
/// <returns></returns>
public ItemStack Add(int quantity)
{
if (quantity <= MinQuantity)
{
return null;
}
int overflow = ComputeOverflow(quantity);
if (overflow > MinQuantity)
{
this.Quantity += quantity;
return new ItemStack(this.Identifier, overflow, this.MaxQuantity);
}
this.Quantity += quantity;
return null;
}
/// <summary>
/// Adds the specified quantity to this ItemStack. If
/// the ItemStack is already full or close to being full,
/// this ItemStack will overflow into a new ItemStack as the
/// return value.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public ItemStack Add(ItemStack other)
{
ItemStack stack = Add(other.Quantity);
other.Subtract(other.Quantity);
return stack;
}
/// <summary>
/// Subtracts the specified quantity from this ItemStack. If
/// the ItemStack is already empty or close to being empty,
/// this ItemStack will underflow into a new ItemStack as the
/// return value.
/// </summary>
/// <param name="quantity"></param>
/// <returns></returns>
public ItemStack Subtract(int quantity)
{
if (quantity <= MinQuantity)
{
return null;
}
int underflow = ComputeUnderflow(quantity);
if (underflow > MinQuantity)
{
this.Quantity -= (quantity - underflow);
}
this.Quantity -= quantity;
return new ItemStack(this.Identifier, quantity, this.MaxQuantity);
}
/// <summary>
/// Subtracts the specified quantity from this ItemStack. If
/// the ItemStack is already empty or close to being empty,
/// this ItemStack will underflow into a new ItemStack as the
/// return value.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public ItemStack Subtract(ItemStack other)
{
ItemStack stack = Subtract(other.Quantity);
other.Subtract(stack.Quantity);
return stack;
}
/// <summary>
/// Clears the Quantity of this ItemStack to 0. MaxValue, however, remains the same.
/// </summary>
public void Clear()
{
this._quantity = MinQuantity;
}
/// <summary>
/// Makes the currect Quantity of this ItemStack equal to the MaxValue of this ItemStack.
/// </summary>
public void Fill()
{
this._quantity = MaxQuantity;
}
/// <summary>
/// Splits this ItemStack into another, giving half to both stacks.
/// If the split amount is an odd number, the result gets +1 so no
/// loss of items happens due to rounding errors.
/// </summary>
/// <returns></returns>
public ItemStack Split()
{
if (this.Quantity <= (MinQuantity + 1))
{
return null; // A split is impossible.
}
int splitResult = (this.Quantity / 2);
if (this.Quantity % 2 == 0)
{
this.Quantity = splitResult;
return new ItemStack(this.Identifier, splitResult, this.MaxQuantity);
}
this.Quantity = splitResult;
return new ItemStack(this.Identifier, splitResult + 1, this.MaxQuantity);
}
/// <summary>
///
/// </summary>
/// <param name="other"></param>
public void Copy(ItemStack other)
{
this.Identifier = other.Identifier;
this.Quantity = other.Quantity;
this.MaxQuantity = other.MaxQuantity;
}
/// <summary>
/// Creates a new ItemStack which is an exact copy of this ItemStack.
/// </summary>
/// <returns></returns>
public ItemStack MakeCopy()
{
return new ItemStack(this.Identifier, this.Quantity, this.MaxQuantity);
}
/// <summary>
/// Determines if this ItemStack is stackable with another ItemStack. This function tests
/// for a match between the string Identifiers, and whether or not this ItemStack is Stackable.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool IsStackableWith(ItemStack other)
{
return this.Identifier == other.Identifier && this.IsStackable;
}
/// <summary>
/// Calculates the amount of overflow that will take place
/// if the desired ItemStack is added to this one.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public int ComputeOverflow(ItemStack other)
{
return ComputeOverflow(other.Quantity);
}
/// <summary>
/// Calculates the amount of overflow that will take place
/// if the desired amount is added to this one.
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
public int ComputeOverflow(int amount)
{
if (amount <= MinQuantity)
{
return MinQuantity;
}
int total = ((this.Quantity + amount) - this.MaxQuantity);
if (total < MinQuantity)
{
return MinQuantity;
}
return total;
}
/// <summary>
/// Calculates the amount of underflow that will take place
/// if the desired ItemStack is subtracted from this one.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public int ComputeUnderflow(ItemStack other)
{
return ComputeUnderflow(other.Quantity);
}
/// <summary>
/// Calculates the amount of underflow that will take place
/// if the desired amount is subtracted from this one.
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
public int ComputeUnderflow(int amount)
{
if (amount <= MinQuantity)
{
return MinQuantity;
}
int total = (this.Quantity - amount);
if (total > MinQuantity)
{
return MinQuantity;
}
return Math.Abs(total);
}
/// <summary>
/// Determines if this ItemStack has the specified quantity.
/// If the quantity is less than or equal to 0, this function
/// will always return false.
/// </summary>
/// <param name="quantity"></param>
/// <returns></returns>
public bool HasQuantity(int quantity)
{
if (quantity <= MinQuantity)
{
return false;
}
return this.Quantity >= quantity;
}
/// <summary>
/// Determines if this ItemStack can still fit the specified amount
/// without overflowing into a new ItemStack. If the quantity is less
/// than or equal to 0, this function will always return false.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool CanHold(ItemStack other)
{
return CanHold(other.Quantity);
}
/// <summary>
/// Determines if this ItemStack can still fit the specified amount
/// without overflowing into a new ItemStack. If the quantity is less
/// than or equal to 0, this function will always return false.
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
public bool CanHold(int amount)
{
if (amount <= MinQuantity)
{
return false;
}
return this.Quantity + amount <= MaxQuantity;
}
/// <summary>
/// Determines if this ItemStack can subtract the specified amount
/// without underflowing into a new ItemStack. If the quantity is less
/// than or equal to 0, this function will always return false.
/// </summary>
/// <param name="other"></param>
/// <returns></returns>
public bool CanSubtract(ItemStack other)
{
return CanSubtract(other.Quantity);
}
/// <summary>
/// Determines if this ItemStack can subtract the specified amount
/// without underflowing into a new ItemStack. If the quantity is less
/// than or equal to 0, this function will always return false.
/// </summary>
/// <param name="amount"></param>
/// <returns></returns>
public bool CanSubtract(int amount)
{
if (amount <= MinQuantity)
{
return false;
}
return this.Quantity - amount >= MinQuantity;
}
public bool Equals(ItemStack other)
{
if (other != null)
{
return
this.Identifier == other.Identifier &&
this.Quantity == other.Quantity &&
this.MaxQuantity == other.MaxQuantity;
}
return false;
}
public override string ToString()
{
return $"{this.Identifier},{this.Quantity},{this.MaxQuantity}";
}
}
}
For more information on this ItemStack implementation, please see another answer I posted here: https://gamedev.stackexchange.com/questions/133000/how-do-inventories-work/133024
We do not pass around items. We pass around the integer ID's. This means we don't need to do any fancy polymorphism at all. When you need to get an item from the database, you just feed in the ID, use the location to cast it to the correct item(or other things that can achieve the cast on a dynamic level) and there you have it. This system also allows dynamic item generation. All you have to do is add another database List for Procedural Weapons, or Apparel. In the game when you augment an item, you're really just creating a new item, adding it to a list in the Database, removing the associated ItemStack from the players inventory, and then adding the new ItemStack with the new generated ID, which points to the created item in the database.
And finally, I believe a picture is worth a thousand words, so here is a screenshot of my engine using the Database system I have mentioned:

It should also be noted that this system scales very well with SQLite databases for actual storage and retrieval later down the road in your development. Essentially, you can rewrite the Database class that I have shown you to be a Query-Cache facade. So you first test to see if the item has been queried from the SQLite file already, and if it has, you just return the copy that you have already queried, else you query the item, cache it in the in-memory database, and then return it. This can greatly speed up your performance. The internal SQLite database is also very good at handling your custom integer indexes. All you would need to do for that is have a table in the SQLite file for each item type, then use the DatabaseKey struct to find out what type of item you're after, then use that to select the table from the SQLite file.
If you have any questions, I would be happy to answer them.