동일한 구성 요소 세트의 엔티티를 선형 메모리로 그룹화


12

기본 시스템 구성 요소 엔터티 접근 방식 부터 시작합니다 .

구성 요소 유형에 대한 정보만으로 어셈블리 ( 기사 에서 파생 된 용어)를 작성해 봅시다 . 구성 요소를 하나씩 요소에 하나씩 추가 / 제거하는 것처럼 런타임에 동적으로 수행되지만 형식 정보에 관한 것이므로 더 정확하게 이름을 지정합시다.

그런 다음 모든 어셈블리 를 지정하는 엔터티 를 구성 합니다. 엔터티를 만든 후에는 해당 어셈블리를 변경할 수 없습니다. 즉, 직접 수정할 수는 없지만 기존 엔터티의 서명을 로컬 복사본 (콘텐츠와 함께)으로 가져 와서 적절히 변경하고 새 엔터티를 만들 수 있습니다. 그것의.

이제 핵심 개념 : 엔터티가 생성 될 때마다 어셈블리 버킷 이라는 객체에 할당됩니다. 이는 동일한 서명 의 모든 엔터티가 동일한 컨테이너에 있음을 의미합니다 (예 : std :: vector).

이제 시스템 은 관심있는 모든 버킷을 반복하고 작업을 수행합니다.

이 방법에는 몇 가지 장점이 있습니다.

  • 구성 요소는 몇 개의 (정확하게 : 버킷 수) 연속적인 메모리 청크에 저장됩니다. 이는 메모리 친 화성을 향상시키고 전체 게임 상태를 덤프하는 것이 더 쉽습니다.
  • 시스템은 선형 방식으로 구성 요소를 처리하므로 캐시 일관성이 향상됩니다-사전 및 임의 메모리 점프
  • 새로운 엔티티를 생성하는 것은 어셈블리를 버킷에 매핑하고 필요한 컴포넌트를 벡터로 푸시하는 것만 큼 쉽습니다.
  • 순서는 중요하지 않기 때문에 엔티티를 삭제하는 것은 std :: move를 호출하여 마지막 요소를 삭제 된 요소로 바꾸는 것만 큼 쉽습니다.

여기에 이미지 설명을 입력하십시오

완전히 다른 서명을 가진 엔터티가 많으면 캐시 일관성의 이점이 줄어들지 만 대부분의 응용 프로그램에서 발생할 것이라고 생각하지 않습니다.

벡터가 재 할당되면 포인터 무효화에 문제가 있습니다. 이는 다음과 같은 구조를 도입하여 해결할 수 있습니다.

struct assemblage_bucket {
    struct entity_watcher {
        assemblage_bucket* owner;
        entity_id real_index_in_vector;
    };

    std::unordered_map<entity_id, std::vector<entity_watcher*>> subscribers;

    //...
};

따라서 게임 로직에서 어떤 이유로 든 새로 생성 된 엔티티를 추적하고, 버킷 내에서 entity_watcher를 등록하고 , 엔티티를 제거하는 동안 std :: move'd를 지정하면 감시자를 찾아 업데이트합니다. 그들의 real_index_in_vector새로운 가치로. 대부분의 경우 이것은 모든 엔티티 삭제에 대해 하나의 사전 검색 만 수행합니다.

이 접근법에 더 이상 단점이 있습니까?

꽤 명백하지만 솔루션이 언급되지 않은 이유는 무엇입니까?

편집 : 의견이 충분하지 않기 때문에 "답변에 답하기"위해 질문을 편집하고 있습니다.

정적 클래스 구성에서 벗어나기 위해 특별히 작성된 플러그 가능 구성 요소의 동적 특성을 잃게됩니다.

난 아니야 어쩌면 나는 충분히 명확하게 설명하지 않았을 수도 있습니다.

