2D City Builder에서 비싼 기능의 성능을 향상시키는 방법


9

이미 답변을 검색했지만 비싼 함수 / 계산을 처리하는 가장 좋은 방법을 찾지 못했습니다.

현재 게임 (2D 타일 기반 도시 건물)에서 사용자는 건물을 배치하고 도로를 건설 할 수 있습니다. 모든 건물은 사용자가지도의 경계에 배치해야하는 교차점에 연결해야합니다. 건물이이 정션에 연결되지 않은 경우 영향을받는 건물 위에 "도로에 연결되지 않음"표시가 나타납니다 (그렇지 않으면 제거해야 함). 대부분의 건물에는 반경이 있으며 서로 관련이있을 수 있습니다 (예 : 소방서에서 반경 30 타일 내에있는 모든 주택을 도울 수 있음). 그것이 도로 연결이 변경 될 때 업데이트 / 확인해야하는 것입니다.

어제 큰 성능 문제가 발생했습니다. 다음 시나리오를 살펴 보겠습니다. 사용자는 물론 건물과 도로를 지울 수도 있습니다. 따라서 사용자가 접속점 바로 다음에 연결을 끊으면 동시에 많은 건물을 업데이트해야합니다 . 첫 번째 조언 중 하나는 중첩 루프를 피하는 것입니다 (이 시나리오에서는 분명히 큰 이유입니다).하지만 확인해야합니다 ...

  1. 도로 타일이 제거 된 경우 건물이 여전히 교차점에 연결된 경우 (해당 도로의 영향을받는 건물에만 해당). (이 시나리오에서는 더 작은 문제 일 수 있습니다)
  2. 반지름 타일 목록을 작성하고 반지름 내에 건물을 가져옵니다 (중첩 루프-큰 문제!) .

    // Go through all buildings affected by erasing this road tile.
    foreach(var affectedBuilding in affectedBuildings) {
        // Get buildings within radius.
        foreach(var radiusTile in affectedBuilding.RadiusTiles) {
            // Get all buildings on Map within this radius (which is technially another foreach).
            var buildingsInRadius = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);  
    
            // Do stuff.
        }
    }

이 모든 것이 FPS 를 1 초 동안 60에서 거의 10으로 나눕니다 .

그렇게 할 수있을 것입니다. 내 아이디어는 다음과 같습니다.

  • 이 스레드에는 주 스레드 (업데이트 기능)를 사용하지 않고 다른 스레드를 사용합니다. 멀티 스레딩을 사용할 때 잠금 문제가 발생할 수 있습니다.
  • 대기열을 사용하여 많은 계산 처리 (이 경우 가장 좋은 방법은 무엇입니까?)
  • 더 많은 계산 (예 : 반경이있는 건물)을 피하려면 객체 (건물)에 더 많은 정보를 보관하십시오.

마지막 접근법을 사용하여 대신이 foreach 형태로 하나의 중첩을 제거 할 수 있습니다.

// Go through all buildings affected by erasing this road tile.
foreach(var affectedBuilding in affectedBuildings) {
    // Go through buildings within radius.
    foreach(var buildingInRadius in affectedBuilding.BuildingsInRadius) {
        // Do stuff.
    }
}

그러나 이것이 충분한 지 모르겠습니다. 플레이어가 큰지도를 가지고있는 경우 도시 스카이 라인과 같은 게임은 훨씬 더 많은 건물을 처리해야합니다. 그들은 어떻게 그런 일을 처리합니까?! 모든 건물이 동시에 업데이트되지는 않으므로 업데이트 대기열이있을 수 있습니다.

나는 당신의 아이디어와 의견을 기대하고 있습니다!

고마워요!


