죽었을 때 게임 루프에서 엔티티를 가장 잘 제거하는 방법은 무엇입니까?


16

좋아, 반복하고 업데이트하는 모든 엔티티의 큰 목록이 있습니다. AS3에서는 이것을 Array (dynamic length, untyped), Vector (typed) 또는 연결된 목록 (기본이 아님)으로 저장할 수 있습니다. 현재 배열을 사용하고 있지만 더 빠르면 벡터 또는 링크 된 목록으로 변경할 계획입니다.

어쨌든 내 질문은 엔티티가 파괴되면 어떻게 목록에서 제거해야합니까? 나는 그 위치를 무효로하거나, 그것을 쪼개거나, "나를 뛰어 넘어 죽었다." 엔티티를 풀링하고 있기 때문에 죽은 엔티티는 어느 시점에서 다시 살아날 가능성이 높습니다. 각 수집 유형에 대해 나의 최선의 전략은 무엇이며 어떤 수집 유형과 제거 방법의 조합이 가장 효과적입니까?


Vector의 길이는 고정되어 있지 않지만 입력되어 배열보다 우수합니다. 단점은 미리 채워진 목록을 정의하는 빠른 구문이 없지만 필자가 생각할 필요는 없다는 것입니다.
Bart van Heukelom

답변:


13

모든 추가 / 제거를 별도의 목록에 저장하고 업데이트 루프를 반복 한 후에 해당 작업을 수행합니다.


10

Flixel 프레임 워크는 dead 플래그 (실제로 그려야 하는지를 결정하는 몇 가지 플래그)를 사용합니다. 엔터티를 소생 시키거나 성능에 문제가 있으면 dead 플래그를 사용한다고 말하고 싶습니다. 필자의 경험에 따르면 새 엔터티를 인스턴스화하는 것은 설명하는 사용 사례에서 가장 비싼 작업이며, 요소를 스 플라이 싱하거나 null로 설정하면 Flash에서 때때로 가비지 수집이 발생하면 메모리가 팽창 할 수 있습니다.


1
flixel의 경우 +1 재활용은 dead실제로 성능에 도움이됩니다.
스노우 블라인드

3

일부 기술은 본질적으로 다른 기술보다 효율적이지만 대상 플랫폼에서주기가 부족한 경우에만 중요합니다. 어떤 기술을 사용하든 게임을 더 빨리 완료 할 수 있습니다. 그 동안 컨테이너 데이터 구조의 특정 구현에 의존하지 마십시오. 필요할 경우 나중에 최적화하는 데 도움이됩니다.

여기 다른 사람들이 이미 논의한 기술 중 일부를 다루기 위해. 엔터티 순서가 중요한 경우 데드 플래그를 사용하면 다음 프레임에서 업데이트 루프 중에 스플 라이스를 수행 할 수 있습니다. 예. 매우 간단한 의사 코드 :

void updateGame()
{
  // updateEntities()
  Entity* pSrcEntity = &mEntities[0];
  Entity* pDstEntity = &mEntities[0];
  newNumEntities = 0;
  for (int i = 0; i < numEntities; i++)
  {
    if (!pSrcEntity->isDead)
    {
       // could be inline but whatever.
       updateEntity(pDstEntity, pSrcEntity);
       // if entity just died, don't update the pDstEntity pointer, 
       // and just let the next entity updated overwrite it.
       if (!pDstEntity->isDead)
       {
          pDstEntity++;
          newNumEntities++;
       }
    }
    pSrcEntity++;
  }
}
numEntities = newNumEntities;

