マップタイリングアルゴリズム


153

地図

私は、perlinノイズハイトマップを使用してJavascriptでタイルベースのRPGを作成し、ノイズの高さに基づいてタイルタイプを割り当てています。

マップはこのように見えます(ミニマップビューで)。

ここに画像の説明を入力してください

画像の各ピクセルからカラー値を抽出し、タイル辞書のタイルに対応する(0〜255)の間の位置に応じて整数(0〜5)に変換するかなり単純なアルゴリズムがあります。次に、この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]、どこのハードエッジが残ります。したがって、その場合はコーナータイルを使用する必要があると考えました。私は2つの3x3スプライトシートを作成し、必要なタイルのすべての可能な組み合わせを保持すると考えました。次に、ゲーム内にあるすべてのタイルについてこれを複製しました(白い領域は透明です)。これは、タイルのタイプごとに16タイルになります(各スプライトシートの中央のタイルは使用されません)。

砂砂2

理想的な結果

したがって、これらの新しいタイルと正しいアルゴリズムを使用すると、サンプルセクションは次のようになります。

正しい

私が行ったすべての試みは失敗しましたが、アルゴリズムには常にいくつかの欠陥があり、パターンは奇妙になってしまいます。私はすべてのケースを正しくすることができないようで、全体的にそれを行うには悪い方法のようです。

解決策?

したがって、この効果をどのように作成できるか、またはプロファイリングアルゴリズムを作成するための方向性に関する代替ソリューションを誰かが提供できるとしたら、私は非常に感謝します!


7
見ていこの記事とリンクの記事だけでなく、特にこれを。ブログ自体には、開始点として役立つことができる多くのアイデアが含まれています。こちらが概要です。
Darcara

アルゴリズムを簡略化する必要があります。これを確認してください:Two-Dimensional-Cellular-Automata
user1097489

回答:


117

このアルゴリズムの基本的な考え方は、前処理ステップを使用してすべてのエッジを見つけ、エッジの形状に応じて適切なスムージングタイルを選択することです。

最初のステップは、すべてのエッジを見つけることです。以下の例では、Xでマークされたエッジタイルはすべて、8つの隣接するタイルの1つ以上に黄褐色のタイルが付いた緑のタイルです。地形の種類が異なる場合、この条件は、隣接する地形番号が小さい場合、タイルをエッジタイルに変換できます。

エッジタイル。

すべてのエッジタイルが検出されたら、次に行うことは、各エッジタイルに適切なスムージングタイルを選択することです。これがあなたのスムージングタイルの私の表現です。

タイルを滑らかにする。

実際にはそれほど多くの種類のタイルはないことに注意してください。3x3の正方形の1つから8つの外側のタイルが必要ですが、ストレートエッジタイルは最初の正方形に既にあるため、他の正方形の4つのコーナーの正方形のみが必要です。これは、合計で12のケースを区別する必要があることを意味します。

これで、1つのエッジタイルを見て、最も近い4つの隣接タイルを見て、境界がどの方向に曲がるかを判断できます。上記のようにXでエッジタイルをマークすると、次の6つの異なるケースがあります。

6つのケース。

これらのケースは、対応するスムージングタイルを決定するために使用され、それに応じてスムージングタイルに番号を付けることができます。

数字で滑らかなタイル。

いずれの場合も、aまたはbの選択肢があります。これは、芝生がどちら側にあるかによって異なります。これを判断する1つの方法は、境界の方向を追跡することですが、おそらく最も簡単な方法は、エッジの隣のタイルを1つ選び、その色を確認することです。以下の画像は、2つのケース5a)と5b)を示しています。たとえば、右上のタイルの色を確認することで区別できます。

5aまたは5bを選択します。

元の例の最終的な列挙は次のようになります。

最後の列挙。

対応するエッジタイルを選択すると、境界線は次のようになります。

最終結果。