auto signature = world.get_signature(entity_id); // this would just return entity_id.bucket_owner->bucket_signature or so
signature.add(foo_component);
signature.remove(bar_component);
world.delete_entity(entity_id); // entity_id would hold information about its bucket owner
world.create_entity(signature); // automatically assigns new entity to an existing or a new bucket

기존 엔터티의 서명을 가져 와서 수정하고 새 엔터티로 다시 업로드하는 것만 큼 간단합니다. 플러그, 동적 특성 ? 물론이야. 여기서는 하나의 "조립"과 하나의 "버킷"클래스 만 있음 을 강조하고 싶습니다 . 버킷은 데이터 중심이며 런타임시 최적의 수량으로 생성됩니다.

유효한 대상을 포함 할 수있는 모든 버킷을 거쳐야합니다. 외부 데이터 구조가 없으면 충돌 감지도 마찬가지로 어려울 수 있습니다.

이것이 우리가 앞서 언급 한 외부 데이터 구조를 갖는 이유 입니다. 해결 방법은 다음 버킷으로 이동할 시점을 감지하는 반복자를 System 클래스에 도입하는 것만 큼 간단합니다. 점프는 논리 순수 투명하다.


또한 모든 구성 요소를 벡터에 저장하는 방법에 대한 Randy Gaul 기사를 읽고 시스템에서 처리하도록합니다. 거기에는 두 가지 큰 문제가 있습니다. 엔티티의 하위 세트 만 업데이트하려는 경우 (예 : 컬링 생각) 그 구성 요소 때문에 엔터티와 다시 연결됩니다. 각 구성 요소 반복 단계마다 속한 엔티티가 업데이트를 위해 선택되었는지 확인해야합니다. 다른 문제는 일부 시스템은 캐시 일관성을 다시 활용하여 여러 가지 다른 구성 요소 유형을 처리해야한다는 것입니다. 이러한 문제를 해결하는 방법에 대한 아이디어가 있습니까?
tiguchi

답변:


7

풀 할당 자와 동적 클래스를 사용하여 정적 객체 시스템을 설계했습니다.

학교 시절에 "조립"시스템과 거의 동일하게 작동하는 객체 시스템을 작성했지만 항상 내 설계에서 "조립"을 "청사진"또는 "아키 타입"이라고 부르는 경향이 있습니다. 아키텍쳐는 순진한 객체 시스템보다 엉덩이에서 더 고통 스러웠으며 비교할 때보 다 유연한 디자인에 비해 성능상의 이점이 없었습니다. 게임 편집기에서 작업 할 때 객체를 수정하거나 재 할당 할 필요없이 객체를 동적으로 수정하는 기능은 매우 중요합니다. 디자이너는 컴포넌트를 객체 정의로 끌어다 놓기를 원할 것입니다. 런타임 코드는 개인적으로 싫어하지만 일부 디자인에서는 구성 요소를 효율적으로 수정해야 할 수도 있습니다. 편집기에서 객체 참조를 연결하는 방법에 따라

사소하지 않은 대부분의 경우에 생각하는 것보다 캐시 일관성이 떨어집니다. 예를 들어 AI 시스템은 Render구성 요소를 신경 쓰지 않지만 각 엔터티의 일부로 구성 요소를 반복적으로 사용합니다. 반복되는 객체는 더 크고 캐시 라인 요청은 불필요한 데이터를 가져오고 각 요청으로 반환되는 전체 객체는 더 적습니다). 그것은 여전히 ​​순진한 방법보다 낫고, 순진한 방법 객체 구성은 큰 AAA 엔진에서도 사용되므로 아마도 더 나을 필요 는 없지만 적어도 더 이상 개선 할 수 없다고 생각하지 마십시오.

