어떻게했는지 : Terraria의 수백만 개의 타일


45

나는 Terraria 와 비슷한 게임 엔진을 주로 도전 과제 로 연구 해 왔으며 대부분을 알아 냈지만 수백만 개의 상호 작용 가능 / 수확 가능한 타일을 처리하는 방법에 대해 머리를 감쌀 수는 없습니다. 게임은 한 번에 있습니다. 내 엔진에서 약 500.000 타일, 즉 Terraria 에서 가능한 것의 1/20을 만들면 프레임 속도가 60에서 20으로 떨어집니다. 심지어 타일을 볼 때만 렌더링합니다. 나는 타일로 아무것도하지 않고 단지 메모리에 보관합니다.

업데이트 : 내가하는 일을 보여주기 위해 코드가 추가되었습니다.

이것은 타일을 처리하고 그리는 클래스의 일부입니다. 범인이 "foreach"부분이라고 생각합니다. 빈 인덱스까지 모든 것을 반복합니다.

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        foreach (Tile tile in this.Tiles)
        {
            if (tile != null)
            {
                if (tile.Position.X < -this.Offset.X + 32)
                    continue;
                if (tile.Position.X > -this.Offset.X + 1024 - 48)
                    continue;
                if (tile.Position.Y < -this.Offset.Y + 32)
                    continue;
                if (tile.Position.Y > -this.Offset.Y + 768 - 48)
                    continue;
                tile.Draw(spriteBatch, gameTime);
            }
        }
    }
...

또한 Tile.Draw 메서드도 있습니다. 각 Tile은 SpriteBatch.Draw 메서드를 4 번 호출하므로 업데이트를 수행 할 수도 있습니다. 이것은 자동 타일링 시스템의 일부이며 인접한 타일에 따라 각 모서리를 그리는 것을 의미합니다. texture_ *는 사각형이며, 각 업데이트가 아닌 레벨 생성시 한 번 설정됩니다.

...
    public virtual void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        if (this.type == TileType.TileSet)
        {
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position, texture_tl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 0), texture_tr, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(0, 8), texture_bl, this.BlendColor);
            spriteBatch.Draw(this.texture, this.realm.Offset + this.Position + new Vector2(8, 8), texture_br, this.BlendColor);
        }
    }
...

내 코드에 대한 비판이나 제안은 환영합니다.

업데이트 : 솔루션이 추가되었습니다.

마지막 Level.Draw 메서드는 다음과 같습니다. Level.TileAt 메소드는 OutOfRange 예외를 피하기 위해 입력 된 값을 확인합니다.

...
    public void Draw(SpriteBatch spriteBatch, GameTime gameTime)
    {
        Int32 startx = (Int32)Math.Floor((-this.Offset.X - 32) / 16);
        Int32 endx = (Int32)Math.Ceiling((-this.Offset.X + 1024 + 32) / 16);
        Int32 starty = (Int32)Math.Floor((-this.Offset.Y - 32) / 16);
        Int32 endy = (Int32)Math.Ceiling((-this.Offset.Y + 768 + 32) / 16);

        for (Int32 x = startx; x < endx; x += 1)
        {
            for (Int32 y = starty; y < endy; y += 1)
            {
                Tile tile = this.TileAt(x, y);
                if (tile != null)
                    tile.Draw(spriteBatch, gameTime);

            }
        }
    }
...

2
카메라 뷰의 내용 만 렌더링한다는 점에서 절대적으로 긍정적입니까? 올바른 렌더링을 결정하는 코드라는 의미입니까?
Cooper

5
할당 된 메모리 때문에 프레임 속도가 60에서 20fps로 감소합니까? 그것은 매우 가능성이 낮습니다. 무언가 잘못되었을 것입니다. 어떤 플랫폼입니까? 시스템이 가상 메모리를 디스크로 바꾸고 있습니까?
Maik Semder

6
@Drackir이 경우 타일 클래스가 있고 적절한 길이의 정수 배열이 있어야하고 50 만 개의 객체가있을 때 OO 오버 헤드가 농담이 아니라면 잘못이라고 말합니다. 객체로 할 수 있다고 생각하지만 요점은 무엇입니까? 정수 배열은 처리하기가 간단합니다.
aaaaaaaaaaaa

3
아야! 모든 타일을 반복 하고 호출하는 각각 의 타일에 네 번 그립니다. 여기에 가능한 몇 가지 개선 사항이 있습니다 ....
thedaian

