Inventory System - What ya'll think?

Decided to rewrite my inventory for something bit more modular. I just wanted to share what I had and if anyone had input on it and possibly overlook and make sure I’m not over complicating things. Been away from programming for very long time so I just want to reassure myself. Thanks for looking at it.

Note: This just sample setup and not full code.

public enum EWeaponType{
empty,
melee,
gun
}

Base Class for Item:

using UnityEngine;
using System.Collections;

public class Item {

    public int id {
        get;
        set;
    }
    public string name {
        get;
        set;
    }
    public string description {
        get;
        set;
    }

    public virtual void Update(){
    }

    public virtual void Equip(){
    }

    public virtual void UnEquip(){
    }

    public virtual void Use(){

    }
    public virtual void Add(){

    }
    public virtual void Drop(){

    }
}

Weapon Class Inherits Item:

using UnityEngine;
using System.Collections;

public class Weapon : Item {

    public EWeaponType weaponType {
        get;
        set;
    }

    public virtual bool IsSemiAuto(){
        return false;
    }
}

Gun Class Inherits Weapon:

using UnityEngine;
using System.Collections;

public class Gun : Weapon{

    public virtual void Reload(){
   
    }
}

This is the unarmed class I created to fill the inventory default. Most of the comments in it are just to kinda give you an example of what you would do or could do. I haven’t written any of the code for unarmed so i used some samples from my shotgun class to fill the comments. Sorry if it brings confusion.

using UnityEngine;
using System.Collections;

public class Unarmed : Weapon
{
    public int id;
    public string name = "Unarmed";
    public string description = "Unarmed";

    public override void Update ()
    {
        /*This is called every frame from the inventory system
        Example if this was gun:
        if (!isReloading && shootTimer > 0) {
        shootTimer -= Time.deltaTime;
        }
        */
    }

    public override void Add ()
    {
        /*This is called when a item(weapon) is added to the inventory
        For example if this was a gun
        isReloading = false;
        currentAmmo = clipSize;
        shootTimer = 0;
        playerController = GameObject.FindGameObjectWithTag("Player").GetComponent<PlayerController> ();
        shotgunMesh = GameObject.FindGameObjectWithTag("ShotgunMesh");
        */
    }

    public override void Equip ()
    {
        /* This is called when an item is swap to become the active item(weapaon)
        You can do things like:
        Enable Shotgun mesh or assign new animator controller to arms, character or whatever needs to be done to show the new weapon on screen.
        */
    }

    public override void UnEquip ()
    {
        //This is the opposite of above "Equip()". This is where you disable anything you enabled above.
    }

    public override void Use ()
    {
        /* This is where all the fun happens. This can be anything custom code you wish.
           This is some sample code from my shotgun class:
      
        if (!isReloading && currentAmmo > 0 && shootTimer <= 0) {
            animationHandler.Fire ();
            currentAmmo--;
            shootTimer += 1 / fireRate;
            SpawnBullet ();  <--this is a function only the shotgun class has. Or you can create virtual function inside the gun class. Which I have already done.
            audioSource.PlayOneShot (ContentLoader.Instance.GetAudio(shootSound));
        }else if(currentAmmo <= 0 && shootTimer <= 0){
            audioSource.PlayOneShot (ContentLoader.Instance.GetAudio(emptyClickSound));
        }
   
        */
    }

    public override void Drop ()
    {
        //Here you can write some code like raycast to the floor and spawn shotgun object.
    }
}

