我如何实现Minecraft的制作网格之类的东西?


55

Minecraft中制作系统使用2x2或3x3网格。您将食材放在网格上,如果将正确的食材放入正确的图案中,它将激活配方。

有关设计的一些有趣之处:

  • 有些食谱可以将某些成分替换为其他成分。例如,镐头用棍棒作柄,头用木板,鹅卵石,铁锭,金锭或钻石宝石。
  • 模式中的相对位置很重要,而不是网格上的绝对位置。也就是说,您可以通过将杆和煤(或木炭)以正确的模式放置在3x3网格上的六个位置中的任何一个位置来制作手电筒
  • 图案可以水平翻转。

我可能想得太多,但这似乎是一个有趣的搜索/减少集的问题。那么,从算法上来说,这是如何(或可能)起作用的呢?


6
很棒的问题!我很感兴趣!我确实有一些想法,但是一到家,我就会分享。
jcora 2011年

我实际上是在尝试编写“尽可能接近”的Minecraft副本。我已经写了所有库存和配方经理的东西,我不得不说它工作得非常整洁。我对干净的代码感到满意。但是,编写它并不容易。我写了大约10个班级,以使它们在配方,网格,库存等方面都能很好地工作。找到时间后,我会写一个答案。
Martijn Courteaux 2012年

您应该看看我的世界如何编码他们的制作配方。
Bradman175 '16

Answers:


14

另一种解决方案是使用稍微复杂的树。树中的分支节点将通过遍历配方来创建(再次使用for (y) { for (x) });这是您的库存标准按书结构。您的最终节点将包含一个附加结构(Dictionary/ HashMap),用于将尺寸映射到配方。

本质上,您要这样做的是:

食谱树

黑色的节点是表示项目类型的分支,红色的节点是您的叶子(终止符),可让您区分大小/方向。

要搜索该树,您首先要找到边界框(如我的第一个答案所述),然后按照遍历树的相同顺序遍历节点。最后,您只需在您Dictionary或中查找尺寸,HashMap就可以得到配方的结果。

我只是为了踢球而实施了它 -这可能会澄清我的答案。另外:我意识到这是一个不同的答案-理所当然的是:这是一个不同的解决方案。


非常简单。我喜欢。
大卫·艾克

我会接受这一点,因为我喜欢切入问题核心的树木,哈希表和图表。一看这张图,我几乎立刻就了解了您的算法。
大卫·埃克

23

您必须记住,《我的世界》只使用了很少的可能配方,因此不需要任何聪明的东西。

就是说,我要做的就是找到适合的最小网格(即忽略空的行和列以找出它是2x2还是3x3还是2x3(门))。然后循环浏览具有该大小的配方列表,只需检查项目类型是否相同(即,在Minecraft中进行9次整数比较,因为它对项目和块使用整数类型ID),并在找到匹配项时停止。

这种方式也使物品的相对位置无关紧要(您可以将手电筒放置在手工网格上的任何位置,因为它将其视为1x2的盒子,而不是3x3的盒子(大部分是空的),所以可以使用)。

如果您有大量食谱,那么通过可能的匹配进行线性搜索将花费很长时间,则可以对列表进行排序并执行二进制搜索(O(log(N))vs O(N))。这将在构建列表时造成一些额外的工作,但是可以在启动时完成一次,然后保存在内存中。

最后一件事,就是允许最简单地水平翻转配方,就是将镜像版本添加到列表中。

如果您想在不添加第二个配方的情况下执行此操作,则可以检查输入配方中[0,0]中的项目ID是否比[0,2]中的项目高(或对于2x2,则为[0,1]),则无需检查对于1x2,如果是这样镜像的话,如果没有,请继续检查下一行,直到到达末尾为止。使用此方法还必须确保以正确的旋转方式添加了配方。


1
我想知道Minecraft中的少量食谱是否可能不适合进行线性搜索。
David Eyk 2011年

1
可以,并且基本上就是我上面写的(可能对二进制搜索进行了优化)。但是,这为制作手电筒留下了一些选择,您基本上可以在任何地方安装它。通过首先获取食谱的大小,您无需为火把添加6个食谱,并且只需一步即可将搜索范围限制为仅正确大小的食谱。-因此,基本上,第2点的选项是按大小分组,将配方移至角落或添加更多重复的配方。
Elva

15

如果将3x3网格编码为字符串并使用正则表达式匹配,查看特定网格配置是否匹配特定配方非常简单。加快查找速度是另一回事,我将在最后讨论。继续读以获取更多信息。

步骤1)将表格编码为字符串

只需为每种类型的单元格提供一个字符ID,然后按此顺序并排连接所有内容:

123
456 => 123456789
789

再举一个更具体的例子,考虑棒配方,其中W代表木头,E是一个空单元格(您可以简单地使用一个空char''):

EEE
WEE => EEEWEEWEE
WEE

第2步)使用正则表达式(或字符串)匹配配方。包含一些对数据的处理

继续上面的示例,即使我们移动编队,字符串中仍然有一个模式(WEEW两侧都用E填充):

EEW
EEW => EEWEEWEEE
EEE

因此,无论您将摇杆移动到何处,它仍将与以下正则表达式匹配: /^E*WEEWE*$/

