보간 및 스레딩을위한 데이터 구조?


20

최근에 내 게임의 프레임 속도 지 터링 문제를 해결해 왔으며 최고의 해결책은 클래식 Fix Your Timestep 에서 Glenn Fiedler ( Gaffer on Games)가 제안한 것 같습니다! 조.

지금-나는 이미 업데이트에 고정 된 시간 단계를 사용하고 있습니다. 문제는 렌더링을 위해 제안 된 보간을 수행하지 않는다는 것입니다. 결과적으로 렌더링 속도가 업데이트 속도와 일치하지 않으면 프레임이 두 배로 건너 뛰거나 건너 뜁니다. 이것들은 시각적으로 눈에 띄게 나타납니다.

게임에 보간을 추가하고 싶습니다. 다른 사람들이이를 지원하기 위해 데이터와 코드를 어떻게 구성했는지 알고 싶습니다.

분명히 내 렌더러와 관련된 두 가지 게임 상태 정보 사본을 저장 (어디서 / 어떻게?)하여 보간 할 수 있도록해야합니다.

또한-스레딩을 추가하기에 좋은 장소 인 것 같습니다. 업데이트 스레드가 게임 상태 의 세 번째 복사본에서 작동 하고 다른 두 복사본은 렌더 스레드의 읽기 전용으로 남겨 둘 수 있다고 생각합니다 . (이것이 좋은 생각입니까?)

게임 상태가 2 ~ 3 가지 버전 인 경우 단일 버전보다 성능 및 훨씬 더 중요한 안정성 및 개발자 생산성 문제가 발생할 수 있습니다. 저는 특히 이러한 문제를 완화시키는 방법에 관심이 있습니다.

특히 게임 상태에서 오브젝트 추가 및 제거를 처리하는 방법에 대한 문제라고 생각합니다.

마지막으로, 일부 상태는 렌더링에 직접 필요하지 않거나 다른 버전 (예 : 단일 상태를 저장하는 타사 물리 엔진)을 추적하기가 너무 어려울 것 같습니다. 사람들은 그러한 시스템 내에서 이런 종류의 데이터를 처리했습니다.

답변:


4

전체 게임 상태를 복제하려고 시도하지 마십시오. 그것을 보간하는 것은 악몽 일 것입니다. 가변적이고 필요한 부분을 렌더링으로 분리하기 만하면됩니다 ( "시각적 상태"라고 함).

각 객체 클래스에 대해 객체 Visual State를 보유 할 수있는 동반 클래스를 작성하십시오. 이 오브젝트는 시뮬레이션에 의해 생성되고 렌더링에 사용됩니다. 보간이 쉽게 연결됩니다. 상태가 변경 불가능하고 값으로 전달되면 스레딩 문제가 없습니다.

렌더링은 일반적으로 객체 간의 논리적 관계에 대해 알 필요가 없으므로 렌더링에 사용되는 구조는 일반 벡터 또는 최대 간단한 트리가됩니다.

전통적인 디자인

class Actor
{
  Matrix4x3 position;
  float fuel;
  float armor;
  float stamina;
  float age;

  void Simulate(float deltaT)
  {
    age += deltaT;
    armor -= HitByAWeapon();
  }
}

시각적 상태 사용

class IVisualState
{
  public:
  virtual void Interpolate(const IVisualState &newVS, float f) {}
};
class Actor
{
  struct VisualState: public IVisualState
  {
    Matrix4x3 position;
    float fuel;
    float armor;
    float stamina;
    float age;

    virtual auto_ptr<IVisualState> Interpolate(const IVisualState &newVS, float f)
    {
      const VisualState &newState = static_cast<const VisualState &>(newVS);
      IVisualState *ret = new VisualState;
      ret->age = lerp(this->age,newState.age);
      // ... interpolate other properties as well, using any suitable interpolation method
      // liner, spline, slerp, whatever works best for the given property
      return ret;
    };
  };

  auto_ptr<VisualState> state_;