이 체계의 특성은 다음과 같습니다.

  • 엔터티의 자연스러운 압축성 (엔터티 슬롯을 회수하기 전에 1 프레임 대기 시간이있을 수 있음).
  • 무작위 재주문 문제가 없습니다.
  • 이중 연결된 목록에는 O (1) 삽입 / 삭제가 있지만 최적의 캐시 대기 시간 숨기기를 위해 프리 페치하기가 매우 어렵습니다. 그것들을 컴팩트 한 배열로 유지하면 블록 프리 페치 기술이 잘 작동합니다.
  • 다중 객체 소멸의 경우 순서 및 압축을 유지하기 위해 중복 시프트 사본을 수행 할 필요가 없습니다 (업데이트 패스 중에 모두 한 번 수행됨)
  • 업데이트 중에 이미 캐시에 있어야하는 데이터를 활용할 수 있습니다.
  • 소스 및 대상 엔티티 poitner가 배열을 분리 해야하는 경우 잘 작동합니다. 그런 다음 멀티 코어 /를 활용하기 위해 엔터티 배열을 이중 버퍼링 할 수 있습니다. 하나의 스레드는 프레임 N에 대한 엔티티를 업데이트 / 쓰기하는 반면, 다른 스레드는 프레임 N-1에 대한 이전 프레임의 엔티티를 렌더링합니다.
  • 컴팩트 함은 예를 들어 훨씬 많은 CPU 작업 오프로드를 위해 이기종 프로세서에 전체 로트를 더 쉽게 전달할 수 있음을 의미합니다. SPU 또는 GPU.

+1. 나는 이것을 좋아한다. 풀 내에서 주문 업데이트를 거의 필요로하지 않지만 상황
Kaj

2

내 일반적인 프로그래밍 경험으로 말하면 스 플라이 싱은 일반적으로 모든 기존 요소를 하나 위로 이동하는 작업이 느린 작업입니다. 나는 그것을 null로 설정하는 것이 여기서 가장 좋은 해결책 이라고 생각 합니다 . 죽은 플래그는 작동하지만 코드가 지저분하지 않도록주의해야합니다.

실제로 채팅방에서 리소스 풀링에 대해 이야기하고있었습니다. 그것은 매우 좋은 습관이며, 그렇게하고 있다는 것을 듣는 것이 좋습니다. :)


1
업데이트 순서가 중요하지 않은 경우 마지막 엔터티를 현재 인덱스로 이동하고 풀 수와 반복자 인덱스를 줄이는 것만 큼 간단하게 스 플라이 싱해야합니다.
Kaj

와, 아주 좋은 지적 Kaj! :)
Ricket

2

개인적으로, 나는 연결리스트를 사용할 것입니다. 즐겨 찾기 목록을 빠르게 반복하고 항목을 추가 및 제거 할 수 있습니다. 구조의 항목에 직접 액세스해야하는 경우 (예 : 인덱스에 대한 액세스) 배열 또는 벡터를 사용하는 것이 좋지만 필요한 것 같지는 않습니다.

링크 된 목록에서 항목을 제거 할 때마다 개체 풀에 추가 한 다음 다시 할당하여 메모리 할당을 절약 할 수 있습니다.

여러 프로젝트에서 다각형 데이터 구조 를 사용했으며 매우 만족했습니다.

편집 : 죄송합니다, 제거 전략 측면에서 대답이 명확하지 않다고 생각합니다. 죽은 즉시 목록에서 항목을 제거하고 풀링 구조 (재활용)에 직접 추가하는 것이 좋습니다. 연결된 목록에서 항목을 제거하면 성능이 뛰어나므로 문제가 없습니다.


1
여기서 이중 연결 목록을 제안한다고 가정합니까? (앞으로 / 뒤로)? 또한 : 링크 요소에 풀을 제안하거나 링크 목록에 각 포인터 홀더를 동적으로 할당하고 있습니까?
Simon

예, 해당 작업에 가장 적합한 이중 연결 목록이어야합니다. 지적 해 주셔서 감사합니다! 항목 재사용과 관련하여 : 풀링 된 풀 / 클래스 구조가 필요하다면 새로운 객체를 생성하거나 기존 인스턴스를 사용하는 풀링 클래스 / 데이터 구조에 대해 생각하고있었습니다. 따라서 실제로 "죽은"항목을 목록에서 제거하고 나중에 사용하기 위해 풀에 추가하는 것이 좋습니다.
bummzack