11
당신이 볼 수있는 것만 렌더링하기 위해 아래에서 언급 한 멋진 파티셔닝이 필요하지 않습니다. 타일 ​​맵입니다. 이미 일반 그리드로 분할되어 있습니다. 화면 왼쪽 상단과 오른쪽 하단의 타일을 계산하고 해당 사각형으로 모든 것을 그립니다.
Blecki

답변:


56

렌더링 할 때 500,000 개의 타일을 모두 반복하고 있습니까? 그렇다면 문제의 일부가 될 수 있습니다. 렌더링 할 때 50 만 개의 타일을 반복하고 타일에 '업데이트'틱을 수행 할 때 50 만 개의 타일을 반복하면 각 프레임마다 백만 개의 타일을 반복합니다.

분명히,이 주위에 방법이 있습니다. 렌더링하는 동안 업데이트 틱을 수행 할 수 있으므로 모든 타일을 반복하는 데 소요되는 시간을 절반으로 줄일 수 있습니다. 그러나 이것은 렌더링 코드와 업데이트 코드를 하나의 함수로 묶으며 일반적으로 BAD IDEA 입니다.

화면에있는 타일을 추적하고 타일을 반복 (및 렌더링) 할 수 있습니다. 타일 ​​크기 및 화면 크기와 같은 항목에 따라 루프를 반복해야하는 타일의 양을 쉽게 줄일 수 있으므로 처리 시간이 상당히 절약됩니다.

마지막으로, 아마도 가장 큰 세계 게임이 가장 좋은 옵션은 지형을 지역으로 나누는 것입니다. 세계를 512x512 타일의 덩어리로 분할하고 플레이어가 지역에 가까워 지거나 지역에서 멀어 질수록 지역을로드 / 언로드합니다. 또한 모든 종류의 '업데이트'틱을 수행하기 위해 멀리 떨어진 타일을 반복하지 않아도됩니다.

엔진이 타일에 대해 어떤 종류의 업데이트 틱도 수행하지 않는 경우에는 이러한 답변 중 일부를 무시할 수 있습니다.


5
내가 기억하는 바에 따르면, 그 지역 부분은 흥미로워 보인다. 그것이 마인 크래프트가하는 방식이다. 그러나 잔디 퍼짐, 선인장 재배 등과 같이 근처에 아무데도 없더라도 Terraria의 지형이 진화하는 방식으로는 작동하지 않는다. 편집 : 물론 지역이 활성화 된 이후 경과 시간을 적용한 다음 해당 기간에 발생한 모든 변경을 수행합니다. 실제로 작동 할 수 있습니다.
윌리엄 마리아 거

2
Minecraft는 확실히 지역을 사용합니다 (그리고 나중에 Ultima 게임이 그랬습니다. 그리고 많은 Bethesda 게임이 확실합니다). Terraria가 지형을 처리하는 방법은 확실하지 않지만 "새"지역에 경과 시간을 적용하거나 전체지도를 메모리에 저장합니다. 후자는 높은 RAM 사용을 요구하지만 대부분의 최신 컴퓨터는이를 처리 할 수 ​​있습니다. 언급되지 않은 다른 옵션도있을 수 있습니다.
thedaian

2
@MindWorX : 다시로드 할 때 모든 업데이트를 수행하는 것이 항상 작동하지는 않습니다. 불모의 영역이 가득 차고 잔디가 있다고 가정하십시오. 당신은 몇 년 동안 떠나서 잔디를 향해 걷습니다. 잔디가있는 블록이로드되면 붙잡고 해당 블록에 퍼지지 만 이미로드 된 더 가까운 블록에는 퍼지지 않습니다. 하지만 이런 일들 은 게임보다 훨씬 느리게 진행 됩니다. 한 번의 업데이트주기에서 타일의 작은 하위 집합 만 확인하면 시스템을로드하지 않고도 먼 지형을 실시간으로 만들 수 있습니다.
Loren Pechtel

1
영역이 플레이어 근처에있을 때 "잡기"의 대안 (또는 보충)으로, 각 먼 영역을 반복하여 느린 속도로 업데이트하는 별도의 시뮬레이션을 사용할 수 있습니다. 각 프레임마다 시간을 예약하거나 별도의 스레드에서 실행되도록 할 수 있습니다. 예를 들어 플레이어가있는 영역과 프레임 당 6 개의 인접 영역을 업데이트 할 수 있지만 1 ~ 2 개의 추가 영역도 업데이트 할 수 있습니다.
Lewis Wakeford 2016 년

