타일링 알고리즘지도


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]하드 에지 남아 곳. 그래서 그 경우 모퉁이 타일을 사용해야한다고 생각했습니다. 필자는 필요한 타일 조합을 모두 보유 할 것으로 생각되는 3x3 스프라이트 시트 두 개를 만들었습니다. 그런 다음 게임에있는 모든 타일에 대해 이것을 복제했습니다 (흰색 영역은 투명합니다). 각 타일 유형에 대해 16 개의 타일이됩니다 (각 스프라이트 시트의 중앙 타일은 사용되지 않습니다).

모래모래 2

이상적인 결과

따라서 새로운 타일과 올바른 알고리즘으로 예제 섹션은 다음과 같습니다.

옳은

내가 시도한 모든 시도는 실패했지만 알고리즘에는 항상 결함이 있으며 패턴은 이상합니다. 모든 사례를 올바르게 파악할 수는 없으며 전반적으로 나쁜 방법으로 보입니다.

해결책?

따라서 누구나이 효과를 만들 수있는 방법이나 프로파일 링 알고리즘을 작성하는 방향에 대한 대안 솔루션을 제공 할 수 있다면 매우 감사 할 것입니다!


7
한 번 봐 가지고 이 글 과뿐만 아니라 링크 된 기사, 특히 이 하나 . 블로그 자체에는 시작점으로 사용할 수있는 많은 아이디어가 포함되어 있습니다. 개요는 다음과 같습니다 .
Darcara

알고리즘을 단순화해야합니다. 이 검사 두 차원 - 셀룰러 오토마타
user1097489

답변:


117

이 알고리즘의 기본 아이디어는 전처리 단계를 사용하여 모든 모서리를 찾은 다음 모서리의 모양에 따라 올바른 스무딩 타일을 선택하는 것입니다.

첫 번째 단계는 모든 모서리를 찾는 것입니다. 아래 예 에서 X로 표시된 가장자리 타일 은 모두 8 개의 이웃 타일 중 하나 이상으로 황갈색 타일이있는 녹색 타일입니다. 지형 유형이 다른 경우이 조건은 낮은 지형 번호의 이웃이있는 경우 모서리 타일로 변환 될 수 있습니다.

가장자리 타일.

모든 모서리 타일이 감지되면 다음으로해야 할 일은 각 모서리 타일에 적합한 스무딩 타일을 선택하는 것입니다. 스무딩 타일에 대한 설명입니다.

스무딩 타일.

실제로 다른 유형의 타일은 많지 않습니다. 3x3 정사각형 중 하나에서 8 개의 외부 타일이 필요하지만 직선 타일이 이미 첫 번째 정사각형에 있기 때문에 다른 모서리에서 4 개의 정사각형 사각형 만 필요합니다. 이것은 우리가 구별해야 할 총 12 가지 경우가 있다는 것을 의미합니다.

이제 하나의 모서리 타일을 보면 가장 가까운 네 개의 인접 타일을보고 경계가 회전하는 방식을 결정할 수 있습니다. 위와 같이 X로 가장자리 타일을 표시하면 다음 6 가지 경우가 있습니다.

6 건

이러한 경우는 해당 스무딩 타일을 결정하는 데 사용되며 그에 따라 스무딩 타일의 번호를 지정할 수 있습니다.

숫자가있는 매끄러운 타일.

각 경우에 대해 여전히 a 또는 b를 선택할 수 있습니다. 이것은 잔디가 어느쪽에 있는지에 달려 있습니다. 이것을 결정하는 한 가지 방법은 경계의 방향을 추적하는 것이지만 아마도 가장 간단한 방법은 가장자리 옆에 하나의 타일을 선택하여 색상이 무엇인지 확인하는 것입니다. 아래 이미지는 예를 들어 오른쪽 상단 타일의 색상을 확인하여 구별 할 수있는 두 경우 5a)와 5b)를 보여줍니다.

5a 또는 5b 선택.

원래 예제의 최종 열거는 다음과 같습니다.

최종 열거.

