C ++에서 엔티티 구성 요소 시스템 간 링크에 대한 조언


10

엔터티 구성 요소 시스템에 대한 몇 가지 설명서를 읽은 후 광산을 구현하기로 결정했습니다. 지금까지 엔티티와 시스템 관리자 (시스템)를 포함하는 World 클래스, std :: map으로 구성 요소를 포함하는 Entity 클래스 및 몇 가지 시스템이 있습니다. World에서 엔티티를 std :: vector로 보유하고 있습니다. 지금까지 아무런 문제가 없습니다. 나를 혼란스럽게하는 것은 엔티티의 반복이며, 그것에 대해 명확한 마음을 가질 수 없으므로 여전히 그 부분을 구현할 수 없습니다. 모든 시스템에 관심있는 엔티티 목록이 있어야합니까? 아니면 World 클래스의 엔터티를 반복하고 시스템을 반복하는 중첩 루프를 만들고 엔터티에 시스템이 관심있는 구성 요소가 있는지 확인해야합니까? 내말은 :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

그러나 비트 마스크 시스템은 스크립팅 언어를 포함시키는 경우 약간의 유연성을 제공한다고 생각합니다. 또는 각 시스템에 대한 로컬 목록이 있으면 클래스의 메모리 사용량이 증가합니다. 정말 혼란 스러워요.


스크립트 바인딩을 방해하는 비트 마스크 접근 방식이 필요한 이유는 무엇입니까? 따로, 엔터티 및 시스템을 복사하지 않도록 for-each 루프에서 참조 (가능한 경우 고정)를 사용하십시오.
Benjamin Kloster

예를 들어 int와 같은 비트 마스크를 사용하면 32 개의 다른 구성 요소 만 보유합니다. 나는 32 개 이상의 구성 요소가 있음을 의미하지는 않지만 내가 가지고 있다면 어떻게해야합니까? 다른 int 또는 64bit int를 만들어야하며 동적이지 않습니다.
deniz

런타임 동적으로 하려는지 여부에 따라 std :: bitset 또는 std :: vector <bool>을 사용할 수 있습니다.
Benjamin Kloster

답변:


7

각 시스템에 대한 로컬 목록이 있으면 클래스의 메모리 사용량이 증가합니다.

전통적인 시공간 트레이드 오프 입니다.

모든 엔티티를 반복하고 서명을 확인하는 것은 코드에 대한 직설적이지만 시스템 수가 증가함에 따라 비효율적 일 수 있습니다. 관련없는 수천 개의 엔티티 중 단일 관심 엔티티를 찾는 특수 시스템 (입력 가능)을 상상하십시오. .

그러나이 방법은 목표에 따라 여전히 충분할 수 있습니다.

속도가 걱정된다면 고려해야 할 다른 솔루션이 있습니다.

모든 시스템에 관심이있는 로컬 엔티티 목록이 있어야합니까?

바로 그거죠. 이것은 적절한 성능을 제공하고 구현하기가 쉬운 표준 접근 방식입니다. 내 생각에 메모리 오버 헤드는 무시할 만하다. 포인터 저장에 대해 이야기하고있다.

이제 이러한 "관심 목록"을 유지 관리하는 방법이 그렇게 명확하지 않을 수 있습니다. 데이터 컨테이너의 경우 std::vector<entity*> targets내부 시스템 클래스로 충분합니다. 이제 내가하는 일은 이것입니다 :

  • 생성시 엔티티가 비어 있으며 시스템에 속하지 않습니다.
  • 엔터티에 구성 요소를 추가 할 때마다 :

    • 현재 비트 서명을 얻습니다 .
    • 구성 요소의 크기를 적절한 청크 크기의 세계 풀에 매핑하고 (개인적으로 boost :: pool을 사용) 구성 요소를 거기에 할당하십시오
    • 엔터티의 새로운 비트 서명 ( "현재 비트 서명"과 새로운 구성 요소)
    • 전 세계의 시스템을 반복 처리하고 그 서명 시스템이 있다면 하지 않는 기업의 현재의 서명과 일치하는 않는 새로운 서명과 일치, 그것은 우리가 우리의 실체에 포인터를와 push_back한다 명백한된다.

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);

엔티티를 제거하는 것은 시스템이 현재 서명과 일치하고 (엔티티가 존재했음을 의미하는) 새 서명과 일치하지 않는 경우 (엔티티가 더 이상 존재하지 않아야 함을 의미 함) 차이점과 완전히 유사합니다. ).

벡터에서 제거하는 것이 O (n)이기 때문에 std :: list의 사용을 고려할 수 있습니다. 중간에서 제거 할 때마다 큰 데이터 덩어리를 이동해야한다는 언급은 없습니다. 실제로, 당신은 그럴 필요가 없습니다. 우리는이 레벨에서 순서를 처리하는 것에 신경 쓰지 않기 때문에 std :: remove를 호출 할 수 있습니다. 모든 삭제에서 우리는 오직 제거 될 엔티티.

