【问题标题】:Text Adventure Game - How to Tell One Item Type from Another and How to Structure the Item Classes/Subclasses?文字冒险游戏 - 如何区分一个项目类型和另一个项目类型以及如何构建项目类/子类?
【发布时间】:2020-06-09 08:21:04
【问题描述】:

我是一名初级程序员(拥有大量与电子游戏设计相关的脚本编写经验,但编程经验很少 - 所以只是基本的东西,如循环、流控制等 - 虽然我确实有 C++ 基础和我的 C++ 数据结构和算法课程)。我正在从事一个文本冒险的个人项目(实际上,在我了解类如何工作之前,我已经用 Python 编写了它——一切都是字典——所以这很可耻)。我正在 C++ 中“改造”它,使用课程摆脱只完成家庭作业的陈规。

我已经编写了我的播放器和房间类(这很简单,因为我只需要一个类)。我正在研究物品类别(物品是房间中的任何东西,例如手电筒、火、标志、容器等)。我不确定如何处理项目基类和派生类。这是我遇到的问题。

  1. 我如何不费吹灰之力地判断一个项目是否属于某种类型(这很有可能是我想多了)?

    • 例如,我设置了我的打印室信息功能,以便除了它可能执行的任何其他操作之外,它还打印其库存中的每个对象的名称(即其中的),我希望它打印一些特殊的东西一个容器对象(例如其库存的内容)。
    • 第一部分很简单,因为每个项目都有一个名称,因为名称属性是基本项目类的一部分。容器有一个清单,这是容器子类独有的属性。
    • 据我了解,基于对象的类类型执行条件逻辑是一种不好的形式(因为一个类应该是多态的),我假设(可能是错误的)放置 getHasInventory 访问器会很奇怪和错误项目基类中的虚函数(我的假设是基于认为在基类中为每个派生类放置虚函数会很疯狂 - 我有大约十几个派生类 - 其中一些是派生的派生类类)。
    • 如果这一切都正确,那么可接受的方法是什么?一件显而易见的事情是向基础添加一个 itemType 属性,然后执行条件逻辑,但这让我觉得是错误的,因为它似乎只是对检查类类型解决方案的重新设计。我不确定上述假设是否正确以及有什么好的解决方案。
  2. 我应该如何构建我的基类/类和派生类?

    • 我最初编写它们时,项目类是基类,而大多数其他类使用单继承(除了具有多级的一对)。
    • 这似乎表现出一些尴尬和重复我自己。例如,我想要一个标志和一封信。标志是可读物品 > 不可取物品 > 物品。一封信是一个可读项目 > 可携带项目 > 项目。因为它们都使用单一继承,所以我需要两个不同的可读项,一个是可接收的,一个不是(我知道在这种情况下我可以将可接收和不可接收的属性设置为基础的属性,我做到了,但这只是一个例子,因为我仍然与其他类有类似的问题)。
    • 这对我来说似乎很恶心,所以我又尝试了一次,并使用多重继承和虚拟继承来实现它们。就我而言,这似乎更灵活,因为我可以组合多个类并为我的类创建一种组件系统。
    • 其中一种方法比另一种更好吗?有没有更好的第三种方法?