2
프로파일 러를 사용하면 문제가있는 코드를 식별하는 데 도움이됩니다. 영향을받는 건물을 찾거나 // 할 일이있을 수 있습니다. 부수적으로 City Skylines를 좋아하는 대형 게임은 쿼드 트리와 같은 공간 데이터 구조를 사용하여 이러한 문제를 해결하므로 모든 공간 쿼리는 for 루프로 배열을 통과하는 것보다 훨씬 빠릅니다. 예를 들어 모든 건물의 종속성 그래프를 가질 수 있으며 해당 그래프를 따르면 반복없이 무엇이 영향을 미치는지 즉시 알 수 있습니다.
Exaila

자세한 정보에 감사드립니다. 나는 의존성의 아이디어를 좋아한다! 나는 이것 하나를 볼 것입니다!
Yheeky

당신의 조언은 위대했습니다! 방금 연결 프로파일이 여전히 유효한지 확인하기 위해 영향을받는 각 건물에 대한 경로 찾기 기능이 있음을 보여준 VS 프로파일 러를 사용했습니다. 물론 그것은 지옥만큼 비싸다! 약 5 FPS이지만 아무것도 아닌 것보다 낫습니다. 나는 그것을 없애고 도로 타일에 건물을 할당 하므로이 길 찾기를 반복해서 반복 할 필요가 없습니다. 고마워요! 아니요, 더 큰 건물 인 반경 문제 만 수정하면됩니다.
Yheeky

유용하다고 생각합니다 : D
Exaila

답변:


3

캐싱 빌딩 범위

어떤 건물이 이펙터 건물 범위에 있는지에 대한 정보를 캐싱한다는 아이디어는 (이펙터에서 또는 영향을 받아 캐시에 저장할 수 있음) 확실히 좋은 생각입니다. 건물 (일반적으로)은 움직이지 않으므로 값 비싼 계산을 다시 실행할 이유가 거의 없습니다. "이 건물에 미치는 영향"및 "이 건물에 미치는 영향"은 건물을 만들거나 제거 할 때만 확인하면됩니다.

이것은 메모리에 대한 CPU주기의 고전적인 교환입니다.

지역별 커버리지 정보 처리

이 정보를 추적하기 위해 너무 많은 메모리를 사용하고있는 것으로 판명되면 맵 영역별로 해당 정보를 처리 할 수 ​​있는지 확인하십시오. 지도를 n*의 정사각형 영역으로 나눕니다.n타일. 소방서가 해당 지역을 완전히 덮으면 해당 지역의 모든 건물도 포함됩니다. 따라서 개별 건물이 아닌 지역별로 적용 범위 정보 만 저장하면됩니다. 지역이 부분적으로 만 덮여있는 경우 해당 지역의 빌딩 별 연결 처리로 폴백해야합니다. 따라서 건물의 업데이트 기능은 먼저 "이 건물이 소방서에 속하는 지역입니까?"를 확인합니다. "이 건물이 개별적으로 소방서에 의해 보호됩니까?" 또한 소방서가 제거 될 때 더 이상 2000 개 건물의 적용 상태를 업데이트 할 필요가없고 100 개 건물과 25 개 지역 만 업데이트하면되기 때문에 업데이트 속도가 빨라집니다.

지연된 업데이트

또 다른 최적화는 모든 것을 즉시 업데이트하지 않고 동시에 모든 것을 업데이트하지 않는 것입니다.

건물이 도로 네트워크에 여전히 연결되어 있는지 여부는 모든 단일 프레임을 확인해야하는 것은 아닙니다 (그런데, 그래프 이론을 조금 살펴봄으로써이를 최적화하는 방법을 찾을 수도 있습니다). 건물이 건축 된 후 몇 초마다 주기적으로 건물을 점검하는 것만으로도 충분할 것입니다 (그리고 도로망에 변화가있을 경우). 건물 범위 효과에도 동일하게 적용됩니다. 건물이 수백 프레임마다 "만약 나에게 영향을 미치는 소방서 중 하나가 여전히 활동하고 있습니까?"

따라서 업데이트 루프는 매 업데이트마다 한 번에 수백 개의 건물에 대해 비싼 계산 만 수행하도록 할 수 있습니다. 현재 화면에있는 건물에 우선 순위를 부여하여 플레이어가 자신의 행동에 대한 즉각적인 피드백을받을 수 있습니다.