1
궁금한 사람은 Terraria가 전체 타일 데이터를 2D 배열 (최대 크기는 8400x2400)로 저장합니다. 그런 다음 간단한 수학을 사용하여 렌더링 할 타일 섹션을 결정합니다.
FrenchyNZ

4

나는 어떤 대답으로도 처리되지 않은 큰 실수를 본다. 물론 더 많은 타일을 그리거나 반복해서는 안되며, 필요합니다. 덜 분명한 것은 실제로 타일을 정의하는 방법입니다. 타일 ​​클래스를 만든 것을 볼 수 있듯이 항상 그렇게 했었지만 큰 실수입니다. 아마도 해당 클래스에 모든 종류의 함수가 있으며 불필요한 처리가 많이 발생합니다.

처리하는 데 실제로 필요한 것만 반복해야합니다. 따라서 타일에 실제로 필요한 것이 무엇인지 생각해보십시오. 그리려면 텍스처 만 있으면되지만 실제 이미지는 처리하기가 커서 반복하기를 원하지 않습니다. int [,] 또는 unsigned byte [,]를 만들 수도 있습니다 (255 개 이상의 타일 텍스처를 기대하지 않는 경우). 이 작은 배열을 반복하고 스위치 또는 if 문을 사용하여 텍스처를 그리기 만하면됩니다.

무엇을 업데이트해야합니까? 유형, 건강 및 피해가 충분 해 보입니다. 이들 모두는 바이트로 저장 될 수 있습니다. 업데이트 루프를 위해 이와 같은 구조체를 만들어 보시지 않겠습니까?

struct TileUpdate
{
public byte health;
public byte type;
public byte damage;
}

실제로 유형을 사용하여 타일을 그릴 수 있습니다. 따라서 구조체에서 그 것을 분리하여 (자체 배열을 만들 수 있음) 드로우 루프의 불필요한 건강 및 피해 필드를 반복하지 않아도됩니다. 업데이트 목적을 위해서는 화면보다 넓은 영역을 고려해야 게임 세계가 더 활기차게 느껴지므로 (엔터티는 화면에서 위치를 변경 함) 사물을 그리기 위해서는 보이는 타일 만 있으면됩니다.

위의 구조체를 유지하면 타일 당 3 바이트 만 걸립니다. 따라서 저장 및 메모리 목적으로 이상적입니다. 처리 속도의 경우 int 또는 byte를 사용하는 경우 또는 64 비트 시스템이있는 경우 long int를 사용하는 것은 실제로 중요하지 않습니다.


1
그래, 나중에 내 엔진의 반복은 그것을 사용하도록 진화했습니다. 주로 메모리 소비를 줄이고 속도를 높이기 위해. ByRef 유형은 ByVal 구조체로 채워진 배열에 비해 느립니다. 그럼에도 불구하고 이것을 답변에 추가하는 것이 좋습니다.
William Mariager 2016 년

1
예, 임의의 알고리즘을 찾는 동안 우연히 발견되었습니다. 코드에서 자체 타일 클래스를 사용하는 위치를 즉시 알 수 있습니다. 새로 오셨을 때 매우 흔한 실수입니다. 2 년 전에 게시 한 이후에도 여전히 활동적인 모습을 보게되어 반갑습니다.
Madmenyo