  void Simulate(float deltaT)
  {
    state_->age += deltaT;
    state_->armor -= HitByAWeapon();
  }
}

1
"new"(C ++의 예약어)를 매개 변수 이름으로 사용하지 않으면 예제를보다 쉽게 ​​읽을 수 있습니다.
Steve S

3

내 솔루션은 대부분의 것보다 훨씬 덜 우아하고 복잡합니다. 내가 사용 Box2D의를 시스템 상태의 하나 개 이상의 복사본을 유지하는 것은 관리 할 수없는, 그래서 내 물리 엔진으로 (물리 시스템이 다음 동기화를 유지하려고 클론, 더 나은 방법이있을 수 있지만 내가 가지고 올 수 없었다 하나).

대신 나는 물리 생성의 카운터를 유지합니다 . 각 업데이트는 물리 시스템이 두 번 업데이트 될 때 생성 카운터가 두 번 업데이트 될 때 물리 생성을 증가시킵니다.

렌더링 시스템은 마지막 렌더링 된 생성 및 해당 생성 이후 델타를 추적합니다. 위치를 보간하려는 객체를 렌더링 할 때 위치 및 속도와 함께이 값을 사용하여 객체를 렌더링해야하는 위치를 추측 할 수 있습니다.

물리 엔진이 너무 빠르면 어떻게해야하는지 언급하지 않았습니다. 나는 당신이 빠른 움직임을 위해 보간해서는 안된다고 거의 주장합니다. 두 가지를 모두했다면 너무 느리게 추측 한 다음 너무 빠르게 추측하여 스프라이트가 점프하지 않도록주의해야합니다.

보간 재료를 쓸 때 그래픽을 60Hz로, 물리를 30Hz로 실행했습니다. Box2D가 120Hz에서 실행될 때 훨씬 안정적이라는 것이 밝혀졌습니다. 이 때문에 내 보간 코드는 거의 사용되지 않습니다. 대상 프레임 속도를 두 배로 늘려서 프레임 당 평균 업데이트의 물리가 발생합니다. 지터를 사용하면 1 ~ 3 배가 될 수 있지만 거의 0 또는 4 이상은 아닙니다. 물리 속도가 높을수록 보간 문제 자체가 해결됩니다. 물리 및 프레임 속도를 60hz로 모두 실행하면 프레임 당 0-2 개의 업데이트가 발생할 수 있습니다. 0과 2의 시각적 차이는 1과 3에 비해 큽니다.


3
나도 이것을 찾았다. 거의 60Hz의 프레임 업데이트를 가진 120Hz 물리 루프는 보간을 거의 쓸모 없게 만듭니다. 불행히도 이것은 120Hz 물리 루프를 감당할 수있는 게임 세트에서만 작동합니다.

방금 120Hz 업데이트 루프로 전환을 시도했습니다. 이것은 물리를보다 안정적으로 만들고 60Hz가 아닌 프레임 속도로 게임을 매끄럽게 보이게하는 두 가지 이점이있는 것 같습니다. 단점은 신중하게 조정 된 게임 플레이 물리를 모두 파괴한다는 것입니다. 따라서 프로젝트 초기에 선택 해야하는 옵션입니다.
Andrew Russell

또한 : 보간 시스템에 대한 설명을 실제로 이해하지 못합니다. 실제로 외삽처럼 들리나요?
앤드류 러셀

잘 했어 실제로 외삽 시스템을 설명했습니다. 위치, 속도 및 마지막 물리 업데이트 이후의 시간을 감안할 때 물리 엔진이 정지되지 않은 경우 물체의 위치를 ​​추정합니다.
deft_code 5

2

나는 시간 간격에 대한 이러한 접근 방식이 꽤 자주 제안되는 것을 들었지만 10 년 동안 게임에서 고정 된 시간 간격과 보간에 의존하는 실제 프로젝트에서 일한 적이 없습니다.

가변 타임 스텝 시스템보다 일반적으로 더 많은 노력이 필요한 것 같습니다 (25Hz-100Hz의 범위에서 합리적인 프레임 속도 범위를 가정).

