0
\$\begingroup\$

My game has an item system that is relatively small compared to most of the RPGs, but it is pretty similar to the games like Half-Life and Counter-Strike where you have an inventory consists of several menus (categories) and a few slots in them. Whenever the player picks up or receives the weapon, from then on they can select it using their inventory which is quite simplistic, in my opinion, and I like it.

First of all, I want to indicate that I am willing to separate logic and data. For data, I am using ScriptableObjects and the basic concept behind is basically referencing ScriptableObjects to MonoBehaviours and handle the tasks in there according to the data. This can be of course made with Prefabs but the thing is, you would de unconsciously duplicating common components that an item would use and only change some of the fields in one of your MonoBehaviours that is related to items. So my approach to that is, keep the data with ScriptableObjects, create only single Prefab, then tie the related components to it and the logic MonoBehaviour which will reference the certain data.

To begin with, I want to talk about how I designed my ScriptableObjects (in other words, data structures). There is the base (abstract) class called ItemData which contains common stuff like every item will have, for example, HUD icon. Then, there are two more abstract classes which are called PassiveItemData & ActiveItemData and derived from ItemData.

public enum ActiveItemType {
    Active,
    Firearm,
    Melee
}

public abstract class ActiveItemData : ItemData {
    public Sprite wieldingSprite;

    public ActiveItemType type;
    public int menuPosition; 

    public SupplyData resourceType;
}

public abstract class PassiveItemData : ItemData {

}

PassiveItemData does not have any fields at the moment, but might do in the future. For now, the only class that derives from it is this:

public class SupplyData : PassiveItemData {
    [Tooltip("The maximum amount that the player can carry this type of resource at once.")]
    public int max;
}

SupplyData is the data of consumable items (e.g. ammo) which have no use on their own but can be consumed by other "active" items. Active items are the items that have actual functionalities (e.g. firearm, melee weapon) and can be directly used by the players and switched to via inventory. And a typical active item would also consume a supply. In games like Half-Life, there is no such thing called "active items", whatever that is can be switched to and directly used, they are called weapons in there. The reason that I named mine version as active items is that because, in my game, there won't be only weapons that can be directly used but also items like Medical Syringe (which consumes syringes) for healing purposes. My problem is I don't know where should I put the methods that perform the tasks like fire, use, swing, etc... Because they are not very generic and may belong to the only specific type of items, which is kinda tricky to implement.

My point was to put them inside a MonoBehaviour that will be tied to the Prefab. However, it is not possible to implement it with a single MonoBehaviour since the data would reference either a firearm or melee weapon data. Say, it is a melee weapon, it'd be non-sense to keep a field like ammoInMagazine that is related to only firearms within the script. But you can never know for sure if the reference data will be firearm or melee weapon. So you have to keep it in there whatsoever. Yet, I don't want this to happen. So I used several more classes that are going to keep the data and perform the tasks accordingly that derives from MonoBehaviour. And the issue has now come to how am I going to use these MonoBehaviours? Create prefabs for each MonoBehaviour? Like Firearm.cs MonoBehaviour to Firearm Prefab and MeleeWeapon.cs MonoBehaviour to Melee Weapon Prefab? Oh, my God. Does this mean I'd have to deal with this each time I implement a new item type? No way I do that. Or maybe I do, but not sure if this is the right approach.

I also thought of doing all these without creating MonoBehaviours and Prefabs that they are going to be tied. But in this case, there are going to be other situations where you might not be able to configure the "fire point", for example.

So, people, this might be a little longer than you expected but I still don't even think I included all the details. Nevertheless, I believe you have the big picture and can suggest a thing or two. Thanks in advance.

\$\endgroup\$
  • \$\begingroup\$ I think your worry about duplicating data in prefabs might be holding you back from more elegant component-based solutions. A little extra data won't cause undue hardship, and it can free you from the inheritance polymorphism knot you find yourself in. \$\endgroup\$ – DMGregory Apr 29 at 15:29
0
\$\begingroup\$

It sounds to me like your items have enough individual variation that trying to support them off a single prefab and a rigid inheritance hierarchy of ScriptableObject types is likely to be a headache.

For cases like this, where you have many item varieties that have their own particular needs, I think prefabs are actually a great way to go. While you say:

This can be of course made with Prefabs but the thing is, you would de unconsciously duplicating common components that an item would use and only change some of the fields in one of your MonoBehaviours that is related to items

This data duplication is actually very cheap. Even over hundreds of items you're probably looking at only a matter of KiB. I'd argue that's a reasonable price to pay for the flexibility we gain.

