65.9K
CodeProject 正在变化。 阅读更多。
Home

战利品表、随机地图和怪物 - 第二部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (24投票s)

2012年7月16日

CPOL

10分钟阅读

viewsIcon

54113

downloadIcon

977

让 RDS 活起来 - 它如何工作。

引言

您已经通过第一部分枯燥的理论学习,现在想看看它是如何运行的吗?欢迎来到 RDS 文章的第二部分!

我现在将通过一系列小演示向您展示“如何操作”(所有演示代码都可以在可下载的源代码中找到)——这是一个简单的控制台应用程序,正如我在第一部分承诺的那样,“没有花哨的图形,没有设计器,只有代码”,它将输出 RDS 的结果。

演示 1 - 一个简单的“6选2”表格

我们创建一个 RDSTable 对象,向表中添加 6 个条目,然后让 RDS 随机选择两个。然后我们对表格进行一些操作,将其中一个条目设置为 rdsAlways=true,这样它每次查询时都会被包含在结果中。尝试更改条目的概率,看看掉落如何变化。

代码非常简单明了:创建一个 RDSTable,添加 6 个条目,并将 rdsCount=2。这将使系统从 6 个中抽取 2 个。 (您看,手动添加条目不是未来的方法。需要一个设计器工具来提供一个良好的支持 GUI,以便在运行时设置和修改您的表格,并直接从文件或数据库加载它们)。

RDSTable t = new RDSTable();
// Add 6 items with equal probability to the table
t.AddEntry(new MyItem("Item 1"), 10);
t.AddEntry(new MyItem("Item 2"), 10);
t.AddEntry(new MyItem("Item 3"), 10);
t.AddEntry(new MyItem("Item 4"), 10);
t.AddEntry(new MyItem("Item 5"), 10);
MyItem m6 = new MyItem("Item 6"); // We need this item later
t.AddEntry(m6, 10);
// Tell the table we want to have 2 out of 6
t.rdsCount = 2;
// First demo: Simply loot 2 out of the 6
Console.WriteLine("Step 1: Just loot 2 out 6 - 3 runs");
for (int i = 0; i < 3; i++)
{
 Console.WriteLine("Run {0}", i + 1);
 foreach (MyItem m in t.rdsResult)
  Console.WriteLine("    {0}", m);
}
// Now set Item 6 to drop always
m6.rdsAlways = true;
Console.WriteLine("Step 2: Item 6 is now set to Always=true - 3 runs");
for (int i = 0; i < 3; i++)
{
 Console.WriteLine("Run {0}", i + 1);
 foreach (MyItem m in t.rdsResult)
  Console.WriteLine("    {0}", m);
}

这是演示 1 的输出(由于这是一个随机系统,您运行演示时输出可能会有所不同)

*** DEMO 1 STARTED ***
----------------------
Step 1: Just loot 2 out 6 - 3 runs
Run 1
    Item 3
    Item 1
Run 2
    Item 6
    Item 2
Run 3
    Item 4
    Item 5
Step 2: Item 6 is now set to Always=true - 3 runs
Run 1
    Item 6
    Item 2
Run 2
    Item 6
    Item 2
Run 3
    Item 6
    Item 4
-----------------------
*** DEMO 1 COMPLETE ***

演示 2 - 简单的递归。一个包含三个表格的表格,并使用 rdsUnique = true

设置了一个简单的递归结构