스레딩은 없지만 고정 시간 단계 로직 업데이트 및 업데이트하지 않을 때 가능한 빨리 렌더링하는 매우 작은 프로토 타입에 대해 고정 된 timestep + interpolation 접근 방식을 한 번 시도했습니다. 내 접근법에는 CInterpolatedVector 및 CInterpolatedMatrix와 같은 몇 가지 클래스가 있습니다.이 클래스는 이전 / 현재 값을 저장하고 렌더 코드에서 액세서를 사용하여 현재 렌더링 시간의 값을 검색합니다 (항상 이전과 현재 시간)

각 게임 오브젝트는 업데이트가 끝날 때 현재 상태를 이러한 보간 가능한 벡터 / 매트릭스 세트로 설정합니다. 이러한 종류의 작업은 스레딩을 지원하도록 확장 될 수 있습니다. 업데이트 된 값과 적어도 2 개의 이전 값 사이에서 보간하려면 적어도 3 개의 값 세트가 필요합니다 ...

일부 값은 사소하게 보간 할 수 없습니다 (예 : '스프라이트 애니메이션 프레임', '특수 효과 활성화'). 게임의 필요에 따라 보간을 완전히 건너 뛰거나 문제가 발생할 수 있습니다.

IMHO, RTS 또는 많은 수의 객체가있는 다른 게임을 만들고 네트워크 게임에 대해 2 개의 독립적 인 시뮬레이션을 유지 해야하는 경우가 아니라면 가변 시간 단계를 거치는 것이 가장 좋습니다 . 개체 위치가 아닌 네트워크). 이러한 상황에서는 고정 시간 단계 만 사용할 수 있습니다.


1
최소한 Quake 3이이 접근 방식을 사용하고있는 것으로 보입니다. 기본 "틱"은 20fps (50ms)입니다.
Suma

흥미 롭군 경쟁이 치열한 멀티 플레이어 PC 게임에는 더 빠른 PC / 높은 프레임 속도가 이점 (많은 반응 형 컨트롤 또는 물리 / 충돌 동작에서 작지만 악용 될 수있는 차이)을 얻지 못하도록하기 위해 경쟁이 치열한 멀티 플레이어 PC 게임에 이점이 있다고 생각합니다. ?
bluescrn

1
10 년 동안 시뮬레이션 및 렌더러에서 물리학을 물리 치지 못한 게임을 만난 적이 있습니까? 그렇게하는 순간 애니메이션에서 지각 된 얼간이를 보간하거나 수용해야합니다.
Kaj

2

분명히 내 렌더러와 관련된 두 가지 게임 상태 정보 사본을 저장 (어디서 / 어떻게?)하여 보간 할 수 있도록해야합니다.

예, 고맙게도 여기서 핵심은 "내 렌더러와 관련이 있습니다". 이것은 이전 위치와 타임 스탬프를 믹스에 추가하는 것 이상입니다. 2 개의 위치가 주어지면 그 사이의 위치로 보간 할 수 있으며 3D 애니메이션 시스템을 사용하는 경우 일반적으로 정확한 시점에 포즈를 요청할 수 있습니다.

정말 간단합니다. 렌더러가 게임 오브젝트를 렌더링 할 수 있어야한다고 상상해보십시오. 예전에는 물체가 어떻게 생겼는지 물어 보았지만 이제는 특정 시간에 어떻게 생겼는지 물어봐야합니다. 그 질문에 대답하는 데 필요한 정보를 저장하면됩니다.

또한-스레딩을 추가하기에 좋은 장소 인 것 같습니다. 업데이트 스레드가 게임 상태의 세 번째 복사본에서 작동하고 다른 두 복사본은 렌더 스레드의 읽기 전용으로 남겨 둘 수 있다고 생각합니다. (이것이 좋은 생각입니까?)

