关于数据库:设计用于存储多人游戏的各种要求和统计数据的表格

Designing tables for storing various requirements and stats for multiplayer game

原始问题:

你好,

我正在创建非常简单的业余爱好项目-基于浏览器的多人游戏。我一直在设计用于存储任务/技能需求信息的表。

目前,我的桌子设计如下:

  • 表用户(用户基本信息)
  • 表统计(各种统计)
  • 表用户状态(用状态连接每个用户)
  • 小精灵

    另一个例子:

    • 表怪物(NPC敌人基本信息)
    • 表怪物的属性(将怪物与属性连接起来,使用上面相同的属性表)
    • 小精灵

      这些都是简单的案例。我必须承认,我在为不同的事情(例如任务)设计需求时遇到了困难。样本任务A可能只有最低的字符级别要求(这很容易实现),但另一个,任务B有很多其他要求(完成的任务,获得的技能,拥有特定的项目等),什么是设计存储此类信息的表格的好方法?

      以类似的方式-什么是存储技能要求信息的有效方法?(特定字符类、最小级别等)。

      我将感谢有关创建数据库驱动的游戏的任何帮助或信息。

      编辑:

      谢谢你的回答,但我想收到更多。由于我在设计一个相当复杂的可加工项目的数据库布局时遇到了一些问题,所以我开始对这个问题进行最大限度的奖励。

      我想收到文章/代码片段/任何与设计用于存储游戏数据的数据库的最佳实践相关联的东西的链接(此类信息的一个好例子是buildingbrowsergames.com)。

      如果有任何帮助,我将不胜感激。


      我会编辑这个来添加尽可能多的其他相关问题,尽管我希望OP能解决我上面的评论。我作为一名专业的在线游戏开发人员,从几年开始,到现在,作为一名业余在线游戏开发人员,我都在为它的价值说话。好的。

      网络游戏意味着某种持久性,这意味着你有两种广泛的数据类型——一种是由你设计的,另一种是由玩家在游戏过程中创建的。最有可能的情况是,您要将这两者都存储在数据库中。确保这些表不同,并通过常用的数据库规范化规则正确地交叉引用它们。(例如,如果你的玩家制作了一把大刀,你就不会创造出一整排具有大刀所有属性的新剑。您可以在player_items表中创建一个具有每个实例属性的新行,并参考item_types表中包含每个itemtype属性的Broadsword行。)如果您发现一行数据包含一些您设计的内容以及player在播放期间更改的内容,则需要将其正常化为两个表。好的。

      这确实是典型的类/实例分离问题,并且适用于此类游戏中的许多事情:地精实例不需要存储它意味着成为地精的所有细节(例如,绿色皮肤),只存储与该实例相关的事情(例如,位置、当前健康)。有时构造行为是微妙的,在这种情况下,需要基于类数据创建数据。(例如,根据地精类型的max health设置地精实例的起始健康状态。)我的建议是将这些硬编码到创建实例并为其插入行的代码中。这些信息很少改变,因为在实践中这样的价值观很少。(健康、体力、体力等可消耗资源的初始分数……就这样。)好的。

      尝试找到一个一致的术语来区分实例数据和类型数据-这将使以后的生活更容易,当你修补一个实时的游戏,并试图通过编辑错误的表来避免垃圾你的玩家的努力工作。这也使得缓存变得更加容易——通常,您可以不受惩罚地缓存类/类型数据,因为只有当您(设计人员)将新数据推送到那里时,它才会发生变化。你可以通过memcached运行它,或者如果你的游戏有一个连续的过程(即不是php/asp/cgi/etc),考虑在启动时加载它。好的。

      记住,一旦你上线,从你的设计端数据中删除任何东西都是有风险的,因为玩家生成的数据可能会引用它。在部署到活动服务器之前,在本地彻底测试所有内容,因为一旦它出现,就很难将其取下。考虑如何能够以安全的方式标记删除的数据行-可能是一个布尔"live"列,如果设置为false,则意味着它不会出现在典型查询中。想一想如果你禁用他们赢得的物品对玩家的影响(如果这些物品是他们支付的,则加倍)。好的。

      如果不知道你想如何设计你的游戏,就无法真正地回答真正的工艺方面。数据库设计必须遵循游戏设计。但我会想出一个小主意。也许你希望能够创造出一个基本的物体,然后用符文或者水晶或者其他东西来增加它。为此,您只需要在项目实例和扩充实例之间建立一对多的关系。(记住,你也可能有物品类型和增援类型表。)每个增援都可以指定物品的属性(例如耐久性、战斗中的最大伤害、重量)和修改器(通常是乘数,例如1.1以增加10%的加值)。你可以在这里和这里看到我关于如何实现这些修改效果的解释-相同的原则适用于临时技能和法术效果,同样适用于永久物品修改。好的。

      对于数据库驱动的游戏中的字符统计,我通常建议使用NA?每个统计的一列(整数或浮点)的ve方法。稍后添加列不是一个困难的操作,因为您将要大量读取这些值,所以可能不希望一直对它们执行联接。但是,如果您确实需要这种灵活性,那么您的方法就可以了。这与我下面建议的技能水平表非常相似:很多游戏数据都可以这样建模-将一个事物的类或实例映射到其他事物的类或实例,通常使用一些附加数据来描述映射(在这种情况下,是统计值)。好的。

      一旦您设置了这些基本联接(实际上是由于类/实例数据的分离而导致的任何其他复杂查询,而这种分离方式可能对您的代码不方便),请考虑在后台创建视图或存储过程来执行它们,这样您的应用程序代码就不必再担心它了。好的。

      当然,其他良好的数据库实践也适用——当您需要确保多个操作自动发生时(例如交易),在您最经常搜索的字段上放置索引,在安静期间使用vacuum/optimize table/whatever来保持性能,等等。好的。

      (在这一点下面的原始答案。)好的。

      老实说,我不会将任务需求信息存储在关系数据库中,而是存储在某种脚本中。最终,"需求"的概念呈现出几种不同的形式,可以利用不同类型的数据(例如,级别、类、已完成的先前任务、项目占有)和操作员(级别可能是最小或最大的,某些任务可能需要项目,而其他任务可能需要项目不在,等等),更不用说连词组合了。ON和析取(有些任务要求满足所有要求,而其他任务可能只要求满足其中的一个)。这类事情在命令式语言中更容易指定。这并不是说您在数据库中没有一个quest表,而是说您不尝试将有时任意的需求编码到模式中。我需要一个"脚本ID"列来引用外部脚本。我想您也可以将实际的脚本作为文本字段放入数据库中。好的。

      不过,技能要求适用于数据库,考虑到学习技能的典型游戏系统,当您在某个班级的各个级别中进行学习时,这些要求非常微不足道:好的。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      table skill_levels
      {
          int skill_id FOREIGN KEY;
          int class_id FOREIGN KEY;
          int min_level;
      }

      myPotentialSkillList = SELECT * FROM skill_levels INNER JOIN
          skill ON skill_levels.skill_id = skill.id
          WHERE class_id = my_skill
          ORDER BY skill_levels.min_level ASC;

      需要技能树吗?添加一列先决条件"技能ID"等。好的。好啊。


      更新:

      从评论来看,似乎很多人对XML有问题。我知道现在痛击它很酷,它确实有问题,但在这种情况下,我认为它是有效的。我选择它的另一个原因是有大量的库用于解析它,这样可以使生活更轻松。

      另一个关键概念是信息实际上是非关系的。所以可以,您可以将数据存储在任何特定示例中的一组不同的表中,这些表有很多连接,但这很麻烦。但是如果我继续给你一个稍微不同的例子,我打赌你必须无限地修改你的设计。我不认为添加表和修改复杂的SQL语句非常有趣。所以@scheibk的评论被投票否决有点令人沮丧。

      原岗位:

      我认为在数据库中存储任务信息可能存在的问题是,它不是真正的关系型的(也就是说,它不容易放在表中)。这可能就是为什么您在为数据设计表时遇到困难的原因。

      另一方面,如果你把你的任务信息直接输入到代码中,这意味着每次你想要添加任务时,你都必须编辑代码并重新编译。瘸腿的

      所以如果我是你,我可能会考虑将我的任务信息存储在一个XML文件或类似的文件中。我知道这是任何事情的通用解决方案,但在这种情况下,我觉得这是正确的。XML实际上是用于存储非关系和/或层次结构数据的,就像您在执行任务时需要存储的东西一样。

      总结:您可以想出自己的模式,创建XML文件,然后在运行时以某种方式加载它(甚至可以将XML存储在数据库中)。

      XML示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      <quests>
          <quest name="Return Ring to Mordor">
              <characterReqs>
                  <level>60</level>
                  <finishedQuests>
                      <quest name="Get Double Cheeseburger" />
                      <quest name="Go to Vegas for the Weekend" />
                  </finishedQuests>
                  <skills>
                      <skill name="nunchuks" />
                      <skill name="plundering" />
                  </skills>
                  <items>
                      <item name="genie's lamp" />
                      <item name="noise cancelling headphones for robin williams' voice />
                  </items>
              </characterReqs>
              <steps>
                  <step number="1">Get to Mordor</step>
                  <step number="2">Throw Ring into Lava</step>
                  <step number="3">...</step>
                  <step number="4">Profit</step>
              </steps>
          </quest>
      </quests>


      听起来您已经准备好接受一般的面向对象设计(OOD)原则了。我将故意忽略上下文(游戏、MMO等),因为这对如何进行设计过程并不重要。我给你链接比解释哪些术语对你自己最有帮助更没用,我会用粗体字写的。

      在OOD中,数据库模式直接来自您的系统设计,而不是相反。您的设计将告诉您基本对象类是什么,哪些属性可以位于同一个表中(与对象的关系为1:1),而哪些属性可以用于创建映射表(关系为1:n或n:m的任何对象-对于ExMaple,一个用户有多个统计信息,所以它是1:n)。事实上,如果您正确地执行OOD,那么对于最终的DB布局,您将没有任何决策要做。

      任何OO映射的"正确"方法都是作为一个称为"数据库规范化"的多步骤过程学习的。其基本原理正如我所描述的:找到对象关系(1:1,1:n,…)的"arity",并为1:n和n:m创建映射表。对于1:n,您最终得到两个表,"base"表和"base_subobjects"表(例如,您的"user s"和"user_stats"是一个很好的例子),将"foreign key"(基对象的ID)作为C。子对象映射表中的列。对于n:m,您最终会得到三个表:"base"、"subbjects"和"base_subbjects_map",其中map有一列作为基ID,一列作为子对象ID。在您的示例中,对于每个具有m需求的n个任务,这可能是必需的(因此需求条件可以在任务之间共享)。

      这是你需要知道的85%。剩下的就是如何处理继承,我建议你跳过,除非你是受虐狂。现在,在你开始编写代码之前,先弄清楚你希望它是如何工作的,剩下的就很容易了。


      @shea daniel的答案中的线程是正确的:一个任务的规范是非关系的,并且还包括逻辑和数据。

      使用XML或Lua就是例子,但更一般的想法是开发自己的领域特定语言来编码请求。以下是一些与游戏设计相关的关于这个概念的文章:

      • 特定领域语言的异想天开
      • 对行为使用特定于域的语言
      • 应用领域特定建模实现计算机游戏开发产业化
      • 小精灵

        您可以将给定任务的代码块存储到数据库中的TEXT字段中,但使用SQL查询特定部分的灵活性不大。例如,考虑到角色目前拥有的技能,哪些任务对他开放?如果查询先决条件是在TEXT字段中的DSL中编码的,那么在SQL中查询就不容易了。

        您可以尝试以关系的方式对各个先决条件进行编码,但它很快就失控了。关系型和面向对象型并不能很好地结合在一起。您可以尝试以这种方式建模:

        1
        Chars <--- CharAttributes --> AllAttributes <-- QuestPrereqs --> Quests

        然后执行LEFT JOIN,寻找角色属性中没有缺少任何先决条件的任何任务。以下是伪代码:

        1
        2
        3
        4
        5
        6
        SELECT quest_id
        FROM QuestPrereqs
         JOIN AllAttributes
         LEFT JOIN CharAttributes
        GROUP BY quest_id
        HAVING COUNT(AllAttributes) = COUNT(CharAttributes);

        但问题在于,现在你必须将角色的每一个方面(状态、技能、等级、所有物、完成的任务)建模为某种符合这种结构的抽象"属性"。

        这解决了跟踪任务先决条件的问题,但它给您留下了另一个问题:角色是以非关系的方式建模的,本质上是一个实体属性-值体系结构,它打破了一系列关系规则,使其他类型的查询异常困难。


        我会非常小心您在数据库中实际存储的内容,特别是对于mmorpg。请记住,这些东西被设计成拥有成千上万的用户,游戏代码必须执行得非常快,并通过网络向玩家发送大量的数据,不仅是在他们的家庭连接上,而且是在后端服务器之间。最后,您还必须扩展,数据库和扩展并不是两件我觉得很好混合的事情,特别是当您开始分片到不同的区域,然后向您的分片添加实例服务器时,等等。最终会有大量的服务器与数据库通信,并传递大量数据,其中一些甚至与游戏根本不相关(通向SQL服务器的SQL文本是无用的网络流量,您应该减少这些流量)。好的。

        这里有一个建议:将SQL数据库限制为只存储玩家玩游戏时会更改的内容。怪物和怪物的属性不会改变。项目和项目状态不会更改。任务目标不会改变。不要将这些内容存储在SQL数据库中,而是将它们存储在代码中的某个地方。好的。

        这样做意味着每一个存在的服务器都将永远知道所有这些信息,而不必查询数据库。现在,你根本不存储任务,你只存储玩家的成就,游戏程序决定了这些任务完成的影响。您不会浪费数据在服务器之间传输信息,因为您只发送事件ID或类似性质的内容(您可以通过仅使用足够的位来表示所有事件ID来优化传递的数据,这将减少网络流量。可能看起来无关紧要,但在大型网络应用程序中,没有什么是无关紧要的。好的。

        对怪物统计和物品统计做同样的事情。这些东西在游戏过程中不会改变,所以根本不需要将它们保存在数据库中,因此这些信息不需要通过网络传输。你存储的唯一东西是物品的ID或者怪物杀死或者类似的任何非确定性的东西(也就是说,它可以在游戏过程中以一种你无法预测的方式改变)。你可以有专用的物品服务器或怪物统计服务器,或者类似的服务器,如果你最终拥有大量占用过多内存的物品,你可以将这些物品添加到你的碎片中,然后将特定任务或区域所需的数据传递给处理该物品的实例服务器,以进一步减少SPAC。但是请记住,这将增加您通过网络来缓冲新实例服务器所需的数据量,因此这是一种权衡。只要你知道这种权衡的后果,你就可以运用良好的判断力来决定你想做什么。另一种可能性是将实例服务器限制在某个特定的任务/区域/事件/任何东西上,并且只为其提供足够的信息,使其满足其所负责的任务,但这更为复杂,而且可能会限制您的扩展,因为资源分配将变为静态而不是动态的(如果每个任务有50个服务器,并且突然之间每个人都在做同样的任务,你将有49个空闲服务器和一个非常繁忙的服务器)。同样,这是一种权衡,因此请确保您理解它,并为您的应用程序做出正确的选择。好的。

        一旦您确定了游戏中哪些信息是不确定的,那么您就可以围绕这些信息设计一个数据库。这变得更容易了:玩家有数据,玩家有物品,玩家有技能,玩家有成就等等,所有这些都很容易绘制出来。您不需要对技能、成就、项目等的描述,甚至不需要它们的效果、名称或任何东西,因为服务器可以在运行时根据这些东西的ID为您确定所有这些东西,而不需要数据库查询。好的。

        现在,对你来说,很多这样的事情听起来都是多余的。毕竟,一个好的数据库可以很快地进行查询。但是,即使在数据中心,您的带宽也是非常宝贵的,因此您需要将带宽的使用限制为只发送绝对必要的数据,并且仅在绝对必要发送数据时才发送该数据。好的。

        现在,为了用代码表示任务,我将考虑规范模式(http://en.wikipedia.org/wiki/specification_pattern)。这将使您能够轻松地根据需要哪些事件来构建任务目标,以确保满足完成该任务的规范。然后,你可以使用Lua(或其他东西)来定义你的任务,当你构建游戏时,这样你就不必进行大规模的代码修改,重新构建整个该死的东西来实现它,这样你就必须杀死11个怪物而不是10个,才能在一个特定的任务中获得1000条真理的宝剑。我认为,如何真正做到这一点超出了这个答案的范围,并且开始触及我对游戏编程知识的边缘,所以如果你选择走这条路,这里的其他人也许可以帮助你。好的。

        另外,我知道我在这个答案中使用了很多术语,请问你是否有不熟悉的地方,我可以解释一下。好的。

        编辑:没有注意到你添加的关于可加工物品的内容。我假设这些是玩家可以在游戏中专门创建的东西,比如定制物品。如果一个播放器可以不断地改变这些项目,那么你可以在运行时组合它们所设计的属性,但是你需要将每个属性的ID存储在数据库的某个地方。如果您可以添加有限数量的内容(如Diablo II中的gems),那么只需将这些列添加到表中,就可以消除连接。如果有有限数量的项目可以被精心设计,并且有有限数量的方法可以将不同的东西组合成新的项目,那么当某些项目组合时,您不需要存储组合的属性;它只是成为一个新的项目,已经在某个点上由您定义了。然后,他们只拥有那个项目而不是它的组件。如果你明确你的游戏行为,我可以添加额外的建议,如果这是有用的。好的。好啊。


        关于您的基本结构,您可能(取决于您的游戏的性质)想要考虑驱动向玩家角色和非玩家角色之间的表示收敛,这样自然地在两者上操作相同的代码就不必担心区别。这就意味着,不必使用usermonster表,而是使用character表来表示PC和NPC的所有共同点,然后使用user表来表示PC和/或用户帐户特有的信息。user表将有一个character_id外键,您可以通过存在对应于该表的user行来判断播放器字符行。

        对于像您这样的模型中的任务表示,我的方式如下:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        quest_model
        ===============
        id
        name ['Quest for the Holy Grail', 'You Killed My Father', etc.]
        etc.

        quest_model_req_type
        ===============
        id
        name ['Minimum Level', 'Skill', 'Equipment', etc.]
        etc.

        quest_model_req
        ===============
        id
        quest_id
        quest_model_req_type_id
        value [10 (for Minimum Level), 'Horseback Riding' (for Skill), etc.]

        quest
        ===============
        id
        quest_model_id
        user_id
        status
        etc.

        因此,quest_model是quest结构的核心定义;每个quest_model可以有0..n个相关的quest_model_req行,这些行是特定于该quest模型的需求。每个quest_model_req都与一个quest_model_req_type相关联,它定义了一般的需求类型:达到最低水平、拥有技能、拥有一件设备等。quest_model_req也有一个value,它配置了这个特定任务的需求;例如,最低级别类型的需求可能有一个value20,这意味着你必须至少达到20级。

        那么,quest表是玩家正在进行或已经进行的任务的个别实例。questquest_modeluser有关(如果你希望NPC能够完成任务,也可以与character有关!)并有一个status指示任务的进展,以及其他任何有用的跟踪。

        这是一个简单的结构,当然,必须构建以适应特定游戏的需要,但它应该说明我建议的方向。

        哦,既然有人放弃了他们的证书,我的是,我已经16年来在面对公众的直播项目上成为了一个业余游戏开发商。


        与数据库的设计没有直接关系,但几周前有人问过类似的问题,关于RPG的类图示例。

        我相信你能在那里找到有用的东西:)


        我将从面向对象的角度,而不是以数据为中心的角度来处理这个问题。看起来您可能有很多(poss-complex)对象——我建议您首先对它们进行建模(使用它们的关系),并依靠ORM进行持久性。


        只是一些需要考虑的小问题:

        1)始终尽量使您的"获取任务"要求简单。"完成任务"的要求很复杂。

        第1部分可以通过"尝试按层次顺序进行任务"来完成:
        例子:

        questa:(杀死乌鸦恶魔)(quest req:lvl1)
        问题1:在森林中保存"未知"以获取一些信息。(quest req:questa)
        问题2:制作水晶之剑……等。。(quest req:questa.1==完成)
        问题3:…等。。(quest req:questa.2==完成)
        问题4:…等。。(quest req:questa.3==完成)
        等。。。< BR>questb(寻找丢失的坟墓)(quest req:(questa.captures==done))
        questc(前往魔鬼超市)(quest req:(questa.captures==done&;player.level==10)
        等。。。。

        这样做可以节省大量的数据字段/表关节。

        其他想法:
        如果您使用上述系统,您可以在您的任务表中添加一个额外的奖励字段,名为"启用任务",并添加需要启用的任务的名称。
        逻辑上。。您将为每个任务分配一个"启用"字段。

        2)为您的工艺问题提供一个小的解决方案,创建工艺配方,其中包含存储在其中的工艺项目工艺要求。所以当玩家试图制作一个物品时……他需要先买个菜谱。然后试着做工……此类项目的一个简单示例如下:
        项目名称:"传说中的死亡之剑"
        工艺等级要求:75
        所需项目:
        物品_1:Blade of the Dead
        项目:a Cursed Seal
        物品:Holy gemstone of the dead
        等。。。

        当他按下"craft"操作时,你可以解析它并与他的库存/工艺箱进行比较…

        所以你的制作数据库只有1个字段(或者如果你想添加一个制作等级请求,只有2个字段)。尽管它已经包含在配方中了。

        其他想法:
        这些项可以以XML格式存储在表中。这将使解析更容易…

        3)类似的XML系统可以应用于您的查询系统。执行任务结束要求。


        我做过类似的事情,我的一般解决方案是使用大量的元数据。我松散地使用这个术语是指,任何时候我需要新的数据来做一个给定的决定(允许一个任务,允许使用一个项目等),我都会创建一个新的属性。这基本上只是一个具有任意数量值和描述的表。然后每个字符都会有这些类型属性的列表。

        例如:杀戮列表、等级、访问区域等。

        这对您的开发过程有两件事:

        1)每次游戏中有一个事件,你都需要一个大的旧的开关块来检查所有这些属性类型,看看是否有什么需要更新。

        2)每次需要一些数据时,在添加新的属性表之前检查所有属性表。

        我发现这是一个很好的快速发展战略,一个有机增长的游戏(不是完全提前在纸上计划好的)-但它的一个大限制是,您的过去/当前内容(级别/事件等)将不兼容未来的属性-即,该地图不会给您一个区域徽章,因为没有区域徽章。当你编码的时候。当然,这要求您在向系统添加新属性时更新过去的内容。


        如果我正在为这种情况设计数据库,我可能会这样做:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        Quest
            [quest properties like name and description]
            reqItemsID
            reqSkillsID
            reqPlayerTypesID
        RequiredItems
            ID
            item
        RequiredSkills
            ID
            skill
        RequiredPlayerTypes
            ID
            type

        在这里,ID映射到相应的表,然后检索该ID下的所有条目,以获得所需项目、技能和所拥有内容的列表。如果允许动态创建项,那么应该有一个到包含所有可能项的另一个表的映射。

        要记住的另一件事是规范化。这里有一篇很长的文章,但我已经将前三个级别大致压缩为以下几级:

        • 第一种正常形式意味着没有数据库条目,其中特定字段包含多个项。
        • 第二种正常形式意味着,如果您有一个复合主键,那么所有其他字段都完全依赖于整个键,而不仅仅是每个表中的部分键。
        • 第三种正常情况是没有依赖于任何表中其他非键字段的非键字段。
        • 小精灵

          [免责声明:我在SQL数据库方面的经验非常少,对这个领域还很陌生。我只是希望我能帮上忙。]


          另一种选择是某种规则引擎(DROLLS,例如,如果您使用Java)。


          当您遇到以数据为中心的问题时,数据库就是您的朋友。到目前为止你所做的似乎都是对的。

          另一方面,你提到的其他问题似乎是以行为为中心的。在这种情况下,面向对象的分析和解决方案会更好地工作。

          例如:用特定的Quest子类创建一个Quest类。每个孩子都应该实现一个bool HasRequirements(Player player)方法。