OOP: 如何 "know" 这个游戏设计中的对象类型

OOP: How to "know" the object's type in this game design

来个游戏图吧,我有这个方案

Item是游戏中所有物品的基础

一些 ItemSword 实现 IEquipable

一些 ItemPotion 实现 IConsumable

当我发放任务奖励时,我给角色添加了一个Item。 当我列出角色的物品栏时,我浏览了他们的 List of Item。这时,我只知道我有一些 Item 但不再知道 Item 是 EquipableConsumable 。基本上,界面不再为我所知。在这种情况下我可以做什么设计?

让我们更具体一点。

当物品 IConsumable 时,我希望它们“堆叠”在彼此之上,可能会给它们带来不同的光芒,并且右键单击操作会导致消耗该物品

当物品 IEquipable 时,物品不会堆叠在物品栏中,可能会给它们不同的光芒,右键单击操作会导致装备该物品

我是否应该进行类型检查(如果项目是 IEquipable,则 -> 执行特定于 IEquipable 的操作或访问特定的 属性)。或者对于这类问题可能会有完全不同的设计?

我正在寻找描述这是否不是问题的答案,或者如果是设计问题我如何避免它(需要访问更“精致”的功能 class 而我们拥有手头是更通用的 class) - 如果可能的话用例子

更新:我认为库存基于 Item 是有意义的。虽然 Item 提供了最基本的功能,例如 NameGoldValueItemIcon.

虽然 DurabilityEnchantments[] 等属性只能应用于 Equipables。让我们来看看 Minecraft 库存。我会说每个基本交互都是从 InventorySystemItem。虽然仍然有必要知道 Item 正在处理什么类型的 InventorySystem?像 Bread 一样,我看到的是它们是消耗品,一个插槽中最多可以堆叠 64 个

在C#中你可以使用is关键字来检查一个对象是否是某个class/interface的实例。因此你可以这样做:

 foreach (var item in inventory)
 {
     if (item is IEquipable)
     {
         IEquipable equipable = item as IEquipable;
         //....
     }
     else if (item is IConsumable)
     {
         IConsumable consumable = item as IConsumable;
         //....
     }
 }

编辑

如果您想使用结构化模式而不是 if else,以下内容可以帮助您。我个人认为在你的情况下增加了很多你可以避免的复杂性。

让我们从IItem界面开始。我们会让 IConsumable 和 IEquipable 从它继承。我们把访问者模式的基础放在这里,以后会有帮助

 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);
 }

访客模式将避免 if/else 之前的解决方案。

现在我假设您有一个 InventoryManager class,您可以在其中填充给定项目列表的库存。它可能如下所示:

 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
       } 
  }

当必须处理继承树的不同分支但不能直接使用多态性解决时,这是您可以使用的最佳模式。 它有一些缺点,例如,如果您添加一个扩展 IItem 的新接口,您需要更改所有访问者 classes。它还增加了代码的复杂性,因此在某些情况下可能不值得为此付出努力。 不管怎样,我想这就是你要找的。

正如其他人所提到的,is 关键字将执行您所描述的内容,但我认为最好改变您解决问题的方法。

我将忽略你问题中的“堆叠”部分,因为我不知道这里的“堆叠”是什么意思——它是屏幕上的视觉堆叠,还是不同的逻辑组合游戏规则中的项目?我认为仅使用发光效果和 consume/equip 动作我的答案就足够清楚了。如果是这样,您应该能够连接点以自己实现堆叠。

isas 关键字以及 Linq OfType<T>() 扩展方法都是执行您描述的类型检查的简单方法。它们允许某种中央游戏管理员或玩家 class(从现在开始我只说“游戏管理员”)询问每个 Item 它的类型是什么,然后根据响应做出决定。该方法适用于简单系统,但随着 Item 类型数量的增加可能会变得难以维护——游戏经理需要了解每种 Item 类型的怪癖。

编辑 1: 另外值得注意的是,类型检查通常被认为性能不佳。我说“一般假设”是因为要确定某些东西在给定情况下是否具有足够的性能,唯一的方法是编写和测试代码——但我个人会避免在游戏的更新或绘制循环中进行任何类型检查。

相反,考虑向 Item 添加新的属性或方法:可能是 GlowEffect 属性 和 Use 方法。这些方法将允许游戏管理员询问 Item 要做什么或通知 Item 游戏中发生了某些事情。游戏管理器不再需要了解每个可能的 Item 类型或其行为——行为封装在 Item 本身中。您甚至可能不再需要 IEquippableIConsumable 接口。

我将其描述为“策略”设计模式。每个 Item 实现都包含有关如何显示特定游戏内资源或工具或与之交互的策略。

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);
    }
}

问题就在这里

Item is what all the game's Item based off

进入 OOP 领域时的一个基本误解是,应用程序是通过创建单个对象层次结构来构建的,其中一个(业务)对象位于其根部。这只会给你带来痛苦。抽象必须基于共同的行为:可以以相同方式使用的对象。每个重要的应用程序都会有许多对象层次结构。如果您发现自己无法使用抽象,那么您就创建了错误的抽象。

以下是我建议如何解释上面的问题陈述。

  1. I have put all of my objects into a bag where I cannot tell the difference between them.
  2. I need to tell the difference between them.

希望答案是显而易见的。 不要把你的对象放进那个袋子里!消除Item抽象,不要把对象当作你不能使用的抽象。当然,设计 right 抽象很难。没有办法解决这个问题,适用于您的应用程序的抽象不一定适用于任何其他应用程序。

关于其他答案,

  • 类型检查是过程式编程或函数式编程中的有效解决方案。它不是面向对象的解决方案。这很好。你不必做 OOP。
  • 访问者模式是针对小型固定对象层次结构的复杂、利基解决方案。它不是应用程序的基础模式。不同的编程范式会更合适。

现在,每个面临这个问题的开发者都想听到的是,解决方案是什么?你会失望的,因为我不能告诉你。没有人可以。考虑一下你在问什么。这个问题与“我应该如何设计面向对象的应用程序?”之间基本上没有区别,提供像“游戏”这样的域并命名少数域对象不会改变这个问题。整本书都是为了回答这个问题。

如果你想通过编码来学习,你所能做的就是尝试不同的抽象。如果这不起作用,您可以尝试非 OO 解决方案。如果那不起作用,您将学到一些东西。在这个发展阶段,实施错误的解决方案并从中吸取教训,比实施正确的解决方案而不了解权衡更有价值。

对于您的特定场景,我建议您在将它们添加到角色时将它们分开。 您可以使用多个重载,编译器将为您完成工作...

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);
    }
}

如果处理分隔的列表不适合您,为项目添加 ItemType 字段 class 会简化事情。

 enum ItemType { Equipable, Consumable }

 // usage example
 public class SomeConsumableItem : IConsumable {

  public readonly ItemType ItemType {get;} = ItemType.Consumable;
  ...
 }