RDSTable t = new RDSTable();
RDSTable subtable1 = new RDSTable();
RDSTable subtable2 = new RDSTable();
RDSTable subtable3 = new RDSTable();
t.AddEntry(subtable1, 10); // we add a table to a table thanks to the interfaces
t.AddEntry(subtable2, 10);
t.AddEntry(subtable3, 10);
subtable1.AddEntry(new MyItem("Table 1 - Item 1"), 10);
subtable1.AddEntry(new MyItem("Table 1 - Item 2"), 10);
subtable1.AddEntry(new MyItem("Table 1 - Item 3"), 10);
subtable2.AddEntry(new MyItem("Table 2 - Item 1"), 10);
subtable2.AddEntry(new MyItem("Table 2 - Item 2"), 10);
subtable2.AddEntry(new MyItem("Table 2 - Item 3"), 10);
subtable3.AddEntry(new MyItem("Table 3 - Item 1"), 10);
subtable3.AddEntry(new MyItem("Table 3 - Item 2"), 10);
subtable3.AddEntry(new MyItem("Table 3 - Item 3"), 10);

第一步,您可以看到递归发生;第二步,我们将计数增加到 10,并将表格 2 设置为 rdsUnique=true。您可以看到,所有表格都多次命中,但结果集中只有 1 条来自表格 2 的记录(无论表格 2 包含多少条目,甚至更多子表格!)。

您会看到,即使我们将 rdsCount=10,结果中也不一定总是有 10 个条目!原因是 rdsUnique=true,因为 RDS 会跳过来自表格 2 的所有后续命中。这就是为什么您得到的结果计数比预期的要少。

Step 1: Loot 3 items - 3 runs
Run 1
    Table 2 - Item 1
    Table 1 - Item 3
    Table 2 - Item 2
Run 2
    Table 3 - Item 2
    Table 2 - Item 1
    Table 3 - Item 1
Run 3
    Table 2 - Item 3
    Table 2 - Item 1
    Table 2 - Item 3
Step 2: Table 2 is now unique, loot 10 items - 3 runs
Run 1
    Table 1 - Item 2
    Table 2 - Item 2
    Table 3 - Item 1
    Table 3 - Item 3
    Table 1 - Item 3
    Table 1 - Item 3
    Table 1 - Item 3
Run 2
    Table 1 - Item 1
    Table 2 - Item 2
    Table 3 - Item 2
    Table 3 - Item 1
    Table 3 - Item 1
    Table 3 - Item 2
    Table 3 - Item 2
Run 3
    Table 2 - Item 3
    Table 1 - Item 1
    Table 3 - Item 2
    Table 3 - Item 1
    Table 3 - Item 2
    Table 1 - Item 2

演示 3 - 动态公式。运行时更改概率

捕获 PreResultEvaluation,并在计算结果之前修改参数。对于此演示,我们派生了一个名为 MyItemDemo3 的类。该类将覆盖 RDSObject 基类的 OnPreResultEvaluation 方法,并基于一个简单公式动态修改概率:每次请求结果时,我们的概率会增加 5%,直到命中为止。命中后,概率将重置为默认值 1。

MyItemDemo3 将在构造函数中根据参数“dynamic”设置自己的概率,然后覆盖两个事件(PreHit)来控制概率,并在命中时进行输出。

如果项目是动态的,它以概率 1 开始,否则以 100 开始。这是为了演示增加概率直到项目最终命中。

