O (N ^ 2) 함수 개선 (모든 다른 엔티티를 반복하는 모든 엔티티)


21

약간의 배경 지식으로, 엔터티 시스템에 ENTT를 사용하여 C ++에서 친구와 함께 진화 게임 을 코딩하고 있습니다. 생물은 2D지도에서 걸어 다니고, 녹색이나 다른 생물을 먹고, 번식하고 그 특성을 변화시킵니다.

또한 게임이 실시간으로 실행될 때 성능은 양호하지만 (60fps 문제 없음) 중요한 변경 사항을보기 위해 4 시간을 기다리지 않아도 속도를 크게 높일 수 있기를 원합니다. 그래서 나는 가능한 빨리 그것을 얻고 싶다.

생물체가 음식을 찾는 효율적인 방법을 찾기 위해 고심하고 있습니다. 각 생물은 그들에게 가장 가까운 최고의 음식을 찾아야합니다.

게임의 스크린 샷 예

먹기를 원한다면, 중앙에 묘사 된 생물체는 149.64 반경 (시거리)에서 주변을 둘러보고 영양, 거리, 유형 (고기 또는 식물)을 기준으로 어떤 음식을 추구해야하는지 판단해야합니다. .

그들의 음식이 런타임의 약 70 %를 먹는 모든 생물을 찾는 책임이있는 기능. 현재 작성된 방식을 단순화하면 다음과 같습니다.

for (creature : all_creatures)
{
  for (food : all_entities_with_food_value)
  {
    // if the food is within the creatures view and it's
    // the best food found yet, it becomes the best food
  }
  // set the best food as the target for creature
  // make the creature chase it (change its state)
}

이 기능은 음식을 찾고 상태를 바꿀 때까지 음식을 찾는 모든 생물에 대해 모든 진드기를 실행합니다. 또한 특정 음식을 이미 쫓고있는 생물을 위해 새로운 음식이 생성 될 때마다 모든 사람이 이용할 수있는 최고의 음식을 찾도록합니다.

이 프로세스를보다 효율적으로 만드는 방법에 대한 아이디어에 열려 있습니다. 나는 O ( N 2 ) 에서 복잡성을 줄이고 싶습니다.O(N2) 그것이 가능한지 모르겠습니다.

내가 이미 그것을 향상시키는 한 가지 방법은 all_entities_with_food_value 그룹 생물이 먹을 수 없을 정도로 큰 음식을 반복 할 때 거기서 멈추는 것입니다. 다른 개선 사항은 환영합니다.

편집 : 답장을 보내 주셔서 감사합니다! 다양한 답변에서 다양한 것을 구현했습니다.

나는 유죄 기능이 5 틱마다 한 번 씩만 실행되도록 처음으로 간단하게 만들었습니다. 이로 인해 게임에 대해 아무것도 변경하지 않으면 서 게임이 약 4 배 빨라졌습니다.

그 후 나는 음식 검색 시스템에 음식과 같은 진드기가 생성 된 배열을 저장했습니다. 이 방법으로 나는 생물이 쫓고있는 음식과 나타난 새로운 음식을 비교하기 만하면됩니다.

마지막으로, 공간 분할에 대한 연구와 BVH 및 쿼드 트리를 고려한 후, 나는 그것이 더 간단하고 내 경우에 더 적합하다고 생각하기 때문에 후자와 함께 갔다. 나는 그것을 매우 빨리 구현하고 성능을 크게 향상 시켰습니다. 음식 검색은 거의 시간이 걸리지 않습니다!

이제 렌더링이 속도를 늦추고 있지만 다른 날에는 문제가됩니다. 모두 감사합니다!


2
동시에 실행되는 여러 CPU 코어에서 여러 스레드를 실험 해 보셨습니까?
Ed Marty

6
평균적으로 생물은 몇 마리입니까? 스냅 샷으로 판단 할 때 그렇게 높지 않은 것 같습니다. 항상 그런 경우 공간 분할은 큰 도움이되지 않습니다. 모든 틱에서이 기능을 실행 하지 않는 것을 고려 했습니까 ? 예를 들어 10 틱마다 실행할 수 있습니다. 시뮬레이션 결과는 질적으로 변하지 않아야합니다.
Turms

4
식품 평가에서 가장 많은 비용이 드는 부분을 파악하기 위해 상세한 프로파일 링을 수행 했습니까? 전반적인 복잡성을 보지 않고 질식하는 특정 계산 또는 메모리 구조 액세스가 있는지 확인해야 할 수도 있습니다.
Harabeck