3
health또는 필요하지 않습니다 damage. 가장 최근에 선택한 타일 위치의 작은 버퍼와 각각의 손상을 유지할 수 있습니다. 새 타일을 선택하고 버퍼가 가득 찬 경우 새 타일을 추가하기 전에 가장 오래된 위치에서 롤오프하십시오. 이것은 한 번에 채굴 할 수있는 타일 수를 제한하지만 어쨌든 (거의 #players * pick_tile_size) 이것에 대한 본질적인 제한이 있습니다. 이 목록을 플레이어별로 쉽게 유지할 수 있습니다. 크기 속도에 중요합니다. 작은 크기는 각 CPU 캐시 라인에 더 많은 타일을 의미합니다.
Sean Middleditch

@SeanMiddleditch 당신이 맞지만 그게 요점이 아닙니다. 요점은 화면에 타일을 그리고 계산을 실제로하기 위해 실제로 필요한 것을 멈추고 생각하는 것이 었습니다. OP가 전체 클래스를 사용하고 있었기 때문에 간단한 예제를 만들었으므로 간단한 학습 과정에서 먼 길을갑니다. 이제 픽셀 밀도에 대한 내 주제를 잠금 해제하십시오. 프로그래머는 분명히 CG 아티스트의 눈에 그 질문을 보려고합니다. 이 사이트는 게임을위한 CG 사이트이기도합니다.
Madmenyo

2

사용할 수있는 인코딩 기술이 다릅니다.

RLE : 좌표 (x, y)로 시작한 다음 하나의 축을 따라 같은 타일이 나란히 (길이) 몇 개나 있는지 계산합니다. 예 : (1,1,10,5)는 좌표 1,1에서 시작하여 타일 유형 5에 나란히 10 개의 타일이 있음을 의미합니다.

대규모 배열 (비트 맵) : 배열의 각 요소는 해당 영역에있는 타일 유형을 유지합니다.

편집 : 방금이 훌륭한 질문을 보았습니다 : 지도 생성을위한 랜덤 시드 기능?

Perlin 소음 발생기는 좋은 해답 인 것 같습니다.


인코딩 (및 펄린 노이즈)에 대해 이야기하고 있기 때문에 묻는 질문에 대답하고 있는지 확실하지 않으며 성능 문제를 다루는 것으로 보입니다.
thedaian

1
적절한 인코딩 (예 : 펄린 노이즈)을 사용하면 모든 타일을 한 번에 메모리에 고정하는 것에 대해 걱정할 필요가 없습니다. 대신 인코더 (소음 발생기)에게 주어진 좌표에 표시 될 내용을 알려 주기만하면됩니다. 여기에는 CPU주기와 메모리 사용량간에 균형이 있으며 노이즈 생성기가 해당 균형 조정 작업에 도움을 줄 수 있습니다.
Sam Ax

2
여기서 문제는 사용자가 어떻게 든 지형을 수정할 수있는 게임을 만드는 경우 소음 발생기를 사용하여 해당 정보를 "저장"하는 것만으로는 작동하지 않는다는 것입니다. 또한 노이즈 생성은 대부분의 인코딩 기술과 마찬가지로 상당히 비쌉니다. 실제 게임 플레이 (물리, 충돌, 조명 등)에 CPU주기를 절약하는 것이 좋습니다. Perlin 노이즈는 지형 생성에 적합하고 인코딩은 디스크에 저장하는 데 적합하지만이 경우 메모리를 처리하는 더 좋은 방법이 있습니다.
thedaian

그러나 thedaian과 마찬가지로 지형도는 사용자와 상호 작용하므로 수학적으로 정보를 검색 할 수 없습니다. 어쨌든 기본 지형을 생성하기 위해 펄린 노이즈를 살펴 보겠습니다.
윌리엄 마리아 거

1

이미 제안한대로 타일 맵을 분할해야합니다. 예를 들어 Quadtree 구조 를 사용하면 불필요한 (보이지 않는) 타일의 잠재적 인 처리 (예 : 단순히 루핑 스루)를 제거 할 수 있습니다. 이렇게하면 처리해야 할 수있는 것만 처리하고 데이터 세트 (타일 맵)의 크기를 늘리더라도 실제 성능 저하가 발생하지 않습니다. 물론 나무의 균형이 잘 잡혀 있다고 가정합니다.

"오래된"을 반복하여 둔한 소리 나 말을하고 싶지 않지만 최적화 할 때는 항상 툴체인 / 컴파일러가 지원하는 최적화를 사용하는 것을 잊지 마십시오. 그렇습니다. 조기 최적화는 모든 악의 근원입니다. 컴파일러를 신뢰하십시오. 대부분의 경우 항상 당신보다 잘 알고 있지만 항상두 번 측정하고 결코 추측에 의존하지 마십시오. 실제 병목 현상의 위치를 ​​모르는 한 가장 빠른 알고리즘을 빠르게 구현하는 것이 아닙니다. 그렇기 때문에 프로파일 러를 사용하여 코드의 가장 느린 경로를 찾고 제거하거나 최적화하는 데 집중해야합니다. 타겟 아키텍처에 대한 저수준 지식은 종종 하드웨어가 제공하는 모든 것을 짜내기 위해 필수적이므로, 이러한 CPU 캐시를 연구하고 분기 예측 변수가 무엇인지 배우십시오. 캐시 / 브랜치 히트 / 미스에 대해 프로파일 러가 알려주는 내용을 확인하십시오. 그리고 어떤 형태의 트리 데이터 구조를 사용하면 다른 방법보다 지능적인 데이터 구조와 벙어리 알고리즘을 사용하는 것이 좋습니다. 성능면에서 데이터가 가장 먼저 나타납니다. :)