Inventory Class:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class Inventory : MonoBehaviour {

    private List<Item> weaponSlots;

    private int maxWeaponSlots = 3;

    private int activeWeapon;

    private int activeMenuWeaponSlot;

    float lastTriggerValue;
    bool triggerPushed;
    string triggerAxis = "Trigger";

    // Use this for initialization
    void Start () {
        weaponSlots = new List<Item> ();
        //Initialize Lists
        //Weapons
        for(int i = 0;i < maxWeaponSlots;i++){
            weaponSlots.Add(new Unarmed());
        }
    }

    // Update is called once per frame
    void Update () {
        if(Input.GetButtonUp("wSlot1")){
            activeWeapon = 0;
            SwapWeapon (activeWeapon);
        }
        if(Input.GetButtonUp("wSlot2")){
            activeWeapon = 1;
            SwapWeapon (activeWeapon);
        }
        if(Input.GetButtonUp("wSlot3")){
            activeWeapon = 2;
            SwapWeapon (activeWeapon);
        }

        triggerPushed = Input.GetAxis(triggerAxis) > lastTriggerValue;
        lastTriggerValue = Input.GetAxis(triggerAxis);

        Weapon weapon = (Weapon)weaponSlots [activeWeapon];

        if ((Input.GetAxis(triggerAxis) > 0))
        {
            if (!weapon.IsSemiAuto())
            {
                if (triggerPushed)
                {
                    WeaponUse ();
                }
            }
            else
            {
                WeaponUse ();
            }
        }
        weaponSlots [activeWeapon].Update ();
    }
   
    public void SwapWeapon(int slot){
        WeaponUnEquip ();
        if (weaponSlots [slot].name == "Unarmed") {
            activeWeapon = 0;
        }else{
            activeWeapon = slot;
        }
        WeaponEquip ();
    }
   
    public bool AddWeapon(Weapon weapon){
        for(int i = 1;i < maxWeaponSlots;i++){
            if (weaponSlots [i].name == "Unarmed") {
                weaponSlots [i] = weapon;
                return true;
            }
        }
        return false;
    }
   
    public void WeaponReload(){
        Weapon weapon = (Weapon)weaponSlots [activeWeapon];
        if (weapon.weaponType = EWeaponType.gun) {
            Gun gun = (Gun)weapon;
            gun.Reload ();

        }
    }

    public void WeaponUse(){
        weaponSlots [activeWeapon].Use ();
    }

    public void WeaponEquip(){
        weaponSlots [activeWeapon].Equip ();
    }

    public void WeaponUnEquip(){
        weaponSlots [activeWeapon].UnEquip();
    }
   
    private void DropWeapon (int slot){
        weaponSlots [activeMenuWeaponSlot].Drop();
        weaponSlots [slot] = new Unarmed();
    }
}

Why are you programming objects instead of behaviours? Unity3D promotes an entity-component design. Every entity(GameObject) in your scene should be the sum of its attached behavioural components. A simple example taking your “Item” class would be to separate it into multiple separate behaviour scripts that handle equipping/unequipping, stacking/unstacking, and dropping the item.

Some pseudo code below… Note You should use generic interfaces to implement specific types on the function parameters and/or an abstract class with base functionality so you can re-use the base behaviour functions.

public class ItemEquippableBehaviour : MonoBehaviour
{
   public virtual void OnEquip() {}
   public virtual void OnUnequip() {}
}
public class ItemStackableBehaviour : MonoBehaviour
{
   public virtual void OnStack() {}
   public virtual void OnUnstack() {}
}
public class ItemDroppableBehaviour : MonoBehaviour
{
   public virtual void OnDrop() {}
}
1 Like

For one its a limitation of C# while inheriting, well not really as I could have used interfaces. Also why not? I mean I don’t require anything from behavior. I could inherit MonoBehavoir at Item base class. I’s there a reason you asked?

Ah just read your edit. Yeah bout the entity-component design sort of sucks when it comes to handling this level of control and modular design. Especially if you are making a network/mmo type game.

I don’t see how the entity-component design would “suck” when creating an online game. You lose out on all the benefits of creating extremely SOLID(haha stupid programmer jokes) scripts by not implementing things this way… I’ve updated my previous reply with some pseudo code.

That’s exactly what I did before. Trust me man. It doesn’t work. If you want I would challenge you to write it out your way and I’ll tell you how and why it won’t work. Just saying as you are bent on doing it that way lol. I just saw your profile and apparently you are a scripting guru. I am truly now curious as to how an inventory as you say could be made. I just cant wrap my mind around it.

The goal is to make your scripts more manageable by following the single responsibility principle. I don’t really see why more scripts is an issue… They will be significantly smaller and have very specific functionality. This isolation is generally overlooked but it significantly improves workflow by allowing you to re-use more code and makes implementing unit tests very easy. Debugging also becomes much easier as you will know exactly which script contains the specific behaviour that is causing problems.