이 시점에서 통증이 추가되는 레시피처럼 들립니다. 전체적인 의미를 생각하지는 않았지만 대기 시간이 길어지면 약간의 추가 처리량이 발생할 수 있습니다. 아, 다른 코어를 사용할 수 있다는 이점이 있지만, 나는 몰라.


1

참고 실제로 보간법을 조사하고 있지 않으므로이 답변으로 해결되지 않습니다. 렌더링 스레드에 대한 게임 상태 사본 하나와 업데이트 스레드에 대한 사본 하나만 있으면됩니다. 보간 문제에 대해서는 언급 할 수 없지만 다음 솔루션을 수정하여 보간 할 수는 있습니다.

멀티 스레드 엔진을 디자인하고 생각하면서 이것에 대해 궁금했습니다. 그래서 어떤 종류의 "저널링"또는 "트랜잭션"디자인 패턴을 구현하는 방법에 대해 Stack Overflow에 대한 질문을했습니다 . 나는 좋은 반응을 얻었고, 받아 들여진 대답은 정말로 나를 생각하게 만들었다.

모든 자식도 변경할 수 없어야하므로 불변의 객체를 만드는 것은 어렵고 모든 것이 진정으로 변하지 않도록주의해야합니다. 그러나 실제로주의를 기울이면 GameState게임의 모든 데이터 (및 하위 데이터 등)가 포함 된 수퍼 클래스 를 만들 수 있습니다 . Model-View-Controller 조직 스타일의 "모델"부분.

그런 다음 Jeffrey가 말했듯이 GameState 객체의 인스턴스는 빠르고 메모리 효율적이며 스레드로부터 안전합니다. 큰 단점은 모델에 대한 내용을 변경하려면 모델을 다시 만들어야 할 필요가 있으므로 코드가 크게 혼란스럽지 않도록 조심해야한다는 것입니다. GameState 객체 내 변수를 새로운 값으로 설정하는 var = val;것은 코드 라인 측면에서 단순한 것보다 더 복잡 합니다.

그래도 정말 흥미 롭습니다. 매 프레임마다 전체 데이터 구조를 복사 할 필요는 없습니다. 불변 구조에 대한 포인터를 복사하기 만하면됩니다. 그 자체로 매우 인상적입니다. 동의하지 않습니까?


실제로 흥미로운 구조입니다. 그러나 일반적인 경우는 프레임마다 정확히 한 번씩 바뀌는 상당히 평평한 객체 트리이기 때문에 게임에 잘 작동하는지 확실하지 않습니다. 또한 동적 메모리 할당은 큰 문제가되지 않기 때문입니다.
Andrew Russell

이와 같은 경우 동적 할당은 매우 쉽게 수행 할 수 있습니다. 원형 버퍼를 사용하고 한쪽에서 자라서 다른 쪽에서 풀어 놓을 수 있습니다.
Suma

... 동적 할당이 아니라 사전 할당 된 메모리의 동적 사용;)
Kaj

1

장면 그래프에서 각 노드의 게임 상태를 3 부 복사하는 것으로 시작했습니다. 하나는 장면 그래프 스레드에 의해 작성되고, 하나는 렌더러에 의해 읽히고 있으며, 다른 하나는 교체해야하는 즉시 읽기 / 쓰기가 가능합니다. 이것은 잘 작동했지만 너무 복잡했습니다.

그런 다음 렌더링 될 대상의 상태를 3 개만 유지하면된다는 것을 깨달았습니다. 내 업데이트 스레드는 이제 훨씬 작은 "RenderCommands"버퍼 중 하나를 채 웁니다. 렌더러는 현재 작성되지 않은 최신 버퍼에서 읽으므로 스레드가 서로를 기다리지 않습니다.

내 설정에서 각 RenderCommand에는 3D 지오메트리 / 재료, 변환 매트릭스 및 이에 영향을주는 라이트 목록이 있습니다 (여전히 렌더링을 수행함).

내 렌더 스레드는 더 이상 컬링 또는 광 거리 계산을 수행 할 필요가 없으며, 이로 인해 큰 장면에서 크게 속도가 향상되었습니다.

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