"조기 최적화는 모든 악의 근원"+1에 대해 유죄입니다. (
thedaian

16
-1 쿼드 트리? 조금 과잉? 이와 같이 2D 게임에서 모든 타일의 크기가 같은 경우 (테라 리아 용) 화면에 어떤 범위의 타일이 있는지 알아내는 것이 매우 쉽습니다. 가장 오른쪽 타일.
BlueRaja-대니 Pflughoeft

1

무승부 전화가 너무 많지 않습니까? 모든 맵 타일 텍스처를 하나의 이미지 (타일 아틀라스)에 넣으면 렌더링하는 동안 텍스처가 전환되지 않습니다. 그리고 모든 타일을 하나의 메쉬로 일괄 처리하는 경우 한 번의 호출로 그려야합니다.

다이내믹 한 조정에 대해 ... 아마 쿼드 트리는 그렇게 나쁜 생각이 아닐 수도 있습니다. 타일이 잎에 놓여지고 잎이 아닌 노드가 자식에서 배치 된 메쉬라고 가정하면 루트는 모든 타일을 하나의 메쉬로 일괄 처리해야합니다. 하나의 타일을 제거하려면 루트까지 노드 업데이트 (메시 재배치)가 필요합니다. 그러나 모든 트리 레벨에서 메시 재배치 된 wchich의 4 분의 1만이 4 * tree_height 메시 조인이어야합니까?

오 그리고 클리핑 알고리즘 에서이 트리를 사용하면 항상 루트 노드가 아닌 일부 하위 노드를 렌더링하므로 모든 노드를 루트까지 업데이트 / 재 배치 할 필요는 없지만 (잎이 아닌) 노드까지 업데이트 할 필요가 없습니다. 현재 렌더링.

내 생각, 코드 없음, 어쩌면 말도 안될 수도 있습니다.


0

@arrival이 맞습니다. 문제는 드로우 코드입니다. 프레임 당 4 * 3000 + 그리기 쿼드 명령 (24000+ 그리기 다각형 명령)으로 구성된 배열입니다 . 그런 다음 이러한 명령이 처리되어 GPU로 파이프됩니다. 이것은 오히려 나쁘다.

몇 가지 해결책이 있습니다.

  • 타일의 큰 블록 (예 : 화면 크기)을 정적 텍스처로 렌더링하고 한 번의 호출로 그립니다.
  • 매 프레임마다 GPU에 지오메트리를 보내지 않고 타일을 그리려면 셰이더를 사용하십시오 (예 : 타일 맵을 GPU에 저장).

0

당신이해야 할 일은 세계를 지역으로 나누는 것입니다. Perlin 노이즈 지형 생성은 공통 시드를 사용할 수 있으므로, 세계가 사전 생성되지 않은 경우에도 시드는 노이즈 알고리즘의 일부를 형성하여 새로운 지형을 기존 파트로 멋지게 마무리합니다. 이런 식으로, 플레이어가 한 번에 한 화면 씩보기 전에 작은 버퍼 이상을 계산할 필요는 없습니다 (현재 주변 화면 몇 개).

플레이어의 현재 화면에서 멀리 떨어진 지역에서 자라는 식물과 같은 것을 처리하는 관점에서 타이머를 사용할 수 있습니다. 이 타이머는 플랜트에 대한 정보, 위치 등을 저장하는 파일을 통해 반복됩니다. 타이머에서 파일을 읽거나 업데이트 / 저장하면됩니다. 플레이어가 세계의 해당 부분에 다시 도달하면 엔진은 정상적으로 파일을 읽고 화면에 새로운 플랜트 데이터를 표시합니다.

나는 작년과 비슷한 게임에서 수확과 농업을 위해이 기술을 사용했습니다. 플레이어는 들판에서 멀리 떨어져서 돌아 왔을 때 아이템이 업데이트되었습니다.


-2

나는 수많은 블록을 처리하는 방법에 대해 생각해 왔으며 내 머리에 오는 유일한 것은 Flyweight Design Pattern입니다. 당신이 모른다면, 나는 그것에 대해 깊이 읽을 것을 제안합니다 : 메모리와 처리를 절약 할 때 많은 도움이 될 수 있습니다 : http://en.wikipedia.org/wiki/Flyweight_pattern


3
-1이 답변은 너무 일반적으로 유용하지 않으며 제공 한 링크가이 특정 문제를 직접 해결하지는 않습니다. Flyweight 패턴은 대용량 데이터 세트를 효율적으로 관리하는 여러 가지 방법 중 하나입니다. Terraria와 같은 게임에서 타일을 저장하는 특정 상황에서이 패턴을 적용하는 방법에 대해 자세히 설명해 주시겠습니까?
postgoodism
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.