순진한 제안 : 현재 수행중인 O (N ^ 2) 방식 대신 쿼드 트리 또는 관련 데이터 구조를 사용할 수 있습니다.
Seiyria

3
@Harabeck이 제안했듯이, 루프에서 모든 시간이 소비되는 곳을 더 깊이 파고 들었습니다. 예를 들어 거리에 대한 제곱근 계산 인 경우 XY 좌표를 비교하여 나머지 후보에 대해 비싼 sqrt를 수행하기 전에 많은 후보를 사전 제거 할 수 있습니다. if (food.x>creature.x+149.64 or food.x<creature.x-149.64) continue;성능이 충분한 경우 "복잡한"스토리지 구조를 구현하는 것보다 추가 가 쉬워야합니다. (관련 : 내부 루프에 코드를 조금 더 게시하면 도움이 될 수 있습니다)
AC

답변:


34

나는 당신이 이것을 충돌로 개념화하지 않는다는 것을 알고 있지만, 당신이하고있는 일은 모든 음식과 함께 생물을 중심으로하는 원을 충돌시키는 것입니다.

당신은 정말로 당신이 알고있는 음식을 확인하고 싶지 않고 단지 근처에있는 것을 확인하고 싶지 않습니다. 이것이 충돌 최적화에 대한 일반적인 조언입니다. 충돌을 최적화하는 기술을 검색하도록 장려하고 검색 할 때 C ++로 제한하지 마십시오.


음식을 찾는 생물

당신의 시나리오를 위해, 나는 세상을 그리드에 놓을 것을 제안합니다. 최소한 충돌하려는 원의 반경을 셀로 만듭니다. 그런 다음 생물이 위치한 하나의 셀과 최대 8 개의 이웃을 선택하고 최대 9 개의 셀만 검색 할 수 있습니다.

참고 : 더 작은 셀을 만들 수 있습니다. 즉, 검색중인 원이 이민 이웃을 넘어서서 반복해야한다는 것을 의미합니다. 그러나 문제가 너무 많은 음식이 있다면, 더 작은 세포는 더 적은 수의 음식 개체를 반복한다는 것을 의미 할 수 있으며, 이는 어느 시점에서 당신에게 유리합니다. 이것이 사실이라고 생각되면 테스트하십시오.

음식이 움직이지 않으면 음식 개체를 만들 때 그리드에 추가 할 수 있으므로 셀에 어떤 개체가 있는지 검색 할 필요가 없습니다. 대신 셀을 쿼리하면 목록이 있습니다.

셀의 크기를 2의 거듭 제곱으로 만들면 단순히 좌표를 잘라내어 생물이 위치한 셀을 찾을 수 있습니다.

가장 가까운 거리를 찾는 동안 제곱 거리 (일명 sqrt는 수행하지 않음)로 작업 할 수 있습니다. sqrt 작업이 적을수록 실행 속도가 빠릅니다.


새로운 음식 추가

새로운 음식이 추가되면 근처의 생물 만 깨워 야합니다. 이제는 셀에 생물 목록을 가져와야한다는 점을 제외하고는 같은 생각입니다.

훨씬 더 흥미롭게도, 쫓고있는 음식에서 얼마나 멀리 떨어져 있는지 생물에게 주석을 달면 그 거리를 직접 확인할 수 있습니다.

당신을 도울 또 다른 것은 음식을 쫓는 생물을 음식에 알리는 것입니다. 그러면 방금 먹은 음식을 쫓는 모든 생물을위한 음식을 찾기위한 코드를 실행할 수 있습니다.

실제로, 음식없이 시뮬레이션을 시작하면 어떤 생물체에도 주석이 달린 거리가 무한대가됩니다. 그런 다음 음식을 추가하십시오. 생물이 움직일 때 거리를 업데이트하십시오. 음식을 먹을 때, 그것을 쫓는 생물의 목록을 취한 다음 새로운 목표를 찾으십시오. 이 경우 외에 식품을 추가하면 다른 모든 업데이트가 처리됩니다.


시뮬레이션 건너 뛰기

생물의 속도를 알면 목표에 도달 할 때까지 얼마인지 알 수 있습니다. 모든 생물의 속도가 같으면 가장 먼저 도달하는 것은 주석이 달린 거리가 가장 작은 것입니다.

더 많은 음식을 더할 때까지 시간을 알고 있다면 ... 그리고 아마도 생식과 사망에 대한 비슷한 예측 가능성이 있다면 , 다음 사건의 시간을 알 수 있습니다 (추가 음식 또는 생물체 섭취).