public class MyItemDemo3 : MyItem
{
 public MyItemDemo3(string name, bool isdynamic)
  : base(name)
 {
  mdynamic = isdynamic;
  rdsProbability = (mdynamic ? 1 : 100);
 }
 private bool mdynamic = false;
 public override void OnRDSPreResultEvaluation(EventArgs e)
 {
  // My probability increases by 5% with every query until i get hit...
  if (mdynamic)
  {
   rdsProbability *= 1.05;
  }
 }
 public override void OnRDSHit(EventArgs e)
 {
  // i am hit! Reset to default probability
  if (mdynamic)
  {
   rdsProbability = 1;
   Console.WriteLine("Dynamic hit! Reset probability to 1");
  }
 }
...
...
...

此演示的运行代码如下:我们设置一个简单的表格,包含 5 个条目,其中一个就是动态条目。然后我们循环遍历结果,直到动态条目被命中。

Loot until we hit the dynamic item
Dynamic is now: Item 1 @ 1,0000
Loot: Item 2
Dynamic is now: Item 1 @ 1,0500
Loot: Item 3
Dynamic is now: Item 1 @ 1,1025
Loot: Item 2
Dynamic is now: Item 1 @ 1,1576
Loot: Item 3
Dynamic is now: Item 1 @ 1,2155
Loot: Item 4
Dynamic is now: Item 1 @ 1,2763
Loot: Item 4
Dynamic is now: Item 1 @ 1,3401
Loot: Item 4
Dynamic is now: Item 1 @ 1,4071
Loot: Item 4
Dynamic is now: Item 1 @ 1,4775
Loot: Item 5
...
...
...
Dynamic is now: Item 1 @ 38,8327
Loot: Item 2
Dynamic is now: Item 1 @ 40,7743
Loot: Item 4
Dynamic is now: Item 1 @ 42,8130
Loot: Item 3
Dynamic is now: Item 1 @ 44,9537
Loot: Item 5
Dynamic is now: Item 1 @ 47,2014
Loot: Item 3
Dynamic is now: Item 1 @ 49,5614
Dynamic hit! Reset probability to 1
Loot: Item 1 @ 1,0000

演示 4 - 创建(生成)一组怪物,甚至可能有一个稀有怪物?

好的,我们需要一群哥布林。紧急!萨满、战士,如果运气好的话,还有全能的 BOB!从第一次听说起,全世界都恐惧的哥布林! 微笑。让我们找出如何创建一组随机设置的怪物。此演示展示了 RDSCreatableObject 类的用法。

此演示的准备工作包括创建一个“Goblin”基类(它基本上与其他演示中的“MyItem”类相同),我们从中派生出 WarriorShaman。全能的 BOB,我们稀有的怪物当然会是一个 Warrior,所以我们派生 BOB : Warrior。然后我们设置一个 RDSTable,其中包含 5 个 Shaman,5 个 Warrior...以及 BOB

为什么每个职业 5 个?因为我想在演示中展示一种可能的怪物生成不同等级的方法。对于演示,我们将变量“AreaLevel = 10”设置为我们想要生成怪物组的区域等级。然后我们添加 1 个 Shaman,等级为 AreaLevel-21 个等级为 AreaLevel-1,1 个等级与 AreaLevel 相同,以及 1 个等级为 +1+2。战士也是如此。+2/-2 的怪物掉落的概率较低,而偶数等级的怪物概率最高。

最后但同样重要的是,我们以显著较低的概率添加 BOBBOB 当然是 rdsUnique...只能有一个 BOB。

玩玩这个演示,反复运行,直到您终于遇到 BOB。看看怪物群的等级和类型(ShamanWarrior)分布如何,您会发现,这会生成完全随机的 10 个萨满组成的组。

也许您想增强此演示,使生成的 Goblin 的数量也随机。尝试添加一个 NullValue 或设置另一个 RDSValue<T> 对象表(或者只是掷骰子)来确定该表的 rdsCount

这是演示 4 的一个可能的输出,显示了基于其概率设置的 Goblin 的不同等级。

Enter Area Level: 20
Spawning Goblins in a Level 20 area:
Shaman - Level 20
Warrior - Level 20
Shaman - Level 22
Shaman - Level 21
Shaman - Level 20
Warrior - Level 22
Shaman - Level 20
Shaman - Level 20
Warrior - Level 20
Shaman - Level 22

您可以看到一群分布良好的随机 Goblin,在这种情况下,萨满的数量略多于战士,但下一组很可能是一堆战士,几乎没有萨满...

通过在此演示中输入零作为区域级别,您可以循环,直到 BOB 最终在一个哥布林群中被发现。输出看起来像这样。

Enter 0 as area level to loop random levels until you hit BOB!
Enter Area Level: 0
BOB IS HERE! ON YOUR KNEES, WORLD! *haaarharharhar*
BOB found in group #281 in a Level 30 area:
Warrior - Level 28
BOB - Level 60
Warrior - Level 28
Shaman - Level 32
Warrior - Level 32
Shaman - Level 29
Warrior - Level 28
Warrior - Level 30
Shaman - Level 29
Warrior - Level 30

这是创建此演示的一些代码。看看 Goblin 及其对 rdsCreateInstance() 的重写。这将向结果集返回一个新的 Goblin,因此包含的每个 Monster 都是其自己的、活动的实例。

此演示中的 Goblin 被创建得尽可能简单。

// The Goblin base class needs at least a level
// In a real game scenario you will likely have a 
// base class "Monster" or even "NPC", which will
// have the level. For this Demo, a Goblin is enough.
public class Goblin : RDSCreatableObject
{
 public Goblin(int level) { Level = level; }
 