귀하의 접근 방식은 일부 에게 가장 적합합니다.구성 요소를 모두 포함하지는 않습니다. ECS는 각 구성 요소를 항상 별도의 컨테이너에 넣는 것을 옹호하므로 강력하게 싫어합니다. 물리적 또는 그래픽에 적합하거나 여러 스크립트 구성 요소 또는 컴포저 블 AI를 허용하는 경우 전혀 의미가 없습니다. 구성 요소 시스템을 내장 개체 이상으로 사용하고 디자이너와 게임 플레이 프로그래머가 개체 동작을 구성하는 방법으로 사용하는 경우 모든 AI 구성 요소 (종종 상호 작용하는) 또는 모든 스크립트를 함께 그룹화하는 것이 좋습니다. 구성 요소 (한 번에 모두 업데이트하기 때문에). 성능이 가장 우수한 시스템을 원하면 구성 요소 할당 및 스토리지 구성표가 혼합되어 있어야하며 각 특정 유형의 구성 요소에 가장 적합한 결정적인 결론을 내릴 시간이 필요합니다.


나는 말했다 : 우리는 엔티티의 서명을 변경할 수 없으며, 직접 수정 할 수는 없지만 로컬 사본에 대한 기존 조립을 가져 와서 변경하고 새로운 엔티티로 다시 업로드 할 수 있습니다. 내가 질문에서 보았 듯이 작업은 꽤 저렴합니다. 다시 한번-오직 하나의 "버킷"클래스가 있습니다. "어셈블리"/ "시그니처"/ "우리가 원하는 이름을 지정하자"는 표준 접근 방식과 같이 런타임에 동적으로 생성 될 수 있으며, 엔티티를 "시그니처"로 생각하는 한까지 진행할 수 있습니다.
Patryk Czachurski

그리고 나는 당신이 반드시 통일을 다루고 싶지는 않다고 말했습니다. "새 엔티티 작성"은 핸들 시스템 작동 방식에 따라 엔티티에 대한 기존의 모든 핸들을 분리하는 것을 의미 할 수 있습니다. 그들이 싼 경우 전화. 나는 엉덩이를 다루는 것이 고통이라는 것을 알았습니다.
Sean Middleditch

좋아, 이제 이것에 대한 당신의 요점을 얻었습니다. 어쨌든 나는 추가 / 제거가 조금 더 비싸더라도 너무나 간헐적으로 발생하여 실시간으로 발생하는 구성 요소에 액세스하는 프로세스를 크게 단순화 할 가치가 있다고 생각합니다. 따라서 "변경"의 오버 헤드는 무시할 수 있습니다. AI 예제에 대해서는 여러 구성 요소의 데이터가 필요한 몇 가지 시스템이 여전히 가치가 있습니까?
Patryk Czachurski

필자는 AI가 접근 방식이 더 나은 곳이지만 다른 구성 요소의 경우 반드시 그렇지는 않다고 지적했다.
Sean Middleditch

4

당신이 한 일은 리엔지니어링 된 C ++ 객체입니다. 이것이 명백하다고 느끼는 이유는 "entity"라는 단어를 "class"로 바꾸고 "component"를 "member"로 바꾸면 mixin을 사용하는 표준 OOP 디자인이기 때문입니다.

1) 정적 클래스 구성에서 벗어나기 위해 특별히 작성된 플러그 가능한 구성 요소의 동적 특성을 잃습니다.

2) 메모리 일관성은 한 장소에서 여러 데이터 유형을 통합하는 객체가 아닌 데이터 유형 내에서 가장 중요합니다. 이것은 class + object 메모리 조각화로부터 벗어나기 위해 component + systems가 작성된 이유 중 하나입니다.

3) 구성 요소 + 시스템 설계에서 엔티티가 내부 작업을 이해하기 쉽도록 태그 / ID 일 때 엔티티를 일관된 객체로 생각하기 때문에이 디자인은 C ++ 클래스 스타일로 돌아갑니다.

4) 프로그래머로서 추적하기가 쉽지 않은 경우, 복잡한 객체보다 컴포넌트가 자체적으로 직렬화하는 것이 쉽다.

5)이 경로의 다음 단계는 시스템을 제거하고 해당 코드를 엔터티에 직접 입력하는 것입니다. 우리는 그것이 무엇을 의미하는지 볼 수 있습니다 =)