그 순간으로 넘어가십시오. 움직이는 생물을 시뮬레이션 할 필요는 없습니다.


1
"그리고 거기에서만 검색하십시오." 그리고 세포는 바로 인접한 9 개의 세포를 의미합니다. 왜 9? 생물이 세포의 구석에 있다면 어떨까요?
UKMonkey

1
@UKMonkey "셀이 충돌하고 싶은 원의 반지름을 셀에 넣으십시오."만약 셀면이 반지름이고 생물체가 모퉁이에 있다면 ... 네 경우에만 4 개만 검색하면된다고 가정합니다. 그러나 우리는 세포를 더 작게 만들 수 있습니다. 음식이 너무 많고 생물이 너무 적 으면 유용 할 수 있습니다. 편집 : 명확히하겠습니다.
치료

2
물론-여분의 세포를 검색 해야하는 경우 운동하고 싶다면 ...하지만 대부분의 세포에는 음식이 없습니다 (주어진 이미지에서). 4 개의 셀을 검색하는 것보다 9 개의 셀을 검색하는 것이 더 빠릅니다.
UKMonkey

@UKMonkey이므로 처음에는 언급하지 않았습니다.
치료

16

BVH 와 같은 공간 분할 알고리즘을 채택해야합니다복잡성을 줄이려면 . 특정 상황에 맞게 음식 조각이 포함 된 축 정렬 경계 상자로 구성된 트리를 만들어야합니다.

계층 구조를 만들려면 AABB에서 음식 조각을 서로 가깝게 배치 한 다음 AABB를 더 큰 AABB에 다시 배치하십시오. 루트 노드가 될 때까지이 작업을 수행하십시오.

트리를 사용하려면 먼저 루트 노드에 대해 원형 AABB 교차 테스트를 수행 한 다음 충돌이 발생하면 각 연속 노드의 하위에 대해 테스트하십시오. 결국 음식 그룹이 있어야합니다.

AABB.cc 라이브러리를 사용할 수도 있습니다.


1
그것은 실제로 N log N으로의 복잡성을 감소시킬 것이지만, 분할을 수행하는 데 비용이 많이 든다. 생물이 모든 진드기를 움직이므로 모든 진드기를 분할해야 할 때 여전히 가치가 있습니까? 파티션을 덜 자주 만드는 솔루션이 있습니까?
Alexandre Rodrigues

3
@AlexandreRodrigues 당신은 모든 틱마다 전체 트리를 재구성 할 필요가 없으며 움직이는 부분 만 업데이트하고 특정 AABB 컨테이너 외부로 나가는 경우에만 업데이트합니다. 성능을 더욱 향상시키기 위해 노드 사이에 약간의 공간을 남겨두고 노드를 구성하여 리프 업데이트시 전체 분기를 다시 만들 필요가 없습니다.
오셀롯

6
BVH는 여기서 너무 복잡하다고 생각합니다. 해시 테이블로 구현 된 균일 한 그리드로 충분합니다.
스티븐

1
@Steven BVH를 구현함으로써 향후 시뮬레이션 규모를 쉽게 확장 할 수 있습니다. 그리고 소규모 시뮬레이션을 위해서라면 아무것도 풀지 않습니다.
오셀롯

2

실제로 설명 된 공간 분할 방법은 주요 문제가 조회가 아니라 시간을 단축시킬 수 있습니다. 검색 속도가 너무 커서 작업 속도가 느려집니다. 따라서 내부 루프를 최적화하지만 외부 루프를 최적화 할 수도 있습니다.

문제는 계속 폴링 데이터입니다. 뒷좌석에 아이들이 "아직 거기 있나?"라고 천 번 물어 보는 것과 비슷합니다. 운전자가 거기에있을 때 알려줄 필요는 없습니다.

대신 가능한 경우 각 작업을 완료하여 대기열에 넣고 해당 버블 이벤트를 내보내도록 노력해야합니다. 이로 인해 대기열이 변경 될 수는 있지만 괜찮습니다. 이를 불연속 이벤트 시뮬레이션이라고합니다. 이 방법으로 시뮬레이션을 구현할 수 있다면 더 나은 공간 파티션 조회에서 얻을 수있는 속도 향상보다 훨씬 큰 상당한 속도 향상을 찾고 있습니다.

이전 경력의 요점을 강조하기 위해 공장 시뮬레이터를 만들었습니다. 우리는이 방법으로 한 시간 안에 몇 주 동안 큰 공장 / 공항 전체 자재 흐름을 각 품목 수준에서 시뮬레이션했습니다. 타임 스텝 기반 시뮬레이션은 실시간보다 4-5 배 더 빠르게 시뮬레이션 할 수 있습니다.

