10
\$\begingroup\$

My aim is to create a modular / as generic as possible item system which could handle things like:

  • Upgradeable Items (+6 Katana)
  • Stat Modifiers(+15 dexterity)
  • Item Modifiers(%X chance to do Y damage, chance to freeze)
  • Rechargeable Items(Magic staff with 30 usages)
  • Set Items(Equip 4 piece of X set to activate Y feature)
  • Rarity(common, unique, legendary)
  • Disenchantable(breaks into some crafting materials)
  • Craftable(can be crafted with certain materials)
  • Consumable(5min %X attack power, heal +15 hp)

*I was able to solve features that are bold in following setup.

Now I tried to add many options us to reflect what I have in mind. I don't plan to add all of these features necessary, but I would like to be able to implement them as I see fit. These are also should be compatible with inventory system and serialization of data.

I am planning to not use inheritance at all but rather an entity-component / data driven approach. Initially I thought of a system that has:

  • BaseStat: a generic class that holds stats on-the-go(can be used for items and character stats too)
  • Item: a class that holds data such as list of, name, itemtype and things that are related to ui, actionName, description etc.
  • IWeapon: interface for weapon. Every weapon will have its own class with IWeapon implemented in. This will have Attack and a reference to character stats. When weapon is equipped, it's data(Item class' stat) will be injected into character stat(whatever BaseStat it has, it will be added to character class as a Stat bonus) So for example, we want to produce a sword(thinking to produce item classes with json) so sword will add 5 attack to character stats. So we have a BaseStat as ("Attack", 5)(we can use enum too). This stat will be added to character's "Attack" stat as a BonusStat(which would be a different class) upon equipping it. So a class named Sword implements IWeapon will be created when it's Item Class is created. So we can inject character stats into this sword and when attacking, it can retrieve total Attack stat from character stat and inflict damage in Attack method.
  • BonusStat: is a way of adding stats as bonuses without touching the BaseStat.
  • IConsumable: Same logic as with IWeapon. Adding direct stat is fairly easy(+15 hp) but I'm not sure about adding temporary weapons with this setup(%x to attack for 5 min).
  • IUpgradeable: This can be implemented with this setup. I am thinking UpgradeLevel as a base stat, which is increased upon upgrading weapon. When upgraded, we can re-calculate weapon's BaseStat to match its Upgrade level.

Until this point, I can see that system is fairly good. But for other features, I think we need something else, because for example I can't implement Craftable feature into this as my BaseStat would not be able to handle this feature and this is where I got stuck. I can add all ingredients as a Stat but that would not make sense.

To make it easy for you to contribute this, here are some questions that you may help with:

  • Should I continue with this setup to implement other features? Would it be possible without inheritance?
  • Are there any way that you can think of, to implement all of these features without inheritance?
  • About Item Modifiers, how could one achieve that? Because it is very generic in it's nature.
  • What can be done to ease the process of building this kind of architecture, any recommendations ?
  • Are there any sources that I can dig that is related to this problem?
  • I really try to avoid inheritance, but do you think these would be solved / achieved with inheritance with ease while keeping it fairly maintainable?

Feel free to answer just a single question as I kept questions very wide so I can get knowledge from different aspects/people.


EDIT


Following @jjimenezg93's answer, I created a very basic system in C# for testing, it works! See if you can add anything to it:

public interface IItem
{
    List<IAttribute> Components { get; set; }

    void ReceiveMessage<T>(T message);
}

public interface IAttribute
{
    IItem source { get; set; }
    void ReceiveMessage<T>(T message);
}

So far, IItem and IAttribute are base interfaces. There were no need(that I can think of) to have a base interface/attribute for message, so we will directly create a test message class. Now for test classes:


public class TestItem : IItem
{
    private List<IAttribute> _components = new List<IAttribute>();
    public List<IAttribute> Components
    {
        get
        {
            return _components;
        }

        set
        {
            _components = value;
        }
    }

    public void ReceiveMessage<T>(T message)
    {
        foreach (IAttribute attribute in Components)
        {
            attribute.ReceiveMessage(message);
        }
    }
}

public class TestAttribute : IAttribute
{
    string _infoRequiredFromMessage;

    public TestAttribute(IItem source)
    {
        _source = source;
    }

    private IItem _source;
    public IItem source
    {
        get
        {
            return _source;
        }

        set
        {
            _source = value;
        }
    }

    public void ReceiveMessage<T>(T message)
    {
        TestMessage convertedMessage = message as TestMessage;
        if (convertedMessage != null)
        {
            convertedMessage.Execute();
            _infoRequiredFromMessage = convertedMessage._particularInformationThatNeedsToBePassed;
            Debug.Log("Message passed : " + _infoRequiredFromMessage);

        }
    }
} 

public class TestMessage
{
    private string _messageString;
    private int _messageInt;
    public string _particularInformationThatNeedsToBePassed;
    public TestMessage(string messageString, int messageInt, string particularInformationThatNeedsToBePassed)
    {
        _messageString = messageString;
        _messageInt = messageInt;
        _particularInformationThatNeedsToBePassed = particularInformationThatNeedsToBePassed;
    }
    //messages should not have methods, so this is here for fun and testing.
    public void Execute()
    {
        Debug.Log("Desired Execution Method: \nThis is test message : " + _messageString + "\nThis is test int : " + _messageInt);
    }
} 

These are the setup needed. Now we can use the system(Following is for Unity).

public class TestManager : MonoBehaviour
{

