地图平铺算法

Posted

技术标签:

【中文标题】地图平铺算法【英文标题】:Map Tiling Algorithm 【发布时间】:2012-02-12 16:35:42 【问题描述】:

地图

我正在使用 javascript 制作基于瓦片的 RPG,使用 perlin 噪声高度图,然后根据噪声的高度分配瓦片类型。

地图最终看起来像这样(在小地图视图中)。

我有一个相当简单的算法,它从图像上的每个像素中提取颜色值,并将其转换为整数 (0-5),具体取决于它在 (0-255) 之间的位置,对应于平铺字典中的平铺。然后将此 200x200 数组传递给客户端。

然后引擎根据数组中的值确定图块并将它们绘制到画布上。因此,我最终得到了具有逼真特征的有趣世界:山脉、海洋等。

现在我想做的下一件事是应用某种混合算法,使瓷砖无缝融入其邻居,如果邻居不是同一类型。上面的示例地图是玩家在他们的小地图中看到的。在屏幕上,他们会看到由白色矩形标记的部分的渲染版本;其中瓷砖与其图像一起渲染,而不是作为单色像素。

这是用户将在地图中看到的示例,但 它与上面的视口显示的位置不同!

正是在这个视图中,我希望发生转换。

算法

我想出了一个简单的算法,它可以在视口内遍历地图,并在每个图块的顶部渲染另一个图像,前提是它位于不同类型的图块旁边。 (不改变地图!只是渲染一些额外的图像。)该算法的想法是分析当前图块的邻居:

这是引擎可能必须渲染的示例场景,当前图块是标有 X 的图块。

创建了一个 3x3 数组,并读入它周围的值。因此对于本例,该数组如下所示。

[
    [1,2,2]
    [1,2,2]
    [1,1,2]
];

然后我的想法是为可能的图块配置制定一系列案例。在一个非常简单的层面上:

if(profile[0][1] != profile[1][1])
     //draw a tile which is half sand and half transparent
     //Over the current tile -> profile[1][1]
     ...

这给出了这个结果:

这是从[0][1][1][1] 的过渡,但不是从[1][1][2][1] 的过渡,仍然存在硬边。所以我认为在那种情况下必须使用角落瓷砖。我创建了两个 3x3 的精灵表,我认为它们可以包含所有可能需要的图块组合。然后我为游戏中的所有图块复制了这个(白色区域是透明的)。对于每种类型的图块,这最终是 16 个图块(不使用每个精灵表上的中心图块。)

理想结果

因此,使用这些新图块和正确的算法,示例部分将如下所示:

我所做的每一次尝试都失败了,算法中总是存在一些缺陷,并且模式最终变得奇怪。我似乎无法正确处理所有情况,总体而言,这似乎是一种糟糕的方式。

解决方案?

所以,如果有人可以提供一个替代解决方案来说明我如何创建这种效果,或者编写分析算法的方向,那么我将非常感激!

【问题讨论】:

看看this article 以及链接的文章,尤其是this one。博客本身包含许多可以作为起点的想法。 Here 是一个概述。 你应该简化你的算法。检查这个:Two-Dimensional-Cellular-Automata 【参考方案1】:

我会建议一些事情:

“中心”图块是什么并不重要,对吧?可能是 2,但如果所有其他都是 1,它会显示 1?

当顶部或侧面的直接邻居存在差异时,只有角落是什么才重要。如果所有的直接邻居都是 1,而一个角是 2,它将显示 1。

我可能会预先计算所有可能的邻居组合,创建一个 8 索引数组,其中前四个指示顶部/底部邻居的值,第二个指示对角线:

edges[N][E][S][W][NE][SE][SW][NW] = 精灵的任何偏移量

所以在你的情况下,[2][2][1][1][2][2][1][1] = 4(第 5 个精灵)。

在这种情况下,[1][1][1][1] 将是 1,[2][2][2][2] 将是 2,其余的都必须计算出来。但是查找特定图块将是微不足道的。

【讨论】:

【参考方案2】:

好的,所以首先想到的是自动解决问题的完美解决方案需要一些相当丰富的插值数学。基于您提到预渲染平铺图像的事实,我认为这里不保证完整的插值解决方案。

另一方面,正如你所说,手工完成地图会带来好的结果...... 但我也认为任何修复故障的手动过程也不是一种选择。

这是一个简单的算法,它不会给出完美的结果,但基于它所付出的努力,这是非常有益的。

而不是尝试混合每个边缘图块,(这意味着您需要先知道混合相邻图块的结果 - 插值,或者您需要多次细化整个地图并且不能依赖预生成的瓷砖)为什么不以交替的棋盘图案混合瓷砖?

