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
CodePudding user response:
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.
CodePudding user response:
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.
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);
}
}