Tuesday, July 24, 2018

unity - Creating a Robust Item System


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.







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 Components { get; set; }

void ReceiveMessage(T message);
}



public interface IAttribute
{
IItem source { get; set; }

void ReceiveMessage(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 _components = new List();
public List Components

{
get
{
return _components;
}

set
{
_components = value;
}

}

public void ReceiveMessage(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 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 :).



Answer



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.


No comments:

Post a Comment

Simple past, Present perfect Past perfect

Can you tell me which form of the following sentences is the correct one please? Imagine two friends discussing the gym... I was in a good s...