OOP: 如何 "know" 这个游戏设计中的对象类型
OOP: How to "know" the object's type in this game design
来个游戏图吧,我有这个方案
Item
是游戏中所有物品的基础
一些 Item
像 Sword
实现 IEquipable
一些 Item
像 Potion
实现 IConsumable
当我发放任务奖励时,我给角色添加了一个Item
。
当我列出角色的物品栏时,我浏览了他们的 List
of Item
。这时,我只知道我有一些 Item
但不再知道 Item 是 Equipable
或 Consumable
。基本上,界面不再为我所知。在这种情况下我可以做什么设计?
让我们更具体一点。
当物品 IConsumable
时,我希望它们“堆叠”在彼此之上,可能会给它们带来不同的光芒,并且右键单击操作会导致消耗该物品
当物品 IEquipable
时,物品不会堆叠在物品栏中,可能会给它们不同的光芒,右键单击操作会导致装备该物品
我是否应该进行类型检查(如果项目是 IEquipable,则 -> 执行特定于 IEquipable 的操作或访问特定的 属性)。或者对于这类问题可能会有完全不同的设计?
我正在寻找描述这是否不是问题的答案,或者如果是设计问题我如何避免它(需要访问更“精致”的功能 class 而我们拥有手头是更通用的 class) - 如果可能的话用例子
更新:我认为库存基于 Item
是有意义的。虽然 Item 提供了最基本的功能,例如 Name
、GoldValue
或 ItemIcon
.
虽然 Durability
或 Enchantments[]
等属性只能应用于 Equipables。让我们来看看 Minecraft 库存。我会说每个基本交互都是从 InventorySystem
到 Item
。虽然仍然有必要知道 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 动作我的答案就足够清楚了。如果是这样,您应该能够连接点以自己实现堆叠。
is
和 as
关键字以及 Linq OfType<T>()
扩展方法都是执行您描述的类型检查的简单方法。它们允许某种中央游戏管理员或玩家 class(从现在开始我只说“游戏管理员”)询问每个 Item
它的类型是什么,然后根据响应做出决定。该方法适用于简单系统,但随着 Item
类型数量的增加可能会变得难以维护——游戏经理需要了解每种 Item
类型的怪癖。
编辑 1: 另外值得注意的是,类型检查通常被认为性能不佳。我说“一般假设”是因为要确定某些东西在给定情况下是否具有足够的性能,唯一的方法是编写和测试代码——但我个人会避免在游戏的更新或绘制循环中进行任何类型检查。
相反,考虑向 Item
添加新的属性或方法:可能是 GlowEffect
属性 和 Use
方法。这些方法将允许游戏管理员询问 Item
要做什么或通知 Item
游戏中发生了某些事情。游戏管理器不再需要了解每个可能的 Item
类型或其行为——行为封装在 Item
本身中。您甚至可能不再需要 IEquippable
和 IConsumable
接口。
我将其描述为“策略”设计模式。每个 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 领域时的一个基本误解是,应用程序是通过创建单个对象层次结构来构建的,其中一个(业务)对象位于其根部。这只会给你带来痛苦。抽象必须基于共同的行为:可以以相同方式使用的对象。每个重要的应用程序都会有许多对象层次结构。如果您发现自己无法使用抽象,那么您就创建了错误的抽象。
以下是我建议如何解释上面的问题陈述。
- 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.
希望答案是显而易见的。 不要把你的对象放进那个袋子里!消除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;
...
}
来个游戏图吧,我有这个方案
Item
是游戏中所有物品的基础
一些 Item
像 Sword
实现 IEquipable
一些 Item
像 Potion
实现 IConsumable
当我发放任务奖励时,我给角色添加了一个Item
。
当我列出角色的物品栏时,我浏览了他们的 List
of Item
。这时,我只知道我有一些 Item
但不再知道 Item 是 Equipable
或 Consumable
。基本上,界面不再为我所知。在这种情况下我可以做什么设计?
让我们更具体一点。
当物品 IConsumable
时,我希望它们“堆叠”在彼此之上,可能会给它们带来不同的光芒,并且右键单击操作会导致消耗该物品
当物品 IEquipable
时,物品不会堆叠在物品栏中,可能会给它们不同的光芒,右键单击操作会导致装备该物品
我是否应该进行类型检查(如果项目是 IEquipable,则 -> 执行特定于 IEquipable 的操作或访问特定的 属性)。或者对于这类问题可能会有完全不同的设计?
我正在寻找描述这是否不是问题的答案,或者如果是设计问题我如何避免它(需要访问更“精致”的功能 class 而我们拥有手头是更通用的 class) - 如果可能的话用例子
更新:我认为库存基于 Item
是有意义的。虽然 Item 提供了最基本的功能,例如 Name
、GoldValue
或 ItemIcon
.
虽然 Durability
或 Enchantments[]
等属性只能应用于 Equipables。让我们来看看 Minecraft 库存。我会说每个基本交互都是从 InventorySystem
到 Item
。虽然仍然有必要知道 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 动作我的答案就足够清楚了。如果是这样,您应该能够连接点以自己实现堆叠。
is
和 as
关键字以及 Linq OfType<T>()
扩展方法都是执行您描述的类型检查的简单方法。它们允许某种中央游戏管理员或玩家 class(从现在开始我只说“游戏管理员”)询问每个 Item
它的类型是什么,然后根据响应做出决定。该方法适用于简单系统,但随着 Item
类型数量的增加可能会变得难以维护——游戏经理需要了解每种 Item
类型的怪癖。
编辑 1: 另外值得注意的是,类型检查通常被认为性能不佳。我说“一般假设”是因为要确定某些东西在给定情况下是否具有足够的性能,唯一的方法是编写和测试代码——但我个人会避免在游戏的更新或绘制循环中进行任何类型检查。
相反,考虑向 Item
添加新的属性或方法:可能是 GlowEffect
属性 和 Use
方法。这些方法将允许游戏管理员询问 Item
要做什么或通知 Item
游戏中发生了某些事情。游戏管理器不再需要了解每个可能的 Item
类型或其行为——行为封装在 Item
本身中。您甚至可能不再需要 IEquippable
和 IConsumable
接口。
我将其描述为“策略”设计模式。每个 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 领域时的一个基本误解是,应用程序是通过创建单个对象层次结构来构建的,其中一个(业务)对象位于其根部。这只会给你带来痛苦。抽象必须基于共同的行为:可以以相同方式使用的对象。每个重要的应用程序都会有许多对象层次结构。如果您发现自己无法使用抽象,那么您就创建了错误的抽象。
以下是我建议如何解释上面的问题陈述。
- 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.
希望答案是显而易见的。 不要把你的对象放进那个袋子里!消除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;
...
}