멀티 스레딩 관련

도시 건설업자는 계산 비용이 많이 드는 경향이 있습니다. 특히 플레이어가 실제로 큰 건물을 건설하고 시뮬레이션 복잡도가 높은 경우에는 더욱 그렇습니다. 따라서 장기적으로 게임에서 어떤 계산이 비동기 적으로 처리 될 수 있는지 생각하는 것은 잘못이 아닐 수 있습니다.


이것은 SNES의 SimCity가 전원을 다시 연결하는 데 시간이 걸리는 이유를 설명합니다. 다른 지역 전체의 영향으로도 발생한다고 생각합니다.
lozzajp

유용한 의견 감사합니다! 또한 메모리에 더 많은 정보를 저장하면 게임 속도가 빨라질 수 있다고 생각합니다. 또한 TileMap을 영역으로 나누는 아이디어가 마음에 들지만이 방법이 초기 문제를 오래 제거 할 수 있을지 모르겠습니다. 지연 업데이트에 관한 질문이 있습니다. FPS를 60에서 45로 낮추는 함수가 있다고 가정하겠습니다. CPU가 처리 할 수있는 완벽한 양을 처리하기 위해 계산을 분할하는 가장 좋은 방법은 무엇입니까?
Yheeky

@Yheeky 이것에 대해 보편적으로 적용 가능한 솔루션은 없습니다. 어떤 계산을 지연시킬 수 있는지, 어떤 계산할 수 없는지, 합리적인 계산 단위인지에 따라 상황에 따라 달라지기 때문입니다.
Philipp

이 계산을 지연시키는 방법은 "CurrentlyUpdating"플래그가있는 항목으로 큐를 만드는 것입니다. 이 플래그가 true로 설정된 항목 만 처리되었습니다. 계산이 완료되면 항목이 목록에서 제거되고 다음 항목이 처리되었습니다. 이것은 효과가 있을까요? 그러나 하나의 계산 자체가 FPS를 낮추는 것을 알고 있다면 어떤 종류의 방법을 사용할 수 있습니까?
Yheeky

1
@Yheeky 내가 말했듯이 보편적으로 적용 가능한 솔루션은 없습니다. 내가 일반적으로 시도하는 것 (순서대로) : 1.보다 적절한 알고리즘 및 / 또는 데이터 구조를 사용하여 계산을 최적화 할 수 있는지 확인하십시오. 2. 개별 작업을 지연시킬 수있는 하위 작업으로 나눌 수 있는지 확인하십시오. 3. 별도의 위협으로 할 수 있는지 확인하십시오. 4. 계산이 필요한 게임 메카닉을 제거하고 계산 비용이 덜 드는 것으로 대체 할 수 있는지 확인하십시오.
Philipp

3

1. 중복 작업 .

당신 affectedBuildings은 아마도 서로 가까이 있기 때문에 다른 반지름이 겹칠 것입니다. 업데이트해야 할 건물에 플래그를 지정한 다음 업데이트하십시오.

var toBeUpdated = new HashSet<Tiles>();
foreach(var affectedBuilding in affectedBuildings) {
    foreach(var radiusTile in affectedBuilding.RadiusTiles) {
         toBeUpdated.Add(radiusTile);

}
foreach (var tile in toBeUpdated)
{
    var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);
    // Do stuff.
}

2. 부적절한 데이터 구조.

var buildingsInTile = TileMap.Buildings.Where(b => b.TileIndex == radiusTile.TileIndex);

분명히해야한다

var buildingsInRadius = tile.Buildings;

여기서 건물은 IEnumerable반복 시간이 일정한 (예 : a List<Building>)


좋은 지적! MoreLINQ를 사용하는 Distinct () 사용하려고 시도했지만 중복 검사보다 빠를 수 있음에 동의합니다.
Yheeky
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.