답은 항상 배열이나 std :: vector를 사용하는 것입니다. 링크 된 목록 또는 std :: map과 같은 유형은 일반적으로 게임에서 절대적으로 끔찍 하며 게임 개체 모음과 같은 경우가 포함됩니다.
배열 / 벡터에 객체 자체 (포인터가 아닌)를 저장해야합니다.
당신은 원하는 인접 메모리를. 당신은 정말로 그것을 정말로 원합니다. 비 연속 메모리의 모든 데이터를 반복하면 일반적으로 많은 캐시 미스가 발생하고 컴파일러와 CPU가 효과적인 캐시 프리 페치를 수행하는 기능이 제거됩니다. 이것만으로도 성능이 저하 될 수 있습니다.
또한 메모리 할당 및 할당 해제를 피하려고합니다. 메모리 할당 기가 빠르더라도 매우 느립니다. 게임마다 프레임 당 수백 개의 메모리 할당을 제거하여 10x FPS 충돌을 경험하는 것을 보았습니다. 그렇게 나쁘게 보이지는 않지만 그렇게 될 수 있습니다.
마지막으로, 게임 오브젝트 관리에 관심이있는 대부분의 데이터 구조는 트리 또는 목록을 사용하는 것보다 배열 또는 벡터에서 훨씬 효율적으로 구현할 수 있습니다.
예를 들어 게임 오브젝트를 제거하기 위해 swap-and-pop을 사용할 수 있습니다. 다음과 같이 쉽게 구현할 수 있습니다.
std::swap(objects[index], objects.back());
objects.pop_back();
다음에 새 객체를 만들어야 할 때 객체를 삭제 된 것으로 표시하고 색인을 빈 목록에 추가 할 수도 있지만 스왑 앤 팝을 수행하는 것이 좋습니다. 루프 자체를 제외하고 분기가없는 모든 라이브 객체에 대해 간단한 for 루프를 수행 할 수 있습니다. 불릿 물리 통합 등의 경우 성능이 크게 향상 될 수 있습니다.
더 중요한 것은 안정적인 고유 한 단일 테이블 조회 쌍이있는 객체를 슬롯 맵 구조를 사용하여 찾을 수 있다는 것입니다.
게임 오브젝트는 메인 배열에 인덱스가 있습니다. 이 인덱스만으로도 매우 효율적으로 조회 할 수 있습니다 (맵 또는 해시 테이블보다 훨씬 빠름). 그러나 개체를 제거 할 때 스왑 및 팝으로 인해 인덱스가 안정적이지 않습니다.
슬롯 맵에는 두 개의 간접 레이어가 필요하지만 둘 다 인덱스가 일정한 간단한 배열 조회입니다. 그들은 빠르다 . 정말 빠릅니다.
기본 아이디어는 기본 객체 목록, 간접 목록 및 간접 목록에 대한 빈 목록의 세 가지 배열을 갖는 것입니다. 기본 개체 목록에는 실제 개체가 포함되어 있으며 각 개체는 고유 한 ID를 알고 있습니다. 고유 ID는 색인과 버전 태그로 구성됩니다. 간접 목록은 단순히 주 개체 목록에 대한 인덱스 배열입니다. 자유리스트는 간접리스트에 대한 인덱스 스택입니다.
기본 목록에서 개체를 만들면 빈 목록을 사용하여 간접 목록에서 사용되지 않는 항목을 찾습니다. 간접 목록의 항목은 기본 목록에서 사용되지 않은 항목을 가리 킵니다. 해당 위치에서 객체를 초기화하고 고유 한 ID를 선택한 간접 목록 항목의 색인 및 기본 목록 요소의 기존 버전 태그에 1을 더한 값으로 설정합니다.
객체를 파괴하면 정상적으로 스왑 앤 팝을 수행하지만 버전 번호도 증가시킵니다. 그런 다음 간접 목록 색인 (객체 고유 ID의 일부)을 사용 가능 목록에 추가하십시오. 스왑 앤 팝의 일부로 객체를 이동할 때 간접 목록의 항목도 새 위치로 업데이트합니다.
의사 코드 예 :
Object:
int index
int version
other data
SlotMap:
Object objects[]
int slots[]
int freelist[]
int count
Get(id):
index = indirection[id.index]
if objects[index].version = id.version:
return &objects[index]
else:
return null
CreateObject():
index = freelist.pop()
objects[count].index = id
objects[count].version += 1
indirection[index] = count
Object* object = &objects[count].object
object.initialize()
count += 1
return object
Remove(id):
index = indirection[id.index]
if objects[index].version = id.version:
objects[index].version += 1
objects[count - 1].version += 1
swap(objects[index].data, objects[count - 1].data)
간접 계층을 사용하면 압축하는 동안 이동할 수있는 리소스 (주 개체 목록)에 대해 안정적인 식별자 (항목이 이동하지 않는 간접 계층의 인덱스)를 가질 수 있습니다.
버전 태그를 사용하면 삭제 될 수있는 객체에 ID를 저장할 수 있습니다. 예를 들어, ID는 (10,1)입니다. 인덱스가 10 인 오브젝트가 삭제됩니다 (예 : 글 머리 기호가 오브젝트에 부딪쳐서 파괴됨). 주 객체 목록에서 해당 메모리 위치에있는 객체의 버전 번호가 충돌하여 (10,2)가됩니다. 오래된 ID에서 (10,1)을 다시 찾으려면 색인 10을 통해 해당 객체를 반환하지만 버전 번호가 변경된 것을 볼 수 있으므로 ID가 더 이상 유효하지 않습니다.
이것은 데이터가 메모리에서 움직일 수있게하는 안정적인 ID로 가질 수있는 가장 빠른 데이터 구조입니다. 이는 데이터 위치 및 캐시 일관성에 중요합니다. 이것은 가능한 해시 테이블 구현보다 빠릅니다. 해시 테이블은 해시 (테이블 조회보다 많은 명령)를 계산해야하며 해시 체인을 따라야합니다 (std :: unordered_map의 끔찍한 경우의 링크 된 목록 또는 열린 주소 목록) 멍청하지 않은 해시 테이블을 구현 한 다음 각 키에 대해 값을 비교해야합니다 (버전 태그 확인보다 비싸지는 않지만 저렴합니다). 매우 좋은 해시 테이블 (STL 구현시에는 해당되지 않습니다. STL은 게임 객체 목록에 대해 게임과 다른 사용 사례에 대해 최적화하는 해시 테이블을 요구하므로) 하나의 간접적 인 저장에 저장 될 수 있습니다.
기본 알고리즘을 다양하게 개선 할 수 있습니다. 예를 들어 기본 객체 목록에 std :: deque와 같은 것을 사용하는 경우; 하나의 추가 간접 레이어이지만 슬롯 맵에서 얻은 임시 포인터를 무효화하지 않고 객체를 전체 목록에 삽입 할 수 있습니다.
객체의 메모리 주소 (this-objects)에서 색인을 계산할 수 있기 때문에 객체 내부에 색인을 저장하지 않아도 될 수 있으며 객체를 제거 할 때만 필요합니다. index)를 매개 변수로 사용하십시오.
쓰기에 대한 사과; 나는 그것이 가장 분명한 설명이라고 생각하지 않습니다. 늦었고 코드 샘플보다 더 많은 시간을 소비하지 않고 설명하기가 어렵습니다.