[1] [*] [2]
[*] [1] [*]
[1] [*] [2]

即只混合上面矩阵中加星标的图块?

假设唯一允许的价值步骤是一次一个,那么您只需设计几个图块...

A    [1]      B    [2]      C    [1]      D    [2]      E    [1]           
 [1] [*] [1]   [1] [*] [1]   [1] [*] [2]   [1] [*] [2]   [1] [*] [1]   etc.
     [1]           [1]           [1]           [1]           [2]           

总共将有 16 种模式。如果您利用旋转和反射对称性,那就更少了。

'A' 将是一个普通的 [1] 样式图块。 'D' 将是对角线。

瓷砖的角落会有小的不连续性,但与您给出的示例相比,这些不连续性很小。

如果可以的话,我稍后会用图片更新这篇文章。

【讨论】:

这听起来不错,我很想通过一些图片来查看它,以便更好地了解您的意思。 我无法将任何图像放在一起,因为我没有自以为拥有的软件...但我一直在思考,它并不是一个很好的解决方案.当然,您可以进行对角线过渡,但这种平滑算法并不能真正帮助其他过渡。您甚至无法保证您的地图不会包含 90 度过渡。抱歉,我想这个有点让人失望。【参考方案3】:

我正在玩类似的东西,由于多种原因没有完成;但基本上它需要一个 0 和 1 的矩阵,0 是地面,1 是墙,用于 Flash 中的迷宫生成器应用程序。由于 AS3 与 JavaScript 类似,用 JS 重写并不难。

var tileDimension:int = 20;
var levelNum:Array = new Array();

levelNum[0] = [1, 1, 1, 1, 1, 1, 1, 1, 1];
levelNum[1] = [1, 0, 0, 0, 0, 0, 0, 0, 1];
levelNum[2] = [1, 0, 1, 1, 1, 0, 1, 0, 1];
levelNum[3] = [1, 0, 1, 0, 1, 0, 1, 0, 1];
levelNum[4] = [1, 0, 1, 0, 0, 0, 1, 0, 1];
levelNum[5] = [1, 0, 0, 0, 0, 0, 0, 0, 1];
levelNum[6] = [1, 0, 1, 1, 1, 1, 0, 0, 1];
levelNum[7] = [1, 0, 0, 0, 0, 0, 0, 0, 1];
levelNum[8] = [1, 1, 1, 1, 1, 1, 1, 1, 1];

