How to manage inventory system if there are too many classes?

Started by
13 comments, last by Fulcrum.013 5 years, 7 months ago

So, initially I was planning to create a base class, and some inherited classes like weapon/armour/etc, and each class will have an enum that specifies its type, and everything was going ok until I hit "usable items".
I ended up with creating UsableItem class, and tons of inherited classes, like Drink/Apple/SuperApple/MagickPotato/Potion/Landmine/(whatever that player can use) each with unique behaviour. I planned to store items in the SQLite database, but I discovered that there are not many ways of creating variables(pointers) with type determined at runtime (that preferably get their stats/model/icon/etc from DB). So, I think that I need to use some variation of the Factory pattern, but I have no idea how I should implement it for this particular case (giant switch/case ? ).

It would be really nice if you guys can give me some advice on how I should manage this kind of problem or maybe how I should redesign the inventory.

Inventory storage is an array of pointers. I'm working with CryEngine V, so RTTI can't be used.

Example code:


namespace Inventory
{
	enum ItemType
	{
		Static,
		Building,
		Usable,
		Weapon,
		Armour
	};

	class InventoryItem
	{
	public:
		virtual ~InventoryItem() = default;

		virtual ItemType GetType() = 0;
		virtual string GetName() = 0;
		virtual string GetIcon() = 0;
		virtual void Destroy()
		{
            //TODO: Notify inventory storage
			delete this;
		}
	};

	class UsableItem : public InventoryItem
	{
	public:
		struct Usage
		{
			int Index;
			string Use_Name;
		};

		virtual CryMT::vector<Usage> GetUsages() = 0;
		virtual void UseItem(int usage) = 0;
	};

	class TestItem : public UsableItem
	{
		int Counter =0;

		ItemType GetType() override
		{
			return ItemType::Usable;
		}

		string GetName() override
		{
			return  "TestItem";
		}

		string GetIcon() override
		{
			return "NULL";
		}

		CryMT::vector<Usage> GetUsages() override
		{
			CryMT::vector<Usage> Usages;
			Usages.push_back(Usage{1, "Dec"});
			Usages.push_back(Usage{2,"Inc"});
			Usages.push_back(Usage{3,"Show"});
			return Usages;
		}

		void UseItem(int usage) override
		{
			CryMT::vector<Usage> uses = GetUsages();
			switch (usage)
			{
			case 0:
				for (int i =0; i<uses.size(); i++)
				{
					CryLog(uses[i].Use_Name);
				}
				break;
			case 1:
				Counter--;
				CryLog("Dec");
				CryLog("%d", Counter);
				break;
			case 2:
				Counter++;
				CryLog("Inc");
				CryLog("%d", Counter);
				break;
			case 3:
				CryLog("%d", Counter);
				break;
			default:
				CryLog("WRONG INDEX");
				break;
			}
		}
	};
}

 

Advertisement