 public int Level = 0;
 public override string ToString()
 {
  return this.GetType().Name + " - Level " + Level.ToString();
 }
}

三个 Goblin 从此类派生,在此演示中它们看起来都一样,所以我只展示 Shaman 作为代表。

public class Shaman : Goblin
{
 public Shaman(int level) : base(level) { }
 public override IRDSObject rdsCreateInstance()
 {
  return new Shaman(Level);
 }
}

此演示中的新内容是 GoblinTable 类。我们不直接使用 RDSTable,而是从它派生,添加一个自定义构造函数,并在派生表中添加条目。看看不同的等级、概率以及 BOB 出现的极低几率。

 public class GoblinTable : RDSTable
 {
  public GoblinTable(int arealevel)
  {
   // Shamans with different level based on the arealevel
   // With a probability curve peaked at the area level
   AddEntry(new Shaman(arealevel - 2), 100);
   AddEntry(new Shaman(arealevel - 1), 200);
   AddEntry(new Shaman(arealevel    ), 500);
   AddEntry(new Shaman(arealevel + 1), 200);
   AddEntry(new Shaman(arealevel + 2), 100);
   // Same for Warriors
   AddEntry(new Warrior(arealevel - 2), 100);
   AddEntry(new Warrior(arealevel - 1), 200);
   AddEntry(new Warrior(arealevel    ), 500);
   AddEntry(new Warrior(arealevel + 1), 200);
   AddEntry(new Warrior(arealevel + 2), 100);
   // BOB is double the arealevel - a real hard one!
   AddEntry(new BOB(arealevel * 2), 1);
   rdsCount = 10;
  }
 }

我想,如果到目前为止还没有发生,那么现在您应该看到了这个库在设计随机内容方面提供的一些强大功能和便利性!

演示 5 - 玩转 RDSValue<T>。随机金币掉落和其他值

终于。BOB 死了!他掉了什么?他到底有多富有?

此简短演示中导入的部分是,您派生了一个名为 RDSValue<T> 的类来包含金币掉落。当构造时,该值会根据构造函数参数 AreaLevelMobLevelPlayerLevel 进行计算。

所采用的公式是:基础金币数量为 10 * AreaLevel。现在加上/减去 MonsterLevel-AreaLevelAreaLevel-Playerlevel(以惩罚低级别区域的高等级玩家)。您也可以在此处使用一些随机公式,我只想展示值的动态分配,并介绍一下 RDSValue<T>

Enter Area Level: 20
Enter Monster Level: 22
Enter Player Level: 24
Querying Gold drop: 198,00

这确实是一个非常简短简单的演示,仅用于展示对 RDSValue<T> 对象的访问以及您可以对其做什么。尝试使用一些 RDSValues,我相信您会找到很多使用场景。

// This table contains only 1 entry to demonstrate the RDSValue<T> class.
// In a real scenario, a gold drop is only one of many entries for the loot
// of a mob of course.
RDSTable gold = new RDSTable();
gold.AddEntry(new GoldDrop(baselevel, moblevel, playerlevel), 1);
Console.WriteLine("Querying Gold drop: " + 
     ((GoldDrop)gold.rdsResult.First()).rdsValue.ToString("n2"));
public class GoldDrop : RDSValue<double>
{
 public GoldDrop(int arealevel, int moblevel, int playerlevel):
  base(0, 1)
 {
  rdsValue = 10 * arealevel + (moblevel - arealevel) + (arealevel - playerlevel);
 }
}

演示 6 - 随机生成一个简单的地图

一个简短的演示,随机选择 25 个地图块来创建一个 5x5 的地图。您当然可以使用此系统创建任何地图大小。

这里的设置是为了演示一项新技术:在 PreResult 重写中,根据地图片段的出口动态启用和禁用单个表格中的条目。

我们创建了一个名为 MiniDungeon : RDSTable 的类。此表格包含许多 MapSegment 对象,它们派生自 RDSObject。每个段都有四个出口:NorthEastSouthWest。这些布尔标志代表 Segment 的可能出口,并用于修改 MiniDungeon 内容的状态。

MapSegment 的构造函数接受四个布尔参数,每个参数描述一个可能的出口。我们只想抽取能够满足地图需求的 Segment(即具有所需出口)。

PreResult 重写中,每个 MapSegment 根据所需的出口来启用/禁用自身,以便只有那些能够满足所需出口的 Segment 保持活动状态。

Map 的算法显然不是您见过的最高级的,但这并不是演示的重点。一个 5x5 地图的演示输出可能看起来像这样,以简单的半图形控制台输出显示。

███████ ████ ████████████
███████ ████ ████████████
██      ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
        ████           ██
██ ████ █████████ ███████
██ ████ █████████ ███████
██ ████ █████████ ███████
██ ████ █████████ ███████
                  ██
███████ ████ ████ ████ ██
███████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
██           ██        ██
██ █████████ ████ ████ ██
██ █████████ ████ ████ ██
██ ████ ████ ████ ████ ██
██ ████ ████ ████ ████ ██
                  ██   ██
██ ███████████████████ ██
██ ███████████████████ ██

这对于几行代码来说足够了,可以为您提供进一步实验的基础。让我们仔细看看 MiniDungeon 类以及这一切是如何工作的。

我设置了这个表格,包含了所有可能的 4 个出口组合,除了 0000(无出口)。为了简单起见,这给我们留下了 15 个条目,所有条目的概率都相同。

public class MiniDungeon : RDSTable
{
 public MiniDungeon()
 {
  // Add all possible combinations of exits except the 0000 (no exit)
  // All have the same probability for this demo, in a real scenario
  // you could and probably will make some combinations 
  // more rare than others of course or have more different segments
  // with the same exits... don't forget, this is just a demo!
  AddEntry(new MapSegment(false, false, false, true ), 10);
  AddEntry(new MapSegment(false, false, true , false), 10);
  AddEntry(new MapSegment(false, false, true , true ), 10);
  AddEntry(new MapSegment(false, true , false, false), 10);
  AddEntry(new MapSegment(false, true , false, true ), 10);
  AddEntry(new MapSegment(false, true , true , false), 10);
  AddEntry(new MapSegment(false, true , true , true ), 10);
  AddEntry(new MapSegment(true , false, false, false), 10);
  AddEntry(new MapSegment(true , false, false, true ), 10);
  AddEntry(new MapSegment(true , false, true , false), 10);
  AddEntry(new MapSegment(true , false, true , true ), 10);
  AddEntry(new MapSegment(true , true , false, false), 10);
  AddEntry(new MapSegment(true , true , false, true ), 10);
  AddEntry(new MapSegment(true , true , true , false), 10);
  AddEntry(new MapSegment(true , true , true , true ), 10);
  rdsCount = 1;
 }
...
...
...

MapSegment 在设计上也非常简单。

public class MapSegment : RDSObject
{
 public MapSegment(bool exitnorth, bool exiteast, bool exitsouth, bool exitwest)
 {
  North = exitnorth;
  East = exiteast;
  South = exitsouth;
  West = exitwest;
 }
 public bool North = false;
 public bool East = false;
 public bool South = false;
 public bool West = false;
 public override void OnRDSPreResultEvaluation(EventArgs e)
 {
  base.OnRDSPreResultEvaluation(e);
  // Look up what our table needs
  // Every RDSObject has a pointer to the table where it is contained
  MiniDungeon t = rdsTable as MiniDungeon;
  rdsEnabled = ((t.NeedEast && East) || !t.NeedEast) &&
   ((t.NeedWest && West) || !t.NeedWest) &&
   ((t.NeedNorth && North) || !t.NeedNorth) &&
   ((t.NeedSouth && South) || !t.NeedSouth);
 }
 ...
 ...
 ...

演示算法侧重于邻近字段的出口来确定允许为下一个字段掉落的元素。仔细查看 override OnRDSPreResultEvaluation 方法。