正则表达式还使您可以执行所提到的条件行为。例如(配方),如果您想要用铁石头制成的镐来得到相同的结果,即:

III    SSS
EWE or EWE
EWE    EWE

您可以将两者合并为正则表达式: /^(III)|(SSS)EWEEWE$/

水平翻转也可以轻松添加(也可以使用|运算符)。

编辑: 无论如何,正则表达式部分不是严格必需的。这只是将问题封装在单个表达式中的一种方法,但是对于变量位置问题,您也可以修剪任何填充空间的网格字符串(在此示例中为E)并执行String.Contains()。对于多成分问题或镜像配方,您可以将它们全部作为具有相同输出的多个(即单独的)配方进行处理。

步骤3)加快查找

至于减少搜索,您将需要创建一些数据结构以将配方分组在一起并帮助查找。将网格视为字符串在这里也有一些优点

  1. 您可以将配方的“长度”定义为第一个非空字符与最后一个非空字符之间的距离。一个简单的Trim().Length()将为您提供此信息。食谱可以按长度分组并存储在字典中。

    要么

    “长度”的另一种定义可以是非空字符的数量。没有其他改变。您也可以按照此标准对配方进行分组。

  2. 如果点号1还不够,则还可以根据配方中出现的第一种成分的类型对配方进行进一步分组。这和操作一样简单Trim().CharAt(0)(并防止Trim导致空字符串)。

因此,例如,您将配方存储在:

Dictionary<int, Dictionary<char, List<string>>> _recipes;

并执行类似以下的查找:

// A string encode of your current grid configuration 
string grid;

// Get length and first char in our grid
string trim = grid.Trim();
int length = trim.Length();
char firstChar = length==0 ? ' ' : trim[0];

foreach(string recipe in _recipes[length][firstChar])
{
    // Check for a match with the recipe
    if(Regex.Match(grid, recipe))
    {
        // We found a matching recipe, do something with it
    }
}

3
这是...非常非常聪明。
拉夫琳2011年

18
您有问题,想使用正则表达式解决吗?现在您有2个问题:)
Kromster表示支持Monica 2011年

2
@Krom我想你可能只是在开玩笑,但是这个评论背后有什么道理吗?使用正则表达式只是一种简洁的方法(使用单行代码将网格与配方匹配)和灵活的方式(适合该问题的所有要求),即可对一组数据(网格内容)进行匹配。与滚动自己的匹配算法相比,我没有看到任何明显的缺点,它简化了整个过程。
David Gouveia

2
@DavidGouveia,对于那些对Regex不太满意的人来说,这只是一个短语。如果他们决定使用正则表达式解决问题,那么他们现在有两个问题:(1)他们要解决的实际问题(2)编写正则表达式模式来解决该问题
iamserious,2011年

2
回到“现在您有两个问题”-除非您的正则表达式处理器支持unicode,并且您可以保证某些组合不会使正则表达式NFA编译器跳闸,否则正则表达式将在您的项目超出ASCII可打印范围时出现问题。起来 您可以将自己的DFA(实际上很容易)组合在一起以匹配模式;但是正则表达式将是制作原型时最好的方法。
乔纳森·迪金森

3

我无法告诉您Minecraft的工作原理-尽管我确定如果您查看了MCP(如果您拥有Minecraft的合法副本),则可以找到答案。

我将如下实现:

  1. 有一些基于磁盘的字典/哈希图(B + Tree / SQL-Light)-该值将是配方结果的项目ID。
  2. 当用户制作东西时,只需找到带有独立项的第一行/列即可;将它们合并成一个偏移量。
  3. 再次独立地找到带有项目的最后一行/列。
  4. 计算项目的宽度和高度,并将其添加到关键点。
  5. 在刚刚计算出的边界框范围内循环,并附加每一项的ID(使用您通常的for (y) { for (x) })。
  6. 在数据库中查找密钥。

例如,假设我们有两种成分;X和Y,空格为*。采取以下食谱:

**X
**Y
**Y

首先,我们计算边界框,产生(2,0)-(2,2)。因此,我们的密钥将如下所示[1][3](1宽3高度)。接下来,我们在边界框内遍历每个项目并附加ID,因此密钥变为[1][3][X][Y][Y]-然后在字典/数据库中查找该密钥,您将获得该配方的结果。

为了更清楚地说明步骤2中的独立性,请考虑以下方法:

*XX
XXX
XX*

顶部/左侧显然是0,0-但是您通常会遇到的第一项可能是0,1或1,0(取决于您的循环)。但是,如果找到第一个非空列以及第一个非空行并将这些坐标相结合,您将得到0,0-相同的原理适用于边界框的底部/右侧。


Bukkit仓库没有任何Minecraft代码,您需要MCP(和Minecraft的副本)
BlueRaja-Danny Pflughoeft 2011年

这不只是您其他答案的反面吗?
大卫·艾克

@DavidEyk(这是我的第一个答案)并非如此,它更多地依赖于哈希表结构(无论是类似于bigtable的还是内存中的)。我再次回答的原因是因为我担心哈希冲突会导致性能下降-至少在内存哈希图中。我的其他答案对于更多项目和内存结构会更好-如果在SQL / etc中,它将更好。
乔纳森·迪金森

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.