最後の注意として、境界がある程度規則的である限り、これは機能すると言います。より正確には、2つのエッジタイルが隣接していないエッジタイルは、隣接するタイルとは別に処理する必要があります。これは、隣接する単一のエッジを持つマップのエッジ上のエッジタイルと、隣接するエッジタイルの数が3または4である可能性がある非常に狭い地形の一部で発生します。


1
これはすばらしいことであり、私にとって非常に役立ちます。一部のタイルを他のタイルに直接移行できない場合を扱っています。たとえば、「ダート」タイルは「ライトグラス」に移行でき、「ライトグラス」は「ミディアムグラス」に移行できます。Tiled(mapeditor.org)は、テレインブラシに何らかのタイプのツリー検索を実装することで、これをうまく処理します。まだ再現できていません。
クレイ

12

次の正方形は金属板を表しています。右上には「ヒートベント」があります。このポイントの温度が一定に保たれると、金属プレートは各ポイントで一定の温度に収束し、上部近くで自然に高温になる様子を確認できます。

ヒートプレート

各点の温度を求める問題は、「境界値問題」として解決できます。ただし、各点で熱を計算する最も簡単な方法は、プレートをグリッドとしてモデル化することです。一定温度でのグリッド上のポイントはわかっています。すべての未知の点の温度を室温に設定しました(あたかもベントがオンになったばかりのように)。次に、収束に達するまで、プレート全体に熱を拡散させます。これは反復によって行われます。各(i、j)点を反復処理します。point(i、j)=(point(i + 1、j)+ point(i-1、j)+ point(i、j + 1)+ point(i、j-1))/ 4を設定します(ただし、 point(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

これをもう少し拡張してもらえますか?不思議なことに、あなたの説明が理解できません。繰り返しを行った後、平均色値をどのように使用しますか?
ちー

1
各グリッドポイントgrid [i] [j]は、適切な色の小さな長方形(または個々のピクセル)としてキャンバスに描画できます。
ロバートキング

5

わかりましたので、最初の考えは、問題の完全な解決策を自動化するには、かなり肉厚な補間計算が必要であるということです。事前にレンダリングされたタイル画像について言及したという事実に基づいて、ここでは完全な補間ソリューションは保証されていないと思います。

一方、あなたが言ったように、手動で地図を仕上げることは良い結果につながります...しかし、私はまた、グリッチを修正するための手動プロセスもオプションではないと思います。

これは完璧な結果をもたらさない単純なアルゴリズムですが、それはそれが取る少ない労力に基づいて非常にやりがいがあります。

すべてのエッジタイルをブレンドしようとする代わりに(つまり、最初に隣接するタイルをブレンドした結果を知る必要がある-補間するか、マップ全体を何度か調整する必要があり、事前に生成されたタイルに依存できない)タイルを交互の市松模様で混ぜてみませんか?

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

つまり、上記のマトリックスでスターを付けたタイルのみをブレンドしますか?

価値のある唯一の許可されたステップが一度に1つであると仮定すると、設計するタイルは数個しかありません...

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であると想定します。また、キーとして使用するファイルとして画像をエクスポートする自由を取っています。

壁タイル

これは不完全であり、おそらくこれを達成するためのハックな方法ですが、私はそれがいくつかの利益になるかもしれないと思いました。

編集:そのコードの結果のスクリーンショット。

生成された結果


1

私はいくつかのことを提案します:

  • 「中央」タイルが何であるかは関係ありませんよね?2の場合もありますが、他のすべてが1の場合、1と表示されますか?

  • トップまたはサイドのすぐ隣に違いがある場合、それはコーナーが何であるかだけが重要です。隣接するすべての要素が1で、コーナーが2の場合、1と表示されます。

  • 私はおそらく、近隣の可能なすべての組み合わせを事前計算し、最初の4つが上部/下部の近隣の値を示し、2番目が対角を示す8インデックス配列を作成します。

edge [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となり、残りは解決する必要があります。しかし、特定のタイルのルックアップは簡単です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.