文字冒险游戏 - 如何区分一种项目类型和另一种项目类型以及如何构建项目 Classes/Subclasses?
Text Adventure Game - How to Tell One Item Type from Another and How to Structure the Item Classes/Subclasses?
我是一名初级程序员(他有很多与视频游戏相关的脚本编写经验,但编程经验很少 - 所以只有循环、流控制等基本知识 - 虽然我有 C++我主讲的基础知识和 C++ 数据结构和算法课程)。我正在从事一个文本冒险个人项目(实际上我早在 Python 之前就已经在 Python 中编写了它,然后才了解 classes 是如何工作的——一切都是一本字典——所以这很可耻)。我 "remaking" 在 C++ 中使用 classes 来摆脱只完成家庭作业的陈规陋习。
我已经写了我的播放器和房间 classes(这很简单,因为我每个只需要一个 class)。我正在处理项目 classes(一个项目是房间里的任何东西,例如火炬、火、标志、容器等)。我不确定如何处理项目基础 class 和派生的 classes。这是我遇到的问题。
我如何以一种非狗屎的方式判断一个项目是否属于某种类型(很有可能我想多了)?
- 例如,我设置了我的打印房间信息功能,这样除了它可能做的任何其他事情之外,它还会打印其清单中每个对象的名称(即在其中),我希望它打印一些东西特别适用于容器对象(例如其库存的内容)。
- 第一部分很简单,因为每个项目都有一个名称,因为名称属性是基本项目的一部分 class。虽然容器有一个库存,这是容器 subclass 独有的属性。
- 据我所知,根据对象的 class 类型执行条件逻辑是一种不好的形式(因为一个人的 classes 应该是多态的)并且我假设(可能是错误的)它将 getHasInventory 访问器虚函数放在项目基 class 中是奇怪和错误的(我在这里的假设是基于认为将每个派生的 class 的虚函数放在基中是疯狂的class - 我有大约一打派生的 classes - 其中一些是派生的 classes of derived classes)。
- 如果这些都是正确的,那么执行此操作的可接受方法是什么?一件明显的事情是向基础添加一个 itemType 属性,然后执行条件逻辑,但这让我觉得是错误的,因为它似乎只是检查 class 类型解决方案的重新换肤。我不确定上述假设是否正确以及好的解决方案可能是什么。
我应该如何构造我的基础 class/classes 和派生的 classes?
- 我最初写它们时 class 项是基础 class 并且大多数其他 classes 使用单继承(除了一对具有多级)。
- 尽管如此,这似乎有些尴尬并在重复我自己。例如,我想要一个标志和一封信。标志是 Readable Item > Untakeable Item > Item。一封信是 Readable Item > Takeable Item > Item。因为它们都使用单一继承,所以我需要两个不同的可读项,一个是可读的,一个不是(我知道我可以在这种情况下将可读和不可读的属性设置为基础的属性,我做到了,但这只是一个例子,因为我仍然与其他 classes 有类似的问题)。
- 这对我来说似乎很恶心,所以我又尝试了一下,并使用多重继承和虚拟继承来实现它们。在我的例子中,这似乎更灵活,因为我可以组合多个 classes 的 classes 并为我的 classes 创建一种组件系统。
- 这些方法中的一种比另一种好吗?还有第三种更好的方法吗?
解决您的问题的一种可能方法是多态性。通过使用多态性,您可以(例如)拥有一个 describe
函数,该函数在被调用时会引导项目向玩家描述自身。您可以对 use
和其他常见动词执行相同的操作。
另一种方法是实现更高级的输入解析器,它可以识别对象并将动词传递给项目的某些(多态)函数以供自己处理。例如,每个项目都可以有一个返回可用动词列表的函数,以及一个返回项目 "names" 列表的函数:
struct item
{
// Return a list of verbs this item reacts to
virtual std::vector<std::string> get_verbs() = 0;
// Return a list of name aliases for this item
virtual std::vector<std::string> get_names() = 0;
// Describe this items to the player
virtual void describe(player*) = 0;
// Perform a specific verb, input is the full input line
virtual void perform_verb(std::string verb, std::string input) = 0;
};
class base_torch : public item
{
public:
std::vector<std::string> get_verbs() override
{
return { "light", "extinguish" };
}
// Return true if the torch is lit, false otherwise
bool is_lit();
void perform_verb(std::string verb, std::string) override
{
if (verb == "light")
{
// TODO: Make the torch "lit"
}
else
{
// TODO: Make the torch "extinguished"
}
}
};
class long_brown_torch : public base_torch
{
std::vector<std::string> get_names() override
{
return { "long brown torch", "long torch", "brown torch", "torch" };
}
void describe(player* p) override
{
p->write("This is a long brown torch.");
if (is_lit())
p->write("The torch is burning.");
}
};
然后如果玩家输入例如light brown torch
解析器查看所有可用物品(玩家物品栏中的物品,然后是房间中的物品),获取每个物品的名称列表(调用物品 get_names()
函数)并将其与brown torch
。如果找到匹配项,解析器将调用项目 perform_verb
函数并传递适当的参数 (item->perform_verb("light", "light brown torch")
).
您甚至可以修改解析器(和项目)来单独处理形容词,甚至像 the
这样的文章,或者保存上次使用的项目以便可以使用 it
引用它。
构建不同的房间和物品是乏味的,但一旦做出好的设计仍然是微不足道的(你真的应该花一些时间创建需求、分析需求和创建设计)。真正困难的部分是编写一个像样的解析器。
请注意,这只是在此类游戏中处理物品和动词的两种可能方式。其他方法还有很多,不一一列举。
你问了一些很好的问题。如何设计、构建和实现程序,以及如何对问题域建模。
OOP,'methods' 和方法
您提出的问题表明您已经了解了OOP(面向对象编程)。在很多关于 OOP 的介绍 material 中,通常鼓励直接通过对象和子类型对问题域进行建模,并通过向它们添加方法来实现功能。一个 class 典型的例子是动物建模,例如 Animal
类型和两个子类型,Duck
和 Cat
,以及实现功能,例如 walk
、quack
和 mew
.
直接使用对象和子类型对问题领域进行建模 是有意义的,但与简单地使用一个或几个不同类型的类型相比,它也可能非常过分和麻烦描述它是什么的字段。在您的情况下,我确实相信更复杂的建模,例如您对对象和子类型或替代方法的建模是有意义的,因为在其他方面,您的功能因类型而异以及有些复杂的数据(例如带有存货)。但要记住这一点 - 存在不同的权衡,有时,使用具有多个不同字段的单一类型来对域进行建模总体上更有意义。
通过基class和子类型上的方法实现所需的功能同样有不同的权衡,对于给定的情况,这并不总是一个好的方法.对于你的一个问题,你可以做一些事情,比如添加一个 print
方法或类似的基本类型和每个子类型,但这在实践中并不总是那么好(一个简单的例子是一个计算器应用程序,其中简化如果使用向基础 class.
添加方法的方法,用户输入的算术表达式(如 (3*x)*4/2
)可能难以实现
替代方法 - 标记 unions/sum 类型
有一个非常好的基础抽象,称为 "tagged union" (it is also known by the names "disjoint union" and "sum type"). The main idea about the tagged union is that you have a union of several different sets of instances, where which set the given instance belongs to matters. They are a superset of the feature in C++ known as enum
. Regrettably, C++ does not currently support tagged unions, though there are research into it (for instance https://www.stroustrup.com/OpenPatternMatching.pdf ,但如果您是初级程序员,这可能有点超出您的能力)。据我所知,这与您在此处给出的示例非常吻合。 Scala 中的一个示例是(许多其他语言也支持标记联合,例如 Rust、Kotlin、Typescript、ML 语言、Haskell 等):
sealed trait Item {
val name: String
}
case class Book(val name: String) extends Item
case object Fire extends Item {
val name = "Fire"
}
case class Container(val name: String, val inventory: List[Item]) extends Item
据我所知,这很好地描述了您的各种物品。请注意,Scala 在这方面有点特殊,因为它通过子类型实现标记联合。
如果您随后想要实现一些打印功能,则可以使用 "pattern matching" 来匹配您拥有的项目并执行特定于该项目的功能。在支持模式匹配的语言中,这很方便且不脆弱,因为模式匹配会检查您是否涵盖了每种可能的情况(类似于 C++ 中的 switch
over enums 检查您是否涵盖了每种可能的情况)。例如在 Scala 中:
def getDescription(item: Item): String = {
item match {
case Book(_) | Fire => item.name
case Container(name, inventory) =>
name + " contains: (" +
inventory
.map(getDescription(_))
.mkString(", ") +
")"
}
}
val description = getDescription(
Container("Bag", List(Book("On Spelunking"), Fire))
)
println(description)
您可以在此处复制粘贴两个片段并尝试 运行 它们:https://scalafiddle.io/ .
这种建模与人们所谓的 "data types" 配合得很好,classes 本身没有或只有很少的功能,[=75=75] 中的字段=]es 基本上是它们接口的一部分("interface" 从某种意义上说,如果您添加、删除或更改类型的字段,您想更改使用这些类型的实现)。
相反,当 class 内部的实现不是其接口的一部分时,我发现更传统的子类型建模和方法更方便,例如,如果我有一个描述碰撞系统接口的基类型, 它的每个子类型都有不同的性能特征,可以在不同的情况下使用。隐藏和保护实现,因为它不是接口的一部分,这很有意义,并且非常适合人们所说的 "mini-modules".
在 C++(和 C)中,尽管缺乏语言支持,有时人们确实会以各种方式使用标记联合。我看到在 C 中使用的一种方法是创建一个 C 联合(尽管在内存和语义等方面要小心),其中使用 enum
标记来区分不同的情况。这很容易出错,因为您可能很容易最终访问一个 enum
案例中的字段,而该字段对该 enum
案例无效。
您还可以将命令输入建模为标记联合。也就是说,解析可能有点挑战性,如果您是初级程序员,解析库可能会涉及一些;保持解析简单可能是个好主意。
旁注
C++ 是一种特殊的语言——我不太喜欢它用于我不太关心资源使用或 运行时间性能等的情况,因为多种不同的原因,因为它可能很烦人并且开发起来不那么灵活。开发起来可能具有挑战性,因为您必须始终非常小心地避免 undefined behaviour。也就是说,如果资源使用或 运行 时间性能很重要,那么 C++ 可能是一个很好的选择,具体取决于具体情况。在 C++ 语言及其社区中也有许多非常有用和重要的见解,例如 RAII、所有权和生命周期。我的建议是学习 C++ 是个好主意,但您也应该学习其他语言,例如静态类型的函数式编程语言。 FP(函数式编程)和支持 FP 的语言,有许多优点和缺点,但它们的一些优点非常非常好,尤其是 reg.不变性和副作用。
在这些语言中,Rust 在某些方面可能是最接近 C++ 的,尽管我没有使用 Rust 的经验,因此不能保证语言或其社区。
作为旁注,您可能对此维基百科页面感兴趣:https://en.wikipedia.org/wiki/Expression_problem .
我是一名初级程序员(他有很多与视频游戏相关的脚本编写经验,但编程经验很少 - 所以只有循环、流控制等基本知识 - 虽然我有 C++我主讲的基础知识和 C++ 数据结构和算法课程)。我正在从事一个文本冒险个人项目(实际上我早在 Python 之前就已经在 Python 中编写了它,然后才了解 classes 是如何工作的——一切都是一本字典——所以这很可耻)。我 "remaking" 在 C++ 中使用 classes 来摆脱只完成家庭作业的陈规陋习。
我已经写了我的播放器和房间 classes(这很简单,因为我每个只需要一个 class)。我正在处理项目 classes(一个项目是房间里的任何东西,例如火炬、火、标志、容器等)。我不确定如何处理项目基础 class 和派生的 classes。这是我遇到的问题。
我如何以一种非狗屎的方式判断一个项目是否属于某种类型(很有可能我想多了)?
- 例如,我设置了我的打印房间信息功能,这样除了它可能做的任何其他事情之外,它还会打印其清单中每个对象的名称(即在其中),我希望它打印一些东西特别适用于容器对象(例如其库存的内容)。
- 第一部分很简单,因为每个项目都有一个名称,因为名称属性是基本项目的一部分 class。虽然容器有一个库存,这是容器 subclass 独有的属性。
- 据我所知,根据对象的 class 类型执行条件逻辑是一种不好的形式(因为一个人的 classes 应该是多态的)并且我假设(可能是错误的)它将 getHasInventory 访问器虚函数放在项目基 class 中是奇怪和错误的(我在这里的假设是基于认为将每个派生的 class 的虚函数放在基中是疯狂的class - 我有大约一打派生的 classes - 其中一些是派生的 classes of derived classes)。
- 如果这些都是正确的,那么执行此操作的可接受方法是什么?一件明显的事情是向基础添加一个 itemType 属性,然后执行条件逻辑,但这让我觉得是错误的,因为它似乎只是检查 class 类型解决方案的重新换肤。我不确定上述假设是否正确以及好的解决方案可能是什么。
我应该如何构造我的基础 class/classes 和派生的 classes?
- 我最初写它们时 class 项是基础 class 并且大多数其他 classes 使用单继承(除了一对具有多级)。
- 尽管如此,这似乎有些尴尬并在重复我自己。例如,我想要一个标志和一封信。标志是 Readable Item > Untakeable Item > Item。一封信是 Readable Item > Takeable Item > Item。因为它们都使用单一继承,所以我需要两个不同的可读项,一个是可读的,一个不是(我知道我可以在这种情况下将可读和不可读的属性设置为基础的属性,我做到了,但这只是一个例子,因为我仍然与其他 classes 有类似的问题)。
- 这对我来说似乎很恶心,所以我又尝试了一下,并使用多重继承和虚拟继承来实现它们。在我的例子中,这似乎更灵活,因为我可以组合多个 classes 的 classes 并为我的 classes 创建一种组件系统。
- 这些方法中的一种比另一种好吗?还有第三种更好的方法吗?
解决您的问题的一种可能方法是多态性。通过使用多态性,您可以(例如)拥有一个 describe
函数,该函数在被调用时会引导项目向玩家描述自身。您可以对 use
和其他常见动词执行相同的操作。
另一种方法是实现更高级的输入解析器,它可以识别对象并将动词传递给项目的某些(多态)函数以供自己处理。例如,每个项目都可以有一个返回可用动词列表的函数,以及一个返回项目 "names" 列表的函数:
struct item
{
// Return a list of verbs this item reacts to
virtual std::vector<std::string> get_verbs() = 0;
// Return a list of name aliases for this item
virtual std::vector<std::string> get_names() = 0;
// Describe this items to the player
virtual void describe(player*) = 0;
// Perform a specific verb, input is the full input line
virtual void perform_verb(std::string verb, std::string input) = 0;
};
class base_torch : public item
{
public:
std::vector<std::string> get_verbs() override
{
return { "light", "extinguish" };
}
// Return true if the torch is lit, false otherwise
bool is_lit();
void perform_verb(std::string verb, std::string) override
{
if (verb == "light")
{
// TODO: Make the torch "lit"
}
else
{
// TODO: Make the torch "extinguished"
}
}
};
class long_brown_torch : public base_torch
{
std::vector<std::string> get_names() override
{
return { "long brown torch", "long torch", "brown torch", "torch" };
}
void describe(player* p) override
{
p->write("This is a long brown torch.");
if (is_lit())
p->write("The torch is burning.");
}
};
然后如果玩家输入例如light brown torch
解析器查看所有可用物品(玩家物品栏中的物品,然后是房间中的物品),获取每个物品的名称列表(调用物品 get_names()
函数)并将其与brown torch
。如果找到匹配项,解析器将调用项目 perform_verb
函数并传递适当的参数 (item->perform_verb("light", "light brown torch")
).
您甚至可以修改解析器(和项目)来单独处理形容词,甚至像 the
这样的文章,或者保存上次使用的项目以便可以使用 it
引用它。
构建不同的房间和物品是乏味的,但一旦做出好的设计仍然是微不足道的(你真的应该花一些时间创建需求、分析需求和创建设计)。真正困难的部分是编写一个像样的解析器。
请注意,这只是在此类游戏中处理物品和动词的两种可能方式。其他方法还有很多,不一一列举。
你问了一些很好的问题。如何设计、构建和实现程序,以及如何对问题域建模。
OOP,'methods' 和方法
您提出的问题表明您已经了解了OOP(面向对象编程)。在很多关于 OOP 的介绍 material 中,通常鼓励直接通过对象和子类型对问题域进行建模,并通过向它们添加方法来实现功能。一个 class 典型的例子是动物建模,例如 Animal
类型和两个子类型,Duck
和 Cat
,以及实现功能,例如 walk
、quack
和 mew
.
直接使用对象和子类型对问题领域进行建模 是有意义的,但与简单地使用一个或几个不同类型的类型相比,它也可能非常过分和麻烦描述它是什么的字段。在您的情况下,我确实相信更复杂的建模,例如您对对象和子类型或替代方法的建模是有意义的,因为在其他方面,您的功能因类型而异以及有些复杂的数据(例如带有存货)。但要记住这一点 - 存在不同的权衡,有时,使用具有多个不同字段的单一类型来对域进行建模总体上更有意义。
通过基class和子类型上的方法实现所需的功能同样有不同的权衡,对于给定的情况,这并不总是一个好的方法.对于你的一个问题,你可以做一些事情,比如添加一个 print
方法或类似的基本类型和每个子类型,但这在实践中并不总是那么好(一个简单的例子是一个计算器应用程序,其中简化如果使用向基础 class.
(3*x)*4/2
)可能难以实现
替代方法 - 标记 unions/sum 类型
有一个非常好的基础抽象,称为 "tagged union" (it is also known by the names "disjoint union" and "sum type"). The main idea about the tagged union is that you have a union of several different sets of instances, where which set the given instance belongs to matters. They are a superset of the feature in C++ known as enum
. Regrettably, C++ does not currently support tagged unions, though there are research into it (for instance https://www.stroustrup.com/OpenPatternMatching.pdf ,但如果您是初级程序员,这可能有点超出您的能力)。据我所知,这与您在此处给出的示例非常吻合。 Scala 中的一个示例是(许多其他语言也支持标记联合,例如 Rust、Kotlin、Typescript、ML 语言、Haskell 等):
sealed trait Item {
val name: String
}
case class Book(val name: String) extends Item
case object Fire extends Item {
val name = "Fire"
}
case class Container(val name: String, val inventory: List[Item]) extends Item
据我所知,这很好地描述了您的各种物品。请注意,Scala 在这方面有点特殊,因为它通过子类型实现标记联合。
如果您随后想要实现一些打印功能,则可以使用 "pattern matching" 来匹配您拥有的项目并执行特定于该项目的功能。在支持模式匹配的语言中,这很方便且不脆弱,因为模式匹配会检查您是否涵盖了每种可能的情况(类似于 C++ 中的 switch
over enums 检查您是否涵盖了每种可能的情况)。例如在 Scala 中:
def getDescription(item: Item): String = {
item match {
case Book(_) | Fire => item.name
case Container(name, inventory) =>
name + " contains: (" +
inventory
.map(getDescription(_))
.mkString(", ") +
")"
}
}
val description = getDescription(
Container("Bag", List(Book("On Spelunking"), Fire))
)
println(description)
您可以在此处复制粘贴两个片段并尝试 运行 它们:https://scalafiddle.io/ .
这种建模与人们所谓的 "data types" 配合得很好,classes 本身没有或只有很少的功能,[=75=75] 中的字段=]es 基本上是它们接口的一部分("interface" 从某种意义上说,如果您添加、删除或更改类型的字段,您想更改使用这些类型的实现)。
相反,当 class 内部的实现不是其接口的一部分时,我发现更传统的子类型建模和方法更方便,例如,如果我有一个描述碰撞系统接口的基类型, 它的每个子类型都有不同的性能特征,可以在不同的情况下使用。隐藏和保护实现,因为它不是接口的一部分,这很有意义,并且非常适合人们所说的 "mini-modules".
在 C++(和 C)中,尽管缺乏语言支持,有时人们确实会以各种方式使用标记联合。我看到在 C 中使用的一种方法是创建一个 C 联合(尽管在内存和语义等方面要小心),其中使用 enum
标记来区分不同的情况。这很容易出错,因为您可能很容易最终访问一个 enum
案例中的字段,而该字段对该 enum
案例无效。
您还可以将命令输入建模为标记联合。也就是说,解析可能有点挑战性,如果您是初级程序员,解析库可能会涉及一些;保持解析简单可能是个好主意。
旁注
C++ 是一种特殊的语言——我不太喜欢它用于我不太关心资源使用或 运行时间性能等的情况,因为多种不同的原因,因为它可能很烦人并且开发起来不那么灵活。开发起来可能具有挑战性,因为您必须始终非常小心地避免 undefined behaviour。也就是说,如果资源使用或 运行 时间性能很重要,那么 C++ 可能是一个很好的选择,具体取决于具体情况。在 C++ 语言及其社区中也有许多非常有用和重要的见解,例如 RAII、所有权和生命周期。我的建议是学习 C++ 是个好主意,但您也应该学习其他语言,例如静态类型的函数式编程语言。 FP(函数式编程)和支持 FP 的语言,有许多优点和缺点,但它们的一些优点非常非常好,尤其是 reg.不变性和副作用。
在这些语言中,Rust 在某些方面可能是最接近 C++ 的,尽管我没有使用 Rust 的经验,因此不能保证语言或其社区。
作为旁注,您可能对此维基百科页面感兴趣:https://en.wikipedia.org/wiki/Expression_problem .