for (var rows:int = 0; rows < levelNum.length; rows++)

    for (var cols:int = 0; cols < levelNum[rows].length; cols++)
    
        // set up neighbours
        var toprow:int = rows - 1;
        var bottomrow:int = rows + 1;

        var westN:int = cols - 1;
        var eastN:int = cols + 1;

        var rightMax =  levelNum[rows].length;
        var bottomMax = levelNum.length;

        var northwestTile =     (toprow != -1 && westN != -1) ? levelNum[toprow][westN] : 1;
        var northTile =         (toprow != -1) ? levelNum[toprow][cols] : 1;
        var northeastTile =     (toprow != -1 && eastN < rightMax) ? levelNum[toprow][eastN] : 1;

        var westTile =          (cols != 0) ? levelNum[rows][westN] : 1;
        var thistile =          levelNum[rows][cols];
        var eastTile =          (eastN == rightMax) ? 1 : levelNum[rows][eastN];

        var southwestTile =     (bottomrow != bottomMax && westN != -1) ? levelNum[bottomrow][westN] : 1;
        var southTile =         (bottomrow != bottomMax) ? levelNum[bottomrow][cols] : 1;
        var southeastTile =     (bottomrow != bottomMax && eastN < rightMax) ? levelNum[bottomrow][eastN] : 1;

        if (thistile == 1)
        
            var w7:Wall7 = new Wall7();
            addChild(w7);
            pushTile(w7, cols, rows, 0);

            // wall 2 corners

            if      (northTile === 0 && northeastTile === 0 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            
                var w21:Wall2 = new Wall2();
                addChild(w21);
                pushTile(w21, cols, rows, 270);
            

            else if (northTile === 0 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 0)
            
                var w22:Wall2 = new Wall2();
                addChild(w22);
                pushTile(w22, cols, rows, 0);
            

            else if (northTile === 1 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 1 && northwestTile === 1)
            
                var w23:Wall2 = new Wall2();
                addChild(w23);
                pushTile(w23, cols, rows, 90);
            

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            
                var w24:Wall2 = new Wall2();
                addChild(w24);
                pushTile(w24, cols, rows, 180);
                       

            //  wall 6 corners

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 0 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 1)
            
                var w61:Wall6 = new Wall6();
                addChild(w61);
                pushTile(w61, cols, rows, 0); 
            

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 0 && westTile === 1 && northwestTile === 1)
            
                var w62:Wall6 = new Wall6();
                addChild(w62);
                pushTile(w62, cols, rows, 90); 
            

            else if (northTile === 1 && northeastTile === 1 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 0)
            
                var w63:Wall6 = new Wall6();
                addChild(w63);
                pushTile(w63, cols, rows, 180);
            

            else if (northTile === 1 && northeastTile === 0 && eastTile === 1 && southeastTile === 1 && southTile === 1 && southwestTile === 1 && westTile === 1 && northwestTile === 1)
            
                var w64:Wall6 = new Wall6();
                addChild(w64);
                pushTile(w64, cols, rows, 270);
            

            //  single wall tile

            else if (northTile === 0 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            
                var w5:Wall5 = new Wall5();
                addChild(w5);
                pushTile(w5, cols, rows, 0);
            

            //  wall 3 walls

            else if (northTile === 0 && eastTile === 1 && southTile === 0 && westTile === 1)
            
                var w3:Wall3 = new Wall3();
                addChild(w3);
                pushTile(w3, cols, rows, 0);
            

            else if (northTile === 1 && eastTile === 0 && southTile === 1 && westTile === 0)
            
                var w31:Wall3 = new Wall3();
                addChild(w31);
                pushTile(w31, cols, rows, 90);
            

            //  wall 4 walls

            else if (northTile === 0 && eastTile === 0 && southTile === 1 && westTile === 0)
            
                var w41:Wall4 = new Wall4();
                addChild(w41);
                pushTile(w41, cols, rows, 0);
            

            else if (northTile === 1 && eastTile === 0 && southTile === 0 && westTile === 0)
            
                var w42:Wall4 = new Wall4();
                addChild(w42);
                pushTile(w42, cols, rows, 180);
            

            else if (northTile === 0 && northeastTile === 0 && eastTile === 1 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 0 && northwestTile === 0)
            
                var w43:Wall4 = new Wall4();
                addChild(w43);
                pushTile(w43, cols, rows, 270);
            

            else if (northTile === 0 && northeastTile === 0 && eastTile === 0 && southeastTile === 0 && southTile === 0 && southwestTile === 0 && westTile === 1 && northwestTile === 0)
            
                var w44:Wall4 = new Wall4();
                addChild(w44);
                pushTile(w44, cols, rows, 90);
            

            //  regular wall blocks

            else if (northTile === 1 && eastTile === 0 && southTile === 1 && westTile === 1)
            
                var w11:Wall1 = new Wall1();
                addChild(w11);
                pushTile(w11, cols, rows, 90);
            

            else if (northTile === 1 && eastTile === 1 && southTile === 1 && westTile === 0)
            
                var w12:Wall1 = new Wall1();
                addChild(w12);
                pushTile(w12, cols, rows, 270);
            

            else if (northTile === 0 && eastTile === 1 && southTile === 1 && westTile === 1)
            
                var w13:Wall1 = new Wall1();
                addChild(w13);
                pushTile(w13, cols, rows, 0);
            

            else if (northTile === 1 && eastTile === 1 && southTile === 0 && westTile === 1)
            
                var w14:Wall1 = new Wall1();
                addChild(w14);
                pushTile(w14, cols, rows, 180);
            

        
        // debug === // trace('Top Left: ' + northwestTile + ' Top Middle: ' + northTile + ' Top Right: ' + northeastTile + ' Middle Left: ' + westTile + ' This: ' + levelNum[rows][cols] + ' Middle Right: ' + eastTile + ' Bottom Left: ' + southwestTile + ' Bottom Middle: ' + southTile + ' Bottom Right: ' + southeastTile);
    


function pushTile(til:Object, tx:uint, ty:uint, degrees:uint):void

    til.x = tx * tileDimension;
    til.y = ty * tileDimension;
    if (degrees != 0) tileRotate(til, degrees);


function tileRotate(tile:Object, degrees:uint):void

    // http://www.flash-db.com/Board/index.php?topic=18625.0
    var midPoint:int = tileDimension/2;
    var point:Point=new Point(tile.x+midPoint, tile.y+midPoint);
    var m:Matrix=tile.transform.matrix;
    m.tx -= point.x;
    m.ty -= point.y;
    m.rotate (degrees*(Math.PI/180));
    m.tx += point.x;
    m.ty += point.y;
    tile.transform.matrix=m;

基本上,这会检查它周围的每个图块,从左到右,从上到下,并假设边缘图块始终为 1。我还冒昧地将图像导出为文件以用作键:

这是不完整的,可能是实现此目的的一种 hacky 方式,但我认为它可能会有一些好处。

编辑:该代码结果的屏幕截图。

【讨论】:

【参考方案4】:

下面的方块代表一块金属板。右上角有一个“散热孔”。我们可以看到,随着该点的温度保持恒定,金属板在每个点处收敛到恒定温度,靠近顶部自然会变热:

求每个点的温度问题可以用“边值问题”来解决。然而,计算每个点的热量的最简单方法是将板建模为网格。我们知道恒温下网格上的点。我们将所有未知点的温度设置为室温(就好像通风口刚刚打开一样)。然后我们让热量通过板扩散,直到我们达到收敛。这是通过迭代完成的:我们迭代每个 (i,j) 点。我们设置 point(i,j) = (point(i+1, j)+point(i-1,j)+point(i, j+1)+point(i,j-1))/4 [除非点(i,j)有一个恒温散热口]

如果您将此应用于您的问题,它非常相似,只是平均颜色而不是温度。您可能需要大约 5 次迭代。我建议使用 400x400 网格。那就是 400x400x5 = 少于 100 万次迭代,这将很快。如果您只使用 5 次迭代,您可能不需要担心保持任何点的颜色不变,因为它们不会从原来的位置偏移太多(实际上只有距离颜色 5 以内的点会受到颜色的影响)。 伪代码:

iterations = 5
for iteration in range(iterations):
    for i in range(400):
        for j in range(400):
            try:
                grid[i][j] = average(grid[i+1][j], grid[i-1][j],
                                     grid[i][j+1], grid[i][j+1])
            except IndexError:
                pass

【讨论】:

你能再扩展一下吗?我很好奇,我无法理解你的解释。完成迭代后如何使用平均颜色值? 每个网格点 grid[i][j] 可以作为适当颜色的小矩形(或单个像素)绘制到画布上。【参考方案5】:

该算法的基本思想是使用预处理步骤找到所有边缘,然后根据边缘的形状选择正确的平滑瓦片。

第一步是找到所有边。在下面的示例中,标有 X 的 边缘图块 都是绿色图块,其中一个棕褐色图块作为其八个相邻图块中的一个或多个。对于不同类型的地形,如果该图块具有较低地形编号的邻居,则此条件可能会转化为边缘图块。

一旦检测到所有边缘图块,接下来要做的就是为每个边缘图块选择正确的平滑图块。这是我对您的平滑图块的表示。

请注意,实际上没有那么多不同类型的图块。我们需要 3x3 方格中的一个方格中的 8 个外部方格,但只需要另一个方格中的四个角方格,因为在第一个方格中已经找到了直边方格。这意味着我们必须区分总共 12 种不同的情况。

现在,查看一个边缘图块,我们可以通过查看其最近的四个相邻图块来确定边界转向的方向。与上面一样用 X 标记边缘图块,我们有以下六种不同的情况。

这些情况用于确定对应的平滑图块,我们可以相应地对平滑图块进行编号。

对于每种情况,仍然可以选择 a 或 b。这取决于草在哪一侧。确定这一点的一种方法可能是跟踪边界的方向,但可能最简单的方法是在边缘旁边选择一个图块并查看它的颜色。下图显示了 5a) 和 5b) 两种情况,可以通过例如检查右上图块的颜色来区分它们。

