web-dev-qa-db-ja.com

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

地図

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

マップは、このようなものになります(ミニマップビュー)。

enter image description here

画像の各ピクセルから色値を抽出し、タイル辞書のタイルに対応する(0-255)の間の位置に応じて整数(0-5)に変換するかなり単純なアルゴリズムがあります。次に、この200x200配列がクライアントに渡されます。

次に、エンジンは配列内の値からタイルを決定し、それらをキャンバスに描画します。ですから、山や海などの現実的な外観を持つ興味深い世界になります。

次に、私がしたい次のことは、タイルを隣接するタイルにシームレスにブレンドする何らかのブレンドアルゴリズムを適用することでしたif隣人は同じタイプではありません。上記のマップ例は、プレーヤーがミニマップで見るものです。画面上には、白い長方形でマークされたセクションのレンダリングバージョンが表示されます。タイルは、単色ピクセルとしてではなく、画像でレンダリングされます。

これは、ユーザーがマップに表示するものの例ですが、上のビューポートが示す場所とは異なります!

enter image description here

このビューで、移行が発生するようにします。

アルゴリズム

ビューポート内のマップをトラバースし、各タイルの上部に別の画像をレンダリングする単純なアルゴリズムを思い付きました。ただし、異なるタイプのタイルの隣にある場合に限ります。 (マップを変更しません!いくつかの余分な画像をレンダリングするだけです。)アルゴリズムのアイデアは、現在のタイルの隣人をプロファイルすることでした:

An example of a tile profile

これは、エンジンがレンダリングする必要があるもののシナリオ例であり、現在のタイルは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]
     ...
}

この結果は次のとおりです。

Result

これは、[0][1]から[1][1]への移行として機能しますが、ハードエッジが残っている[1][1]から[2][1]への移行としては機能しません。そのため、その場合はコーナータイルを使用する必要があると考えました。必要な可能性のあるすべてのタイルの組み合わせを保持できると考えた2つの3x3スプライトシートを作成しました。次に、ゲーム内にあるすべてのタイルに対してこれを複製しました(白い領域は透明です)。これは、タイルのタイプごとに16タイルになります(各スプライトシートの中央のタイルは使用されません)。

SandSand2

理想的な結果

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

Correct

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

解決策?

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

153
Dan Prince

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

最初のステップは、すべてのエッジを見つけることです。以下の例では、Xでマークされたエッジタイルはすべて、8つの隣接するタイルの1つ以上として黄褐色のタイルを持つ緑のタイルです。さまざまなタイプの地形では、この条件は、より低いterrain-numberの隣接がある場合、タイルがエッジタイルに変換される可能性があります。

Edge tiles.

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

Smoothing tiles.

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

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

Six cases.

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

Smoothed tiles with numbers.

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

Choosing 5a or 5b.

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

Final enumeration.

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

Final result.

最後に、境界がある程度規則的である限り、これは機能すると言うかもしれません。より正確には、隣接するエッジタイルが2つだけではないエッジタイルは、個別に処理する必要があります。これは、単一のエッジネイバーを持つマップのエッジ上のエッジタイルと、隣接するエッジタイルの数が3つまたは4つである非常に狭い地形の場合に発生します。

117
user1884905

次の正方形は金属板を表します。右上隅に「ヒートベント」があります。この点の温度が一定に保たれると、金属板が各点で一定の温度に収束し、上部付近で自然に熱くなることがわかります。

heatplate

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

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

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

完全な結果が得られない単純なアルゴリズムを次に示しますが、手間がかからないため非常にやりがいがあります。

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

[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」は対角線になります。

タイルの角に小さな不連続性がありますが、これらはあなたが与えた例と比べると軽微です。

できれば、後でこの投稿を画像で更新します。

5
perfectionist

私はこれに似たもので遊んでいたが、いくつかの理由で終了しなかった。しかし、基本的には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;
}

基本的に、これは周囲のすべてのタイルを左から右、上から下にチェックし、Edgeタイルは常に1であると仮定します。また、キーとして使用するファイルとして画像をエクスポートする自由を取りました。

Wall tiles

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

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

Generated Result

2
Ben

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

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

  • 上部または側面のすぐ隣に違いがある場合にのみ、コーナーが重要になります。すぐ隣のすべてが1で、コーナーが2の場合、1が表示されます。

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

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

1
elijah