【问题讨论】:

    标签: c++ game-development adventure


    【解决方案1】:

    你在问一些很好的问题。如何设计、构建和实现程序,以及如何对问题域进行建模。

    OOP、“方法”和方法

    您提出的问题表明您已了解 OOP(面向对象编程)。在许多关于 OOP 的介绍性材料中,通常鼓励通过对象直接对问题域进行建模,并通过向它们添加方法来实现功能的子类型化和实现。一个经典的例子是为动物建模,例如一个Animal 类型和两个子类型DuckCat,并实现功能,例如walkquackmew

    直接使用对象和子类型对问题域建模是有意义的,但与简单地使用具有不同字段的单个或几个类型来描述它是什么相比,它也可能非常过分和麻烦.在您的情况下,我确实相信像您使用对象和子类型或替代方法这样更复杂的建模是有意义的,因为在其他方面,您的功能取决于类型以及有些复杂的数据(例如带有存货)。但要记住这一点 - 存在不同的权衡,有时,使用具有多个不同字段的单一类型来对域进行建模总体上更有意义。

    实现通过基类和子类型上的方法实现所需的功能同样有不同的权衡,对于给定的情况,这并不总是一个好的方法。对于您的一个问题,您可以执行类似添加print 方法或类似于基本类型和每个子类型的操作,但这在实践中并不总是那么好(一个简单的示例是简化算术的计算器应用程序)用户输入的表达式(如(3*x)*4/2)如果使用向基类添加方法的方法,实现起来可能会很麻烦)。

    替代方法 - 标记联合/总和类型

    有一个非常好的基本抽象被称为"tagged union"(它也被称为“不相交联合”和“总和类型”)。关于标记联合的主要思想是,您有几个不同实例集的联合,其中给定实例属于哪个集合。它们是 C++ 中称为enum 的功能的超集。遗憾的是,C++ 目前不支持标记联合,尽管有对其进行研究(例如 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 并通过枚举检查您是否涵盖了每种可能的情况)。例如在 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)
    

    您可以在此处复制粘贴两个 sn-ps 并尝试运行它们:https://scalafiddle.io/

    这种建模非常适用于所谓的“数据类型”,类本身没有或只有很少的功能,类中的字段基本上是其接口的一部分(“接口”从某种意义上说,如果您添加、删除或更改类型的字段,您希望更改使用这些类型的实现)。

    相反,当类内部的实现不是其接口的一部分时,我发现更传统的子类型建模和方法更方便,例如,如果我有一个描述碰撞系统接口的基类型,以及它的每个子类型具有不同的性能特点,适用于不同的情况。隐藏和保护实现,因为它不是接口的一部分,这很有意义,并且非常适合人们所说的“迷你模块”。

    在 C++(和 C)中,尽管缺乏语言支持,有时人们还是会以各种方式使用标记联合。我看到在 C 中使用的一种方法是创建一个 C 联合(尽管要小心注册方面,例如内存和语义),其中使用 enum 标记来区分不同的情况。这很容易出错,因为您可能很容易在一个 enum 案例中访问一个字段,而该字段对于该 enum 案例无效。

    您还可以将命令输入建模为标记联合。也就是说,解析可能会有些挑战,如果您是初学者,解析库可能会有点涉及;保持解析有点简单可能是个好主意。

    旁注

    C++ 是一种特殊的语言 - 我不太喜欢它,因为我不太关心资源使用或运行时性能等等,因为多种不同的原因,因为它可能很烦人并且开发起来不那么灵活。并且在其中进行开发可能具有挑战性,因为您必须始终小心避免undefined behaviour。也就是说,如果资源使用或运行时性能确实很重要,C++ 可以根据情况成为一个非常好的选择。 C++ 语言及其社区中还有许多非常有用和重要的见解,例如 RAII、所有权和生命周期。我的建议是学习 C++ 是一个好主意,但你也应该学习其他语言,例如静态类型的函数式编程语言。 FP(函数式编程)和支持 FP 的语言,有许多优点和缺点,但它们的一些优点非常非常好,尤其是 reg。不变性和副作用。

    在这些语言中,Rust 可能在某些方面最接近 C++,尽管我没有使用 Rust 的经验,因此不能保证该语言或其社区。​​p>

    作为旁注,您可能对此维基百科页面感兴趣:https://en.wikipedia.org/wiki/Expression_problem

    【讨论】:

      【解决方案2】:

      解决问题的一种可能方法是多态性。通过使用多态性,您可以(例如)拥有一个 describe 函数,该函数在调用时会引导项目向玩家描述自身。您可以对use 和其他常用动词执行相同操作。


      另一种方法是实现更高级的输入解析器,它可以识别对象并将动词传递给项目的某些(多态)函数以供自己处理。例如,每个项目可以有一个返回可用动词列表的函数,以及一个返回项目“名称”列表的函数:

      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-&gt;perform_verb("light", "light brown torch"))。

      您甚至可以修改解析器(和项目)以单独处理形容词,甚至像the 这样的文章,或者保存最后使用的项目,以便使用it 引用它。

      构建不同的房间和物品是乏味的,但一旦做出了好的设计(你真的应该花一些时间来创建需求、分析需求和创建设计)。真正困难的部分是编写一个像样的解析器。


      请注意,这只是在此类游戏中处理项目和动词的两种可能方式。还有很多其他方法,很多人都一一列举。

      【讨论】:

      • 我很欣赏解释和详细的纲要。虽然我接触过多态性的概念,但我并没有真正看到它的实际应用。这有助于我了解如何将它用于项目类。我以前的思维方式已经如此固定,以至于我正在考虑使用外部打印室信息功能的想法来解决问题,该功能打印项目信息并且从未想过让项目描述自己。现在看来如此明显。感谢您帮助我克服了这个障碍。
      • @slucas3816 拥有外部打印室信息功能不一定是一个坏方法,尽管它确实取决于具体情况和您使用的编程语言。
      • @MelvinWM 这就是我在 Python 中构建它时的构建方式,一切都是字典。所有的项目都只是字典,其中一个键值对是项目的类型(只是我给它的一个字符串,比如“容器”),所以我可以只检查类型,只为类型做有效的事情,这完全工作,但我肯定会让任何真正的程序员畏缩。我在构建它时甚至比现在更不知道自己在做什么(没有编程课程),但我确实设法构建了一个保存/加载系统和一个识别动词、名词、介词、形容词和冠词的文本解析器。
      • @slucas3816 保存/加载系统和一个文本解析器,这非常好,实际上:)。如果没有静态类型检查和良好的语言支持,我相信这种检查类型的方法可能有点脆弱。维护等因素。但是有了这些,这种方法实际上是不错的。我想知道一个有趣的学习实验(可能在你构建了 C++ 程序之后)是否会将你的 Python 程序直接移植到诸如 Haskell、OCaml、F#、Scala、Rust、Typescript 之类的语言,但是你使用标记的联合和项目的模式匹配。
      • @slucas3816 当然,这样的语言可能还包括使用不可变数据结构和集合,这可能会使直接移植它的范围有点太大,所以如果你认为它可能很有趣并且有利可图对于学习,等到你稍微涉足函数式编程之后再学习可能是有意义的。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-01-23
      • 2012-07-08
      • 2021-08-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多