In your case an “Item” is an object because it implements a collection of multiple behaviours; equippable, usable, stackable(add?), and droppable in a single script. Instead you should implement the behaviours separately and attach them as components onto a entity(GameObject) in your scene. You can then assume any entity in your scene that contains the aforementioned behaviours is an “Item”.

Creating an “Item” purely as code is also very simple… You could take it further and implement item templates(ScriptableObjects) and reference them when you create items. This will allow you to specify constant attributes like names, ammo types, and ammo count.

public static GameObject CreateItem()
{
   GameObject go = new GameObject("NewItem");
   go.AddComponent<ItemEquippableBehaviour>();
   go.AddComponent<ItemUsableBehaviour>();
   go.AddComponent<ItemStackableBehaviour>();
   go.AddComponent<ItemDroppableBehaviour>();
   return go;
}

When I said you should use abstract classes I meant you should implement the base behaviour and extend further if needed. Example below…

public abstract class EquippableBehaviour<T> : MonoBehaviour
{
   public virtual void OnEquip(T equipper)
   {
      //do generic OnEquip behaviour here
   }
   public virtual void OnUnequip(T unequipper)
   {
      //do generic OnUnequip behaviour here
   }
}
public class ItemEquippableBehaviour : EquippableBehaviour<CharacterType>
{
   public override void OnEquip(CharacterType equipper)
   {
      base.OnEquip(equipper);

      //do item specific OnEquip behaviour here
   }
   public override void OnUnequip(CharacterType unequipper)
   {
      base.OnUnequip(unequipper);

      //do item specific OnUnequip behaviour here
   }
}

Your “Inventory” also appears to be an object and suffers the same problem. Also, why do you need any lists at all? Why don’t you just implement a “WeaponSlotBehaviour” script? Attach 3 instances of it to a child GameObject(named “Inventory” :smile:) of your player entity and assign an exposed variable to use for the specific input keys.

More pseudo code…

public class WeaponSlotBehaviour : MonoBehaviour
{
   [SerializeField]
   private GameObject weaponMount;
   private GameObject weapon;
   [SerializeField]
   private string activateWeaponKey;
   [SerializeField]
   private string dropKey;

   void Update()
   {
      if (this.weapon != null)
      {
         if (Input.GetButtonUp(this.activateWeaponKey))
         {
            this.ActivateWeapon();
         }
         else if (Input.GetButtonUp(this.dropKey))
         {
            this.DropWeapon();
         }
      }
   }

   private void ActivateWeapon()
   {
      //disable the weapon currently attached to this.weaponMount
      //enable this.weapon
   }

   private void PickupWeapon(GameObject weapon)
   {
      if (this.weapon != null)
      {
         DropWeapon();
      }
      this.weapon = weapon;
   }

   private void DropWeapon()
   {
      ItemDroppableBehaviour droppable = this.weapon.GetComponent<ItemDroppableBehaviour>();
      if (droppable != null)
      {
         droppable.OnDrop();
      }
      this.weapon = null;
   }
}

P.S. There is no spoon…

1 Like

You don’t seem to understand… You shouldn’t be implementing specific weapon scripts like a Shotgun because they are object types. You should be implementing separate behaviours that make up the object… AmmoBehaviour and DurabilityBehaviour are completely separate things that should be in their own scripts. They can then be attached to ANY weapon entity(GameObject) in your game that requires ammo handling or durability.

Why is calling GetComponent an issue when firing your weapon? Most guns have a specific rate at which they shoot. Calling GetComponent a few times every few milliseconds isn’t going to impact performance whatsoever and you can most likely cache any components that are used in the first place.

AHHHHHH *facepalm. I read few articles as well as what you just posted. Now makes bit more sense to me. I take to heart what I learned in the last 3 years of college and growing up. I totally understand in breaking things down in small parts and build anything with them. Just recently my mentality was to reduce the amount of scripts that I had. I’ll take what I learned today and hopefully build a new mindset around programming in this style. Makes absolute sense now. Hard to break old habits lol