I’m just hanging out tonight. So I figured I’d come back and give you a general example of something that might do what you’re asking for. Or rather… something along the lines of what I’d do.
I’m going to preface that all code here is written slap dash, not in an IDE, and so therefore likely contains spelling mistakes or other minor bugs. Treat this as pseudo-code.
First off I personally separate my inventory items into a few parts.
ItemDescriptor
public class ItemDescriptor : ScriptableObject
{
public string Id; //I actually use a guid for this and tether it to the assetguid, but that's outside the scope of this discussion
public string DisplayName;
public string Description;
public int Rarity;
//add any other fields that are related to all items regardless of type?
[SerializeReference] //you're going to need to write a custom editor for this...
[SerializeRefPicker] //I already have my own custom property drawer for it...
public IItemSlotState StateTemplate;
public virtual IItemSlotState CreateState()
{
this.StateTemplate.ItemDescriptor = this;
return this.StateTemplate.Clone();
}
public virtual ItemAvatar CreateItemAvatar(IItemSlotState state, Vector3 pos, Quaternion rot, Transform parent = null)
{
var obj = UnityEngine.Object.Instantiate(AvatarPrefab, pos, rot, parent);
obj.Initialize(state);
return obj;
}
}
ItemDescriptor would be a ScriptableObject that acts as the source information about items. This can contain all the information about the item that is generalized. Id, name, description, other info unique to your game, and… a state template. This is going to be our next part.
Item State
public interface IItemSlotState
{
ItemDescriptor ItemDescriptor { get; }
bool Stackable { get; }
IItemSlotState Clone();
ItemAvatar CreateAvatar(Entity owner, Vector3 pos, Quaternion rot, Transform parent = null);
bool AttemptStack(Entity owner, IItemSlotState other);
}
[System.Serializable]
public class ItemSlotState : IItemSlotState
{
public ItemAvatar AvatarPrefab;
public ItemDescriptor ItemDescriptor { get; set; }
public virtual bool Stackable => false;
public virtual IItemSlotState Clone() => this.MemberwiseClone() as IItemSlotState;
public virtual ItemAvatar CreateAvatar(Entity owner, Vector3 pos, Quaternion rot, Transform parent = null) => UnityEngine.Object.Instantiate(AvatarPrefab, pos, rot, parent).Initialize(owner, this);
public virtual bool AttemptStack(Entity owner, IItemSlotState other) => false;
}
[System.Serializable]
public class StackableItemSlotState : ItemSlotState
{
public int Count;
public override bool Stackable => true;
public override bool AttemptStack(Entity owner, IItemSlotState other)
{
if (other.ItemDescriptor.Id != this.ItemDescriptor.Id) return false;
this.Count += (other as StackableItemSlotState)?.Count ?? 0;
}
}
[System.Serializable]
public class WeaponItemSlotState : IItemSlotState
{
public WeaponAvatar AvatarPrefab;
public AmmoType AmmoType;
public int AmmoCount;
public int MaxCount;
public ItemDescriptor ItemDescriptor { get; set; }
public virtual bool Stackable => false;
public virtual IItemSlotState Clone() => this.MemberwiseClone() as IItemSlotState;
public virtual ItemAvatar CreateAvatar(Entity owner, Vector3 pos, Quaternion rot, Transform parent = null) => UnityEngine.Object.Instantiate(AvatarPrefab, pos, rot, parent).Initialize(owner, this);
public virtual bool AttemptStack(Entity owner, IItemSlotState other)
{
if (other.ItemDescriptor.Id != this.ItemDescriptor.Id) return false;
var ammopouch = owner.GetComponent<AmmoPouch>();
ammopouch.AddAmmo(this.AmmoType, (other as WeaponItemSlotState)?.AmmoCount ?? 0);
}
}
The state is what represents the items in game. It’s sort of like your ‘InventorySlot’, though it serves more than just the slot information. I still maintained the name ‘slot’ in my example here to emphasize that relationship. Though really I probably would have named it ‘IItemState’.
Note that I have various implementations of the state for my different item types. And our ItemDescriptor at the top had a special property drawer for configuring out ItemDescriptor as the appropriate item type. When doing so the item type specific fields would become editable to the ScriptableObject for things like the ammotype and avatarprefab. Which leads us to the next thing.
Item Avatar
public class ItemAvatar : MonoBehaviour
{
public IItemSlotState State { get; protected set; }
public virtual ItemAvatar Initialize(Entity owner, IItemSlotState state)
{
this.State = state;
//do other initializing???
}
}
public class WeaponAvatar : ItemAvatar
{
private WeaponItemSlotState _weaponState;
public override void Initialize(Entity owner, IItemSlotState state)
{
if (state == null) throw new System.ArgumentNullException(nameof(state));
if (!(state is WeaponItemSlotState)) throw new System.ArgumentException($"State for '{state.ItemDescriptor.DisplayName}' was malformed, confirm the ItemDescriptor is configured correctly.", nameof(state));
base.Initialize(state);
_weaponState = state as WeaponItemSlotState;
}
public bool FireWeapon(Entity owner)
{
if (_weaponState.AmmoCount <= 0) return false;
var ammopouch = owner.GetComponent<AmmoPouch>();
ammopouch.UseAmmo(_weaponState.AmmoType, 1);
_weaponState.AmmoCount--;
//perform firing action
return true;
}
public bool Reload(Entity owner)
{
var ammopouch = owner.GetComponent<AmmoPouch>();
_weaponState.AmmoCount = Mathf.Min(ammopouch.GetAmmoCount(_weaponState.AmmoType), _weaponState.MaxCount);
//TODO - signal reload anim???
}
}
Item Avatar is the visual representation of our object in the world. It’s the script attached to the prefab for the item that represents it. Say you removed the item from inventory and wanted to drop it on the ground… you might call CreateAvatar and just place it on the ground.
Say it’s a weapon. You might call CreateAvatar, parent it in the player’s hand, and then use it accordingly.
I sort of made the Fire/Reload methods part of the WeaponAvatar. Honestly… I’d probably compose that into its own distinct script without using inheritance like this. But I used inheritance just for brevity sake in this post. Again… this is more like pseudo-code to demonstrate the dependency inversion and structural ideas.
Then finally we’d have things like your ItemPouch:
public class ItemPouch
{
private List<IItemSlotState> _items = new List<IItemSlotState>();
private IReadOnlyList<IItemSlotState> Items => _items;
public void AddItem(ItemDescriptor item) => AddItem(item.CreateState());
public void AddItem(IItemSlotState state)
{
if (_items.Contains(state)) return;
if (state.Stackable)
{
foreach (var item in _items)
{
if (item.ItemDescriptor.Id == state.ItemDescriptor.Id &&
item.AttemptStack(state))
{
return; //the item was consumed by another stack, just exit now
}
}
}
_items.Add(state);
}
public bool RemoveItem(IItemSlotState state)
{
return _items.Remove(state);
}
public IItemSlotState FindItem(string id)
{
foreach (var item in _items)
{
if (item.ItemDescriptor.Id == id) return item;
}
return null;
}
}
And that’s honestly the main gist of it.
You may notice I still use the ‘as’ in some place. Because honestly… it’s useful. A creationary pattern like this may have to validate its creationary steps at times. Sure could I have made it more strict… but the code would become even longer. You don’t need to avoid ‘as’ like the plague though, it’s useful in place.
But if I were to… I’d just shim in another interface like so:
public interface IItemAvatar
{
IItemSlotState State { get; }
GameObject gameObject { get; }
}
public class ItemAvatar : MonoBehaviour, IItemAvatar
{
public IItemSlotState State { get; protected set; }
public virtual ItemAvatar Initialize(Entity owner, IItemSlotState state)
{
this.State = state;
//do other initializing???
}
}
public class WeaponAvatar : MonoBehaviour, IItemAvatar
{
private WeaponItemSlotState _weaponState;
public WeaponItemSlotState State => _weaponState;
IItemSlotState IItemAvatar.State => _weaponState;
public virtual void Initialize(Entity owner, WeaponItemSlotState state)
{
if (state == null) throw new System.ArgumentNullException(nameof(state));
_weaponState = state;
}
public bool FireWeapon(Entity owner)
{
if (_weaponState.AmmoCount <= 0) return false;
var ammopouch = owner.GetComponent<AmmoPouch>();
ammopouch.UseAmmo(_weaponState.AmmoType, 1);
_weaponState.AmmoCount--;
//perform firing action
return true;
}
public bool Reload(Entity owner)
{
var ammopouch = owner.GetComponent<AmmoPouch>();
_weaponState.AmmoCount = Mathf.Min(ammopouch.GetAmmoCount(_weaponState.AmmoType), _weaponState.MaxCount);
//TODO - signal reload anim???
}
}
Of course now all the ‘CreateAvatar’ methods would need to return IItemAvatar rather than ItemAvatar. Also now we have a component represented as an interface and any generalize properties for the interface need to be duplicated between the avatar types all to avoid an ‘as’ operator. But, it’s easily doable, and honestly might even be the way I would do it in the end. But I have various tools that glue interface and Unity together since its direct support for interface components is only half-way there IMO (for example, you can’t easily have a serialized ref to a component by interface type).