  • 这里第一个新东西:每个 RDSObject 都有一个指向它所属表格的指针,即 rdsTable 字段。它由 RDSTable 对象的 AddEntry 方法设置。您可以使用此字段在运行时获取表格数据,在本例中,获取下一个字段所需的出口。
  • MapSegment 根据所需的出口和自身支持的出口来设置自己的 rdsEnabled 属性。如果结果为 false,则此 Segment 无法掉落。就是这么简单。

MiniDungeon 类现在有一个方法 GenerateMap(..,..),该方法根据生成过程中当前的位置来调整所需出口的布尔标志。在上行,只需要一个 South 出口;在最左边或最右边的列,也需要一个 EastWest 出口;对于地图中间的所有字段,NeedNorthNeedWest 根据相邻字段的出口进行设置,因此我们得到与邻居匹配的 Segment

// Generates a random map with a given dimension
public MapSegment[,] GenerateMap(int sizeX, int sizeY)
{
 MapSegment[,] map = new MapSegment[sizeX, sizeY];
 for (int y = 0; y < sizeY; y++)
 {
  for (int x = 0; x < sizeX; x++)
  {
   if (y == 0)
   {
    NeedNorth = false;
    NeedSouth = true;
   }
   else if (y == sizeY - 1)
   {
    NeedNorth = true;
    NeedSouth = false;
   }
   else
   {
    NeedNorth = (map[x, y - 1].South);
    NeedSouth = !NeedNorth;
   }
   if (x == 0)
   {
    NeedEast = true;
    NeedWest = false;
   }
   else if (x == sizeX - 1)
   {
    NeedEast = false;
    NeedWest = true;
   }
   else
   {
    NeedWest = (map[x - 1, y].East);
    NeedEast = !NeedWest;
   }
   map[x, y] = (MapSegment)rdsResult.First();
  }
 }
 return map;
}

再次强调:这是一个非常简单且远非完美的算法,我老实说不认为它目前的状态可以用于任何实际游戏。但我认为,它足以作为基础工作,让您步入正轨,并让您看到 RDS 的可能性。

每次,对于每个随机内容,都是同样的方案。无论您是拥有像暗黑破坏神那样的掉落系统(其中一个 ZOD 符文可能每百万次掉落中才出现一次),还是想生成地图,在随机位置和随机数量下生成 Monster,对于您想动态创建的任何东西。

我希望您现在对 RDS 能为您做什么有一个很好的了解。我认为这是一个非常有价值的库,如果您同意真正实现(继承)RDS 类,它将为您节省大量决策工作。它们都能很好地协同工作,您几乎拥有所有可以想象的自由,并且有很多 virtual 方法可以重写。

希望您喜欢这个库,

你的,

Mike。

历史

  • 2012-07-13: 开始初稿
© . All rights reserved.