原始示例的最终枚举将如下所示。

在选择相应的边缘图块后,边框看起来像这样。

作为最后一点,我可能会说,只要边界有点规则,这将起作用。更准确地说,没有恰好两个边缘瓦片作为它们的邻居的边缘瓦片将不得不分开处理。这将发生在地图边缘的边缘瓦片上,它只有一个边缘邻居,而对于非常狭窄的地形,相邻边缘瓦片的数量可能是三个甚至四个。

【讨论】:

这很好,对我很有帮助。我正在处理一些瓷砖无法直接过渡到其他瓷砖的情况。例如,“泥土”瓷砖可以过渡到“浅草”,“浅草”可以过渡到“中草”。 Tiled (mapeditor.org) 通过为地形画笔实现某种类型的树搜索来很好地处理这个问题;不过,我还不能复制它。

以上是关于地图平铺算法的主要内容,如果未能解决你的问题,请参考以下文章

在给定 4 个角位置的情况下,无法编写算法来填充平铺地图

如何显示谷歌地球旧世界地图(或空白/物理世界地图)的谷歌地图平铺覆盖?

使用画布的等距平铺地图(带有点击检测)

用于存储巨大平铺地图的选项

通过 brutile/sharpmap 向 geoserver 发送 wms 请求以加载平铺地图 (tiled=true)

GL_DEPTH_COMPONENT 24 OZ 的等距平铺地图闪烁