또한 실제로 매달린 과일은 시뮬레이션에서 드로잉 루틴을 분리하는 것을 고려하십시오. 시뮬레이션은 간단하지만 그림을 그리는 데 약간의 오버 헤드가 있습니다. 디스플레이 드라이버가 초당 x 업데이트로 제한하는 반면 실제로 프로세서는 100 배 더 빠른 작업을 수행 할 수 있습니다. 이것은 프로파일 링의 필요성을 강조합니다.


@Theraot 우리는 그림이 어떻게 구성되어 있는지 모릅니다. 그러나 그래도 무승부가 빠르면 무승부는 병목이 될 것입니다.
joojaa

1

스위프 라인 알고리즘을 사용하여 복잡성을 Nlog (N)로 줄일 수 있습니다. Voronoi 다이어그램의 이론은 생물을 둘러싼 영역을 다른 생물보다 그 생물에 더 가까운 모든 지점으로 구성된 영역으로 분할하는 것입니다.

소위 Fortune의 알고리즘은 Nlog (N)에서이를 수행하며, 위키 페이지에는이를 구현하기위한 의사 코드가 포함되어 있습니다. 나는 거기에도 라이브러리 구현이 있다고 확신합니다. https://en.wikipedia.org/wiki/Fortune%27s_algorithm


GDSE에 오신 것을 환영합니다. 답변 해 주셔서 감사합니다. 이를 OP의 상황에 정확히 어떻게 적용 하시겠습니까? 문제 설명에 따르면 실체는 시야 거리 내에있는 모든 음식을 고려해야하고 가장 좋은 음식을 선택해야합니다. 전통적인 보로 노이는 다른 개체에 더 가까운 범위의 음식을 배제 할 것입니다. Voronoi가 작동하지 않는다는 말은 아니지만 OP가 설명 된대로 문제를 어떻게 사용 해야하는지는 명확하지 않습니다.
Pikalek

이 아이디어가 마음에 들어 확장 된 것을보고 싶습니다. 메모리 데이터 구조에서와 같이 보로 노이 다이어그램을 어떻게 표현합니까? 어떻게 쿼리합니까?
치료

@ Theraot 당신은 같은 스위프 라인 아이디어를 tge voronoi 다이어그램이 필요하지 않습니다.
joojaa

-2

가장 쉬운 해결책은 물리 엔진을 통합하고 충돌 감지 알고리즘 만 사용하는 것입니다. 각 엔터티 주위에 원 / 구를 만들고 물리 엔진이 충돌을 계산하도록합니다. 2D의 경우 Box2D 또는 ChipmunkBullet for 3D를 제안 합니다.

전체 물리 엔진을 통합하는 것이 너무 많다고 생각되면 특정 충돌 알고리즘을 살펴 보는 것이 좋습니다. 대부분의 충돌 감지 라이브러리는 두 단계로 작동합니다.

  • 광범위한 위상 탐지 :이 단계의 목표는 가능한 한 빨리 충돌 할 수있는 후보 객체 쌍 목록을 얻는 것입니다. 두 가지 일반적인 옵션은 다음과 같습니다.
    • 청소 및 정리 정리 : X 축을 따라 경계 상자를 정렬하고 교차하는 객체 쌍을 표시합니다. 다른 축마다 반복하십시오. 응시자 쌍이 모든 테스트를 통과하면 다음 단계로 넘어갑니다. 이 알고리즘은 시간적 일관성을 활용하는 데 매우 유용합니다. 정렬 된 엔티티 목록을 유지하고 매 프레임마다 업데이트 할 수 있지만 거의 정렬되어 있으면 매우 빠릅니다. 또한 공간 일관성을 활용합니다. 엔티티는 공간 오름차순으로 정렬되기 때문에 충돌을 검사 할 때 엔티티가 충돌하지 않는 즉시 중지 할 수 있습니다. 다음 항목은 모두 멀리 있기 때문입니다.
    • 쿼드 트리, 옥트리 및 그리드와 같은 공간 분할 데이터 구조 그리드는 구현하기 쉽지만 엔터티 밀도가 낮 으면 무한한 공간에 구현하기가 매우 어려울 수 있습니다. 정적 공간 트리도 구현하기 쉽지만 균형을 잡거나 업데이트하기가 어렵 기 때문에 각 프레임을 다시 작성해야합니다.
  • 좁은 단계 : 넓은 단계에서 발견 된 후보 쌍은 더 정확한 알고리즘으로 추가 테스트됩니다.
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.