해당 가장자리 타일을 선택한 후 테두리는 다음과 같습니다.

최종 결과.

마지막으로 경계가 다소 규칙적인 한 이것이 효과가 있다고 말할 수 있습니다. 보다 정확하게는 정확히 2 개의 모서리 타일이없는 모서리 타일은 인접 타일로 별도로 처리해야합니다. 이는 단일 엣지 이웃이있는 맵의 엣지 타일과 인접한 엣지 타일의 수가 3 개 또는 4 개일 수있는 매우 좁은 지형 조각에 대해 발생합니다.


1
이것은 위대하고 매우 도움이됩니다. 일부 타일은 다른 타일로 직접 전환 할 수없는 경우를 다루고 있습니다. 예를 들어, "흙"타일은 "가벼운 잔디"로 전환 될 수 있고 "가벼운 잔디"는 "중간 잔디"로 전환 될 수 있습니다. Tiled (mapeditor.org)는 지형 브러시에 대해 일부 유형의 트리 검색을 구현하여이를 처리하는 데 큰 도움이됩니다. 그래도 나는 그것을 재현 할 수 없었습니다.
Clay

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 = 백만 회 미만의 반복이 빠릅니다. 반복을 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

이것을 조금 더 확장 할 수 있습니까? 궁금해서 설명을 이해할 수 없습니다. 반복을 한 후에 평균 색상 값을 어떻게 사용합니까?
Chii

1
각 그리드 포인트 그리드 [i] [j]는 적절한 색상의 작은 사각형 (또는 개별 픽셀)으로 캔버스에 그려 질 수 있습니다.
로버트 킹

5

첫 번째 생각은 문제에 대한 완벽한 솔루션을 자동화하기 위해서는 약간의 보간법이 필요하다는 것입니다. 사전 렌더링 된 타일 이미지에 대해 언급 했으므로 여기에서는 전체 보간 솔루션이 보증되지 않는다고 가정합니다.

당신이 말했듯이, 손으로지도를 마무리하면 좋은 결과를 얻을 수 있습니다 ...하지만 결함을 수정하는 수동 프로세스도 옵션이 아니라고 가정합니다.

완벽한 결과를 제공하지는 않지만 간단한 노력을 기울이면 매우 보람있는 간단한 알고리즘이 있습니다.

모든 모서리 타일을 혼합하려고 시도하는 대신 (인터 레이팅 인접 타일을 먼저 혼합 한 결과를 알아야하거나 전체 맵을 여러 번 수정해야하며 사전 생성 된 타일에 의존 할 수 없음) 대체 바둑판 패턴으로 타일을 혼합하지 않는 이유는 무엇입니까?

[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'는 대각선이됩니다.

타일 ​​모퉁이에는 작은 불연속성이 있지만 제시 한 예와 비교하면 약간 불 연속적입니다.

가능한 경우이 이미지를 나중에 이미지로 업데이트하겠습니다.


이것은 좋은 소리, 나는 당신이 의미하는 바를 더 잘 이해하기 위해 일부 이미지로 그것을보고 싶습니다.
Dan Prince

내가 생각했던 소프트웨어가 없기 때문에 이미지를 함께 넣을 수 없습니다. 물론 대각선 전환을 수행 할 수 있지만이 전환 알고리즘으로 다른 전환은 실제로 도움이되지 않습니다. 지도에 90도 전환이 포함되지 않을 수도 있습니다. 미안하지만, 이건 조금 실망스러운 것 같아요.
완벽 주의자

3

나는 이것과 비슷한 것을 가지고 놀고 있었는데, 여러 가지 이유로 끝나지 않았습니다. 그러나 기본적으로 0과 1의 매트릭스가 필요합니다. 0은 접지이고 1은 플래시의 미로 생성기 응용 프로그램의 벽입니다. 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 개가 위쪽 / 아래쪽 이웃의 값을 나타내고 두 번째가 대각선을 나타내는 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가되고 나머지는 해결해야합니다. 그러나 특정 타일에 대한 조회는 쉽지 않습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.