    // Use this for initialization
    void Start()
    {
        TestItem testItem = new TestItem();
        TestAttribute testAttribute = new TestAttribute(testItem);
        testItem.Components.Add(testAttribute);
        TestMessage testMessage = new TestMessage("my test message", 1, "VERYIMPORTANTINFO");
        testItem.ReceiveMessage(testMessage);
    }

}

Attach this TestManager script to a component in scene and you can see in debug that message is successfully passed.


In order to explain things: Every item in the game will implement IItem interface and every Attribute(name should not confuse you, it means item feature/system. Like Upgradeable, or disenchantable) will implement IAttribute. Then we have a method to process the message(why we need message will be explained in further example). So in context, you can attach attributes to an item and let the rest do for you. Which is very flexible, because you can add/remove attributes at ease. So a pseudo-example would be Disenchantable. We will have a class called Disenchantable(IAttribute) and it in Disenchant method, it will ask for:

  • List ingredients(when item is disenchanted, what item should be given to player) note: IItem should be extended to have ItemType, ItemID etc.
  • int resultModifier(if you implement a kind of boost the disenchant feature, you can pass an int here to increase the ingredients received when disenchanted)
  • int failureChance(if disenchant process has a failure chance)

etc.

These information will be provided by a class called DisenchantManager, it will receive the item and form this message according to item(ingredients of the item when disenchanted) and player progression(resultModifier and failureChance). In order to pass this message, we will create a DisenchantMessage class, which will act as a body for this message. So DisenchantManager will populate a DisenchantMessage and send it to the item. Item will receive the message and pass it to all of it's attached Attributes. Since Disenchantable class's ReceiveMessage method will look for a DisenchantMessage, only Disenchantable attribute will receive this message and act on it. Hope this clears things as much as it did for me :).

\$\endgroup\$
  • 1
    \$\begingroup\$ You may find some useful inspiration in the modifier systems used in For Honor \$\endgroup\$ – DMGregory Sep 1 '17 at 4:36
  • \$\begingroup\$ @DMGregory Hey! Thanks for the link. While it seems very resourceful, unfortunately I need the actual talk to grasp the concept. And I try to find out the actual talk, which is unfortunately GDCVault's member-only content(495$ for a year is insane!). (You can find the talk here if you have GDCVault membership -,- \$\endgroup\$ – Vandarthul Sep 1 '17 at 6:23
  • \$\begingroup\$ How, exactly, would your "BaseStat" concept preclude craftable weapons? \$\endgroup\$ – Attackfarm Sep 1 '17 at 14:58
  • \$\begingroup\$ It doesn't really preclude but doesn't really fit in the context in my mind. It is doable to add "Wood", 2 and "Iron", 5 as a BaseStat to a crafting recipe, which would yield the sword. I think changing the name of BaseStat to BaseAttribute would serve better in this context. But still, the system will not be able to serve it's purpose. Think about consumable item with 5min - %50 attack power. How would I pass it as a BaseStat? "Perc_AttackPower", 50 this needs to be resolved as "if it's Perc, treat integer as a percentage" and missing the info of minutes. O hope you get what I mean. \$\endgroup\$ – Vandarthul Sep 1 '17 at 15:13
  • \$\begingroup\$ @Attackfarm on second thought, this "BaseStat" concept can be expanded with having a list of ints instead of just one int. so, for consumable buff, I can supply "Attack", 50, 5, 1 and IConsumable would look for 3 integers, 1. - value, 2. - minutes, 3. - if it's a percentage or not. But it feels impellent as other systems gets in and forced to explain themselves only in int. \$\endgroup\$ – Vandarthul Sep 1 '17 at 15:30
6
\$\begingroup\$

I think you can achieve what you want in terms of scalability and maintainability by using an Entity-Component System with basic inheritance and a messaging system. Of course, have in mind that this system is the most modular/customizable/scalable I can think of, but it will probably perform worse than your current solution.

I'll explain further:

First of all, you create an interface IItem and an interface IComponent. Any item you want to store must inherit from IItem, and any component you want to affect your items must inherit from IComponent.

IItem will have an array of components and a method for handling IMessage . This handling method simply sends any received message to all stored components. Then, the components which are interested in that given message will act accordingly, and the others will ignore it.

One message, for example, is of type damage, and it informs both the attacker and attacked, so you know how much you hit and maybe charge your fury bar based on that damage. Or the enemy's AI can decide to run if it hits you and makes less than 2HP damage. These are dumb examples but using a system similar to this I'm mentioning, you won't need to do anything more than creating a message and the appropiate handlings to add most of this kind of mechanics.

I have an implementation for an ECS with messaging here, but this is used for entities instead of items and it uses C++. Anyway, I think it can help if you take a look at component.h, entity.h and messages.h. There are a lot of things to be improved but it worked for me in that simple university work.

Hope it helps.

\$\endgroup\$
  • \$\begingroup\$ Hey @jjimenezg93, thank you for your answer. So in order to elaborate with a simple example of what you've explained: We want a sword that is: - Disenchantable[Component] - Stat Modifier[Component] - Upgradeable[Component] I have a list of actions that contains all things like - DISENCHANT - MODIFY_STAT - UPGRADE Whenever item receives this message, goes through all of it's components and sends this message, each component will know what to do with given message. In theory, this seems awesome! I didn't check your example but I will, thanks a lot! \$\endgroup\$ – Vandarthul Sep 1 '17 at 14:33
  • \$\begingroup\$ @Vandarthul Yes, that's basically the idea. This way, item will know nothing about its components so there's no coupling at all and, at the same time, it will have all the functionality you desire, which can also be shared among different type of items. I hope it fits your needs! \$\endgroup\$ – jjimenezg93 Sep 1 '17 at 14:38

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.