Just out of curiosity, have you looked into scripting in CryEngine? (A quick Google search suggests it supports Lua and C#, but I haven't looked into it further than that.) I just ask because it seems like it could be relevant with respect to some of the issues you mention.

Yeah, i've already looked for that, but in it current state c # does not work yet, and lua does not work anymore and will deprecate soon. So c++ is the most stable option. Same goes for Flowgraph and schematic. I think my biggest problem is lack of knowledge of programming patterns and designs.

You will probably need a unique use_item function for each item if the behavior is unique for each. I don't code in C++ but perhaps you could create separate use_item functions and pass them as an argument upon object creation?

Ex in Python:


class UseableItem(InventoryItem):
	
    def __init__(self, use_item_function):

       self.use_item_function = use_item_function

health_potion = UseableItem(use_health_potion_function)

 

So you would use pointers to functions I guess?

Basically, you would have one UseableItem class that can receive a use_item function upon creation.

And perhaps you could pass parameters to the function you send UseableItem upon creation as well so you don't need separate functions for lets say different levels of a health potion.

Note again I'm a Python developer and I don't have experience with Cry Engine so I'm just providing whatever insight I can. Basically, if you can change the use_item function into an argument the UseableItem can receive upon creation than you can avoid lots of duplication of code.

Please let me know if I was clear or if I did not understand your question correctly. Or if anyone thinks this is a bad approach... 

6 minutes ago, mrpeed said:

You will probably need a unique use_item function for each item if the behavior is unique for each. I don't code in C++ but perhaps you could create separate use_item functions and pass them as an argument upon object creation?

Ex in Python:



class UseableItem(InventoryItem):
	
    def __init__(self, use_item):

       self.use_item = use_item

health_potion = UseableItem(use_health_potion_function)

 

So you would use pointers to functions I guess?

Basically, you would have one UseableItem class that can receive a use_item function upon creation.

And perhaps you could pass parameters to the function you send UseableItem upon creation as well so you don't need separate functions for lets say different levels of a health potion.

Note again I'm a Python developer and I don't have experience with Cry Engine so I'm just providing whatever insight I can. Basically, if you can change the use_item function into a property the UseableItem can receive upon creation then you can avoid lots of duplication of code.

Please let me know if I was clear or if I did not understand your question correctly.

Thanks for the suggestion, but this is something that I'm achieving with multiple classes (that function is overwritten for each class), and I'm still need somehow determine which function should be passed to object of type X. I'll probably try to use Factory pattern with self-registering classes.

7 hours ago, MarkNefedov said:

I'll probably try to use Factory pattern with self-registering classes.

By other word to use a self-made manual working RTTI table. I guess it much easily to make (or use exisitng for other engines) tool to generate RTTI table from C++ code than to create and mantain table manually for such complexive set of classes. Also C++ commitete has created a group for researching way to have full RTTI. But i guess it will not happend prior to 2023 or ever 2026 standart.

Other way is to describe objects actions by data driven only way in case it is possible for required set.

#define if(a) if((a) && rand()%100)

How to reinvent virtual dispatch using C++? There are many ways, all of them wrong.

Perhaps you should just simplify instead.  Have an Item class.  Store objects of the Item class in your inventory.  Give items properties, such as `is_wearable` and `melee_damage`.  Use them as appropriate.  RInse and repeat.

Stephen M. Webb
Professional Free Software Developer

This is one of the reasons that heavily inheritance-based OO techniques have fallen out of favour among a crosssection of game developers.

A popular alternate approach is to go "component based", and replace your UseableItem hierarchy with a single class. That class then contains a set of components that implement all the required behaviours...


HealthPotion = UsableItem(sprite: "red_bottle.png", sound_effect: "drinkme.wav", on_use: [AddHealth(health: 100)])
InvulnerabilityPotion = UsableItem(sprite: "yellow_bottle.png", sound_effect: "drinkme.wav", on_use: [SetInvulnerable(seconds: 5)])
PowerUp = UsableItem(sprite: "mushroom.png", sound_effect: "levelup.wav", on_use: [MakeBig()])

 

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

2 hours ago, swiftcoder said:

That class then contains a set of components that implement all the required behaviours...

And by this way you just enlarging scale of problem,becouse  ever in case you have better hierarhy (that you really will have better) it require biggest set of classes to describe it, ever in case each of specific classes will be simplier. Main problem here is absence of automatic persistent technology that required to automatically manage components/objects serialization/deserialization. So it still have 2 options - make a RTTI table required for persistent implementation manually or find a tool to generate it automatically.

#define if(a) if((a) && rand()%100)

23 minutes ago, Fulcrum.013 said:

Main problem here is absence of automatic persistent technology that required to automatically manage components/objects serialization/deserialization.

Yeah, so I presented an unreasonably simplified example, suitable for a dynamic language where you don't have to worry about lack of RTTI.

In practice, if you make your entity class be a pure data container, and put all the functionality into component systems that operate on that data, you can avoid use of RTTI entirely. Yes, some component systems may boil down to a big switch/case statement, but that's not necessarily a bad thing.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

This topic is closed to new replies.

Advertisement