std :: list는 O (1) 제거를 제공하지만 다른쪽에는 약간의 추가 메모리 오버 헤드가 있습니다. 또한 엔터티를 처리하고 제거하지 않는 대부분의 시간을 기억하십시오. 이는 std :: vector를 사용하여 더 빠르게 수행됩니다.

성능이 매우 중요한 경우 다른 데이터 액세스 패턴도 고려할 수 있지만 어떤 방식 으로든 "관심 목록"을 유지 관리 할 수 ​​있습니다. Entity System API를 충분히 추상화하면 프레임 레이트 때문에 프레임 레이트가 떨어지는 경우 시스템의 엔티티 처리 방법을 개선하는 데 문제가되지 않으므로 지금은 코딩하기 가장 쉬운 방법을 선택하십시오. 그런 다음 필요한 경우 프로파일 링하고 개선하십시오.


5

각 시스템이 자신과 관련된 구성 요소를 소유하고 엔터티가 해당 구성 요소 만 참조하는 위치를 고려할 가치가있는 접근 방식이 있습니다. 기본적으로 (간체 화 된) Entity클래스는 다음과 같습니다.

class Entity {
  std::map<ComponentType, Component*> components;
};

RigidBody연결된 구성 요소를 말하면 시스템 Entity에서 요청합니다 Physics. 시스템은 구성 요소를 작성하고 엔티티가 해당 구성 요소에 대한 포인터를 유지할 수있게합니다. 그런 다음 시스템은 다음과 같습니다.

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

이제는 처음에는 약간 반 직관적으로 보일 수 있지만 이점은 구성 요소 엔터티 시스템이 상태를 업데이트하는 방식에 있습니다. 종종 시스템을 반복하여 관련 구성 요소 업데이트를 요청합니다.

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

시스템이 소유 한 모든 구성 요소를 연속 메모리에 보유하는 장점은 시스템이 모든 구성 요소를 반복하고 갱신 할 때 기본적으로 수행해야한다는 것입니다

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

잠재적으로 업데이트해야 할 구성 요소가없는 모든 엔티티를 반복 할 필요가 없으며 구성 요소가 모두 연속적으로 저장되므로 캐시 성능이 매우 우수 할 수도 있습니다 . 이것이이 방법의 가장 큰 장점은 아니지만 하나입니다. 당신은 종종 주어진 시간에 수십만 개의 구성 요소를 가지게되며 가능한 한 성능을 발휘하려고 노력할 것입니다.

이 시점에서 사용자 World는 시스템 update을 반복하고 엔티티를 반복하지 않고도 시스템을 호출 합니다. 시스템의 책임이 훨씬 명확하기 때문에 더 나은 디자인입니다.

물론, 그러한 디자인에는 무수한 디자인이 있으므로 게임의 요구를 신중하게 평가하고 가장 적합한 것을 선택해야하지만 여기에서 볼 수 있듯이 약간의 디자인 세부 사항이 때로는 차이를 만들 수 있습니다.


좋은 답변, 감사합니다. 그러나 구성 요소에는 기능 (예 : update ())이 없으며 데이터 만 있습니다. 시스템은 그 데이터를 처리합니다. 따라서 귀하의 예에 따르면 구성 요소 클래스에 대한 가상 업데이트와 각 구성 요소에 대한 엔티티 포인터를 추가해야합니다.
deniz

@deniz 그것은 모두 당신의 디자인에 달려 있습니다. 컴포넌트에 메소드가없고 데이터 만있는 경우에도 시스템은 해당 메소드를 반복하여 필요한 조치를 수행 할 수 있습니다. 엔티티에 다시 연결하는 경우, 예를 들어 컴포넌트 자체에 소유자 엔티티에 대한 포인터를 저장하거나 시스템이 컴포넌트 핸들과 엔티티 사이의 맵을 유지하도록 할 수 있습니다. 그러나 일반적으로 구성 요소를 최대한 독립적으로 유지하려고합니다. 부모 엔터티에 대해 전혀 모르는 구성 요소가 이상적입니다. 그러한 방향으로 의사 소통이 필요한 경우 이벤트 등을 선호하십시오.
pwny

효율성이 더 좋다고 말하면 패턴을 사용합니다.
deniz

@deniz 실제로 코드를 프로파일 링하고 종종 특정 engin에 효과가있는 것과 그렇지 않은 것을 식별하도록하십시오 :)
pwny

좋아 :) 나는 스트레스 테스트를 할 것이다
deniz

1

제 생각에는 좋은 아키텍처는 엔터티에서 구성 요소 계층을 만들고이 구성 요소 계층에서 각 시스템의 관리를 분리하는 것입니다. 예를 들어, 논리 시스템에는 해당 엔티티에 영향을주는 일부 논리 구성 요소가 있으며 엔티티의 모든 구성 요소에 공유되는 공통 속성을 저장합니다.

그런 다음 각 시스템의 개체를 다른 지점이나 특정 순서로 관리하려면 각 시스템에서 활성 구성 요소 목록을 만드는 것이 좋습니다. 시스템에서 작성 및 관리 할 수있는 모든 포인터 목록은로드 된 자원이 하나 미만입니다.

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