2) 캐싱을 완전히 이해하지 못하지만 10 가지 구성 요소와 작동하는 시스템이 있다고 가정 해 봅시다. 표준 접근법에서 각 엔티티를 처리한다는 것은 풀을 사용하더라도 구성 요소가 메모리의 임의 위치에 흩어져 있기 때문에 RAM에 10 번 액세스하는 것을 의미합니다. 다른 구성 요소는 다른 풀에 속하기 때문입니다. 한 번에 전체 엔터티를 캐시하고 사전 검색을하지 않고도 단일 캐시 누락없이 모든 구성 요소를 처리하는 것이 "중요하지"않습니까? 또한, 나는 1) 포인트
Patryk Czachurski를

@Sean Middleditch는 그의 캐싱 분석에 대한 그의 설명에서 좋은 설명을 가지고 있습니다.
패트릭 휴즈

3) 어떤 식 으로든 일관된 대상이 아닙니다. John이 지적했듯이 구성 요소 A가 메모리의 구성 요소 B 바로 옆에있는 것은 "논리적 일관성"이 아니라 "메모리 일관성"입니다. 버킷은 생성시 원하는 순서대로 구성 요소를 섞을 수 있으며 원칙은 여전히 ​​유지됩니다. 4) 추상화가 충분하다면 "추적"을 유지하는 것이 똑같이 쉬울 수 있습니다. 우리가 이야기하는 것은 단순히 반복자와 함께 제공되는 스토리지 체계 일 뿐이며 아마도 바이트 오프셋 맵은 표준 접근 방식처럼 처리를 쉽게 할 수 있습니다.
Patryk Czachurski

5) 그리고 나는이 아이디어에서 아무것도이 방향을 가리키고 있다고 생각하지 않습니다. 나는 당신에게 동의하고 싶지 않다. 나는이 토론이 어디에서 일어날 지 궁금하다. 어쨌든 그것이 "측정"또는 잘 알려진 "조기 최적화"로 이어질 것이다. :)
Patryk Czachurski

@PatrykCzachurski 그러나 귀하의 시스템은 10 개의 구성 요소와 작동하지 않습니다.
user253751

3

엔티티처럼 같이 유지하는 것은 생각만큼 중요하지 않으므로 "단위이기 때문에"이외의 유효한 이유를 생각하기가 어렵습니다. 그러나 논리적 일관성과 반대로 캐시 일관성을 위해 실제로이 작업을 수행하고 있으므로 의미가 있습니다.

한 가지 어려움은 다른 버킷의 구성 요소 간 상호 작용입니다. 예를 들어 AI가 쏠 수 있는 무언가 를 찾는 것이 굉장히 간단하지는 않습니다 . 유효한 대상을 포함 할 수있는 모든 버킷을 통과해야합니다. 외부 데이터 구조가 없으면 충돌 감지도 마찬가지로 어려울 수 있습니다.

논리적 일관성을 위해 엔터티를 함께 구성하려면 엔터티를 함께 유지해야하는 유일한 이유는 내 임무에서 식별 목적을위한 것입니다. 엔터티 유형 A 또는 유형 B를 방금 만들 었는지 알아야 하며이 문제를 해결합니다.이 엔터티를 구성하는 어셈블리를 식별하는 새 구성 요소를 추가합니다. 그럼에도 불구하고, 나는 위대한 작업을 위해 모든 구성 요소를 모으지 않고 단지 그것이 무엇인지 알아야합니다. 그래서 나는이 부분이별로 유용하지 않다고 생각합니다.


나는 당신의 대답을 잘 이해하지 못한다는 것을 인정해야합니다. "논리적 일관성"이란 무엇입니까? 상호 작용의 어려움에 대해 편집했습니다.
Patryk Czachurski

"논리적 일관성": 트리 엔티티를 구성하는 모든 구성 요소를 서로 가깝게 유지하는 것이 "논리적 의미"입니다.
존 맥도날드
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.