Using a prefab for each item lets us use composition over inheritance. Special behaviours like ammunition get offloaded to a component that an item can "have" rather than getting baked into what type of item it "is", and that lets use mix and match them and create interesting variants of items just by combining components, without re-coding our inheritance hierarchy every time we want to add a new combination of behaviours.

For instance, you might make a core Item component that all your inventory interaction and item use gameplay scripts interact with. It does very little on its own - it just has slots to call into other components to customize its action:

public class Item : MonoBehaviour {

    public class TargetedEvent : UnityEvent<GameObject> {}

    [SerializeField]
    ItemSlot _slot;
    public ItemSlot slot { get { return _slot;} }

    [SerializeField]
    UnityEvent _onUse;

    [SerializeField]
    TargetedEvent _onUseOnTarget;

    [SerializeField]
    IUseRestriction[] _restrictions;

    public bool CanUse() {
        if(_onUse == null) return false;

        foreach(var restriction in _restrictions)
            if(restriction.CanUse() == false) return false;

        return true;
    }

    public bool CanUseOn(GameObject target) {
        if(_onUseOnTarget == null) return false;

        foreach(var restriction in _restrictions)
            if(restriction.CanUseOn(target) == false) return false;

        return true;
    }

    public bool TryUse() {
        if(CanUse() == false)
            return false;

        _onUse.Invoke();

        foreach(var restriction in _restrictions) {
            restriction.OnUse();
        }
        return true;
    }

    public bool TryUseOn(GameObject target) {
        if(CanUseOn(target) == false)
            return false;

        _onUseOnTarget.Invoke(target);

        foreach(var restriction in _restrictions) {
            restriction.OnUse();
        }
        return true;
    }
}

Using a UnityEvent means you can wire up any function you want as the action to perform when an item is used, so you can run wild with different item effects without modifying the core access point for interacting with items.

We've also abstracted any rule that might affect when we're allowed to use this item, making them just one or more components that we can attach to the item as needed, or even stack up in different combinations:

public interface IUseRestriction {
    bool CanUse();
    bool CanUseOn(GameObject target);

    void OnUse();
}

Your ammunition is then just one kind of IUseRestriction component that might be attached to an object.

public class Ammunition : MonoBehaviour, IUseRestriction {

    [SerializeField]
    int _currentAmmo;

    [SerializeField]
    int _maxAmmo;

    public bool CanUse() { return _currentAmmo > 0; }
    public bool CanUseOn(GameObject target) { return CanUse(); }

    public void OnUse() { _currentAmmo--; }

    public int Reload(int availableAmmo) {
        int amountLoaded = _maxAmmo - _currentAmmo;
        amountLoaded = Mathf.Max(amountLoaded, availableAmmo);

        _currentAmmo += amountLoaded;
        return amountLoaded;
    }
}

(If you have a centralized ammo inventory on your character rather than in the item itself, you can modify this to query the character's ammo inventory instead)

Other restrictions might include Cooldown or SuperMeter or EffectiveRange tests, friendly/enemy UsableOnFaction masks, even a Consumable restriction that self-destructs the item after use. ;)

Similarly, your concept of an "Active" item can be just another component:

public class Equipable : MonoBehaviour {
    public EquipmentSlot slot;
    public Sprite equippedSprite;
}

Now your code can test if something is equipable just by checking:

var equipable = item.GetComponent<Equipable>();
if(equipable != null) {
    // Offer/execute option to equip this item,
    // using the data in the equipable instance.
}

You can do similarly for Sellable items that need a price/value, Throwable items that need to draw a range-finder when used, etc.

This lets you mix and match different item effects, restrictions, etc. in any way you want, just by combining different components on the prefab, without a care for rigid inheritance hierarchies.

Maybe most of your potions are pure inventory items that can't be equipped except to a quick-use slot. But you have an acid potion that's allowed to be equipped as a weapon. No problem! You just add the Equipable component to that single prefab, and you don't have to create a whole new branch of the potion inheritance hierarchy that duplicates the equipable behaviour from the ActiveItemData type, or a branch of the ActiveItemData type that duplicates the stacking behaviour of the PassiveItemData type. New original gameplay combinations with zero code changes. 😍

If you find you have a lot of components that need to reference the same information, and the duplication bothers you, you can still parcel up just that information to a shared ScriptableObject that they all reference, without losing the flexibility of individual prefabs. Say for instance you have 20 different varieties of potion, where you're allowed to carry a stack of 20 of each, and 12 different varieties of food, where you're allowed to carry a stack of 50 of each, and 5 varieties of gemstone, where you're allowed to carry a stack of 99 of each. Each Stackable component on these prefabs can expose a StackType enum, from which you select Potion, Food, or Gem, and you can have a separate InventoryCapacity ScriptableObject that contains a map from StackType to the max item count.

| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.