Reputation: 5220
Let's image for a game, I have this scheme
Item
is what all the game's Item based off
Some Item
like Sword
implements IEquipable
Some Item
like Potion
implements IConsumable
When I am giving out a quest reward, I add an Item
to the character.
When I am listing out the character's inventory, I go through their List
of Item
. At this time, I only know I have some Item
but no longer know that the Item is Equipable
or Consumable
. Basically, the Interface is no longer known to me. What design can I do in this circumstance?
Let's be more specific.
When an Item is IConsumable
, I would like them to "stack" on top of eachother, may be give a different glow to them and a right-click action will result in consuming that item
When an Item is IEquipable
, The item will not stack inside the inventory, may be give a different glow to them and a right-click action will result in equipping that item
Should I do type-checking (If item is IEquipable then -> Do IEquipable-specific actions or access specific property). Or maybe there would be a different design altogether for this type of problem?
I am looking for answer that describe if this is not a problem or how I can avoid it if is a design problem (of needing to access to feature of more "refined" class while what we have at hand is the more generic class) - with example if possible
Update: I would argue that it makes sense for inventory to be basing off Item
. While Item provides the most basic functionality like Name
, GoldValue
or ItemIcon
.
Though properties like Durability
or Enchantments[]
can only be applied to Equipables. Let's take a look at Minecraft inventory. I would say every basic interaction are from InventorySystem
to Item
. Though it is still necessary to know what is the kind of Item
the InventorySystem
is processing? Like Bread
, what I see is that they are consumables and can stack up to 64 in one single slot
Upvotes: 0
Views: 319
Reputation: 1104
For your particular scenario, I suggest you separate the items at the moment of adding them to the character. You can use several overloads and the compiler will do the job for you...
public class Character {
...
List<Item> Items {get;} = new List<Item>();
List<Item> ConsumableItems {get;} = new List<Item>();
List<Item> EquipableItems {get;} = new List<Item>();
public void AddItem(IEquipable item) {
Items.Add(item); EquipableItems.Add(item);
}
public void AddItem(IConsumable item) {
Items.Add(item); ConsumableItems.Add(item);
}
}
In case handling separated lists is not working for you, adding an ItemType field for the Item class would simplify things.
enum ItemType { Equipable, Consumable }
// usage example
public class SomeConsumableItem : IConsumable {
public readonly ItemType ItemType {get;} = ItemType.Consumable;
...
}
Upvotes: 1
Reputation: 1709
As others have mentioned, the is
keyword will do what you're describing, but I think it would be better to change your approach to the problem.
I'm going to ignore the "stacking" part of your question because I don't know what "stacking" means here--is it a visual stacking on-screen, or is it a logical combination of different items in the rules of the game? I think my answer will be clear enough using just the glow effect and consume/equip actions. If so, you should be able to connect the dots to implement stacking yourself.
The is
and as
keywords and the Linq OfType<T>()
extension method are all simple ways to do the type-checking you described. They allow some sort of central game manager or player class (I'll just say "game manager" from now on) to ask each Item
what its type is and then make decisions based on the response. The approach will work for simple systems, but might become difficult to maintain as the number of Item
types grows--the game manager needs to know about the quirks of every Item
type.
Edit 1: It's also worth noting that type-checking is generally assumed to perform poorly. I say "generally assumed" because the only way to know for sure whether something is performant enough for a given situation is to write and test the code--but I would personally avoid doing any type-checking in a game's update or draw loop.
Instead, consider adding new properties or methods to Item
: perhaps a GlowEffect
property, and Use
method. These methods would allow the game manager to ask the Item
what to do or notify the Item
that something has happened in the game. The game manager no longer needs to know about every possible Item
type or its behavior--the behavior is encapsulated in the Item
itself. You might not even need the IEquippable
and IConsumable
interfaces anymore.
I would characterize this as a "strategy" design pattern. Each Item
implementation contains strategies for how to display or interact with a specific in-game resource or tool.
public abstract class Item : Item
{
public abstract GlowEffect GlowEffect { get; }
// A reference to the current Player allows the "Use"
// method to affect the active player.
public abstract void Use(Player player);
}
public class Sword
{
public override GlowEffect GlowEffect { get { return new GreenGlowEffect(); } }
public override void Use(Player player)
{
player.Equip(this);
}
}
Upvotes: 4
Reputation: 17066
The problem is here.
Item
is what all the game's Item based off
A fundamental misconception when entering the realm of OOP is that an application is built by creating a single object hierarchy with one (business) object at its root. This will cause you nothing but pain. Abstractions must be based upon common behaviors: objects which can be used in the same way. Every non-trivial application will have numerous object hierarchies. If you find yourself with an abstraction you cannot use, then you have created the wrong abstraction.
Here is how I suggest interpreting the problem statement above.
- I have put all of my objects into a bag where I cannot tell the difference between them.
- I need to tell the difference between them.
Hopefully the answer is obvious. Don't put your objects into that bag! Eliminate the Item
abstraction and don't treat objects as abstractions you can't use. Of course, designing the right abstraction is hard. There's no way around that, and the abstractions that are right for your application won't necessarily be the same for any other application.
Regarding other answers,
Now, what every developer who faces this problem wants to hear is, what is the solution? You're going to be disappointed, because I can't tell you. No one can. Consider what you're asking. There is essentially no difference between this question and, "How should I design an Object-Oriented application?" Providing a domain like "game" and naming a handful of domain objects does nothing to change the question. Entire books are written to answer this question.
If you want to learn by coding, all you can do is try different abstractions. If that doesn't work, you can try non-OO solutions. If that doesn't work you will have learned something. At this stage of development, implementing the the wrong solution and learning from it is more valuable than implementing the right solution without understanding tradeoffs.
Upvotes: 2
Reputation: 2565
In C# you can use the is
keyword to check whether an object is an instance of a certain class/interface. Therefore you can do:
foreach (var item in inventory)
{
if (item is IEquipable)
{
IEquipable equipable = item as IEquipable;
//....
}
else if (item is IConsumable)
{
IConsumable consumable = item as IConsumable;
//....
}
}
EDIT
If you want to use a structured pattern rather than an if else the following can help you. I personally think that in your case adds a lot of complexity that you can avoid.
let's start from the IItem interface. We'll let IConsumable and IEquipable inherit from it. We put the foundation of a Visitor pattern here that will be helpful later
interface IItem {
void Accept(IItemVisitor visitor);
}
interface IConsumable: IItem {}
interface IEquipable: IItem {}
interface IItemVisitor {
void Visit(IConsumable consumable);
void Visit(IEquipable equipable);
}
abstract class ConsumableBase: IConsumable
{
void Accept(IItemVisitor visitor) => visitor.Visit(this);
}
abstract class EquipableBase: IEquipable
{
void Accept(IItemVisitor visitor) => visitor.Visit(this);
}
the visitor pattern is what will avoid that if/else of the previous solution.
Now I assume you have an InventoryManager class where you populate the Inventory given a list of items. It might look like the following:
class InventoryManager : IItemVisitor
{
void PopulateInventory(IEnumerable<IItem> items)
{
foreach (var item in items)
{
item.Accept(this);
}
}
void Visit(IEquipable equipable)
{
//add equipable to the inventory
}
void Visit(IConsumable consumable)
{
//add consumable to the inventory
}
}
This is the best pattern you can use when have to deal with different branches of the inheritance tree but you cannot solve with polymorphism directly. It has some drawbacks, for instance if you add a new interface that extends IItem you need to change all the visitor classes. It also adds some complexity to the code, so it's probably not worth the effort in some cases. Anyway, I think this is what you are looking for.
Upvotes: 2