단독으로 연결된 목록이 정상적으로 작동합니다. 이중 연결리스트는 양방향 반복의 이점 만 제공합니다. 현재 항목을 제거하는 옵션으로 단일 연결 목록을 반복하려면 이전 항목을 추적해야합니다.
deft_code

@caspin 네 맞습니다. 단일 링크 목록을 사용하는 경우 이전 노드를 추적 next하고 삭제 된 노드 이후에 해당 노드에 포인터를 연결해야 합니다. 이 작업을 직접 수행해야하는 번거 로움을 원하지 않으면 이중 연결 목록이 DataStructure의 선택입니다.
bummzack

1

"단순히"나를 건너 뛰고 죽었다. "라고 플래그를 설정했습니다. 나는 엔티티를 모으고 있습니다. 따라서 죽은 엔티티는 어느 시점에서 다시 살아날 가능성이 높습니다"

이 특정 응용 프로그램과 관련하여 자신의 질문에 대답했다고 생각합니다. 푸시 앤 팝 이외의 작업을 계획하고 있다면 배열에서 벗어날 것입니다. 무거운 작업을 수행하려는 경우 연결된 목록이 더 현명한 방법입니다. 이미 말했듯이 동일한 엔티티를 게임에 다시 통합하려는 경우 부울 변수 만 설정하고 게임 작업 루프 중에 확인하는 것이 좋습니다.


0

내가 사용한 lib에서 찾은 깨끗하고 일반적인 솔루션 중 하나는 잠금 가능한 맵을 사용하는 것입니다.

당신은이 작업을 lock()하고 unlock()당신이 있습니다지도를 통해 당신으로 반복하면서, lock()지금이 시점이 적용되지 않는 맵을 변경하는 모든 작업에서, 그것은 단지로 밀어 도착은 CommandQueue당신이 전화를 한 번 실행됩니다unlock() .

따라서 엔티티를 제거하면 다음 의사 코드가 생깁니다.

void lockableMap::remove(std::string id) {
   if(isLocked) {
       commandQueue.add(new RemoveCommand(id));
   } else {
       //remove element from map
   }

그리고 당신이 unlock()

isLocked = false
commandQueue.execute(this);

당신이 고려해야 할 것은 루프 후에 엔티티 만 제거한다는 것입니다.

편집 : 이것은 Simon이 제안한 솔루션입니다.



0

두 가지 방법이 있습니다.

삭제할 객체를 호출하면 실제로 두 개의 플래그가 설정됩니다.

1. 컨테이너에 객체가 삭제되었음을 알리는 것

컨테이너에 삭제 요청 된 개체를 알려주는 하나

void object::deleteObject()
{
    container->objectHasBeenDeleted = true;
    isToDelete = true;
}

하나 의 객체 벡터를 사용하는 것

std::vector<object*> objects;

그런 다음 업데이트 기능에서 객체가 삭제되었는지 확인하고 모든 객체를 반복하는지 확인하고 삭제 플래그가있는 객체를 제거하십시오.

void container::update()
{
    if (objectHasBeenDeleted)
    {
        std::vector<object*>::iterator ListIterator;
        for(ListIterator=objects.begin(); ListIterator!=objects.end();)
        {
            if( (*ListIterator)->isToDelete )
            {
                ListIterator = objects.erase(ListIterator);
                delete *ListIterator;
            }
            else {
                ++ListIterator;
            }
        }
    objectHasBeenDeleted = false;
    }
}

2 개의 객체의 (포인터를 가리키는) 벡터 사용.

std::vector<object*> *objects;

업데이트 기능에서 객체를 삭제하려면 객체를 반복하고 삭제하지 않을 객체를 새 벡터에 추가하십시오. 객체 벡터를 삭제하고 포인터를 새 벡터로 설정

void container::update()
{
    if (objectHasBeenDeleted)
    {
        std::vector<object*> *newVector;
        unsigned long i;
        for (i = 0; i < objects->size(); i++)
        {
            if (!objects->at(i)->isToDelete)
            {
                newVector->push_back(objects->at(i));
            }
        }
        delete objects;
        objects = newVector;
        objectHasBeenDeleted = false;
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.