게임 로직 스레드와 렌더링 스레드 간의 동기화


16

하나의 게임 로직과 렌더링은 어떻게 분리됩니까? 나는 이미 여기에 정확하게 묻는 질문이있는 것처럼 보이지만 대답은 나에게 만족스럽지 않습니다.

내가 지금까지 이해 한 바에 따르면 다른 스레드로 분리하는 시점은 렌더링이 스왑 버퍼 호출에서 렌더링이 최종적으로 차단되는 다음 vsync를 기다리는 대신 게임 로직이 다음 틱을 즉시 실행하기 시작할 수 있다는 것입니다.

그러나 구체적으로 게임 로직 스레드와 렌더링 스레드 간의 경쟁 조건을 방지하기 위해 사용되는 데이터 구조 아마도 렌더링 스레드는 무엇을 그릴 지 알아 내기 위해 다양한 변수에 액세스해야하지만 게임 로직은 동일한 변수를 업데이트 할 수 있습니다.

이 문제를 처리하기위한 사실상의 표준 기술이 있습니까? 게임 로직을 실행할 때마다 렌더링 스레드에 필요한 데이터를 복사하는 것과 같습니다. 어떤 솔루션이 동기화 오버 헤드이거나 단일 스레드 만 실행하는 것보다 적은가?


1
나는 단지 링크를 스팸으로 만드는 것을 싫어하지만, 이것은 매우 좋은 읽기라고 생각하며 모든 질문에 대답해야합니다 : altdevblogaday.com/2011/07/03/threading-and-your-game-loop
Roy T.


1
이러한 링크는 일반적인 최종 결과를 제공하지만 그 방법을 자세히 설명하지는 않습니다. 전체 장면 그래프를 각 프레임 또는 다른 것으로 복사 하시겠습니까? 토론이 너무 높고 모호합니다.
user782220

각 경우에 얼마나 많은 상태가 복사되는지에 대한 링크가 상당히 명시 적이라고 생각했습니다. 예. (첫 번째 링크부터) "일괄 처리에는 프레임을 그리는 데 필요한 모든 정보가 포함되어 있지만 다른 게임 상태는 포함되어 있지 않습니다." 또는 (두 번째 링크에서) "그러나 여전히 데이터를 공유해야하지만 이제는 각 시스템이 공통 데이터 위치에 액세스하여 위치 또는 방향 데이터를 가져 오는 대신 각 시스템마다 자체 사본이 있습니다"(특히 3.2.2-상태 참조) 관리자)
DMGregory

인텔 기사를 쓴 사람은 최상위 스레딩이 매우 나쁜 아이디어라는 것을 알지 못하는 것 같습니다. 아무도 바보 같은 짓을하지 않습니다. 갑자기 전체 응용 프로그램이 특수 채널을 통해 통신해야하며 모든 곳에서 잠금 및 / 또는 대규모 조정 된 상태 교환이 있습니다. 전송 된 데이터가 처리 될시기를 알 수 없으므로 코드의 기능에 대해 추론하기가 매우 어렵습니다. 관련 씬 데이터 (참조 카운트 포인터로 변경할 수 없음, 값으로 변경할 수 있음)를 한 시점에 복사하고 서브 시스템이 원하는대로 정렬하도록하는 것이 훨씬 쉽습니다.
snake5

답변:


1

나는 똑같은 일을 해왔다. 추가 문제는 OpenGL (및 필자의 지식으로는 OpenAL) 및 기타 여러 하드웨어 인터페이스가 여러 스레드에 의해 호출되지 않는 효과적인 상태 머신이라는 점입니다. 나는 그들의 행동이 정의되어 있다고 생각하지 않으며 LWJGL (아마도 JOGL)의 경우 종종 예외가 발생합니다.

내가 한 일은 특정 인터페이스를 구현하는 일련의 스레드를 만들고 제어 객체의 스택에로드하는 것입니다. 해당 개체가 게임을 종료하라는 신호를 받으면 각 스레드를 통해 실행되고 구현 된 ceaseOperations () 메서드를 호출 한 다음 닫히기 전에 대기합니다. 사운드, 그래픽 또는 기타 데이터 렌더링과 관련 될 수있는 범용 데이터는 휘발성이거나 모든 스레드에서 보편적으로 사용할 수 있지만 스레드 메모리에는 절대 보관되지 않는 일련의 객체에 보관됩니다. 약간의 성능 저하가 있지만 올바르게 사용하면 오디오를 한 스레드에, 그래픽을 다른 스레드에, 물리를 또 다른 스레드에 유연하게 할당 할 수있어 기존의 "게임 루프"에 묶지 않고도 유연하게 할당 할 수 있습니다.

따라서 모든 OpenGL 호출은 그래픽스 스레드, 오디오 스레드를 통한 모든 OpenAL, 입력 스레드를 통한 모든 입력 및 조직 제어 스레드가 염려해야 할 모든 스레드 관리를 통과합니다. 게임 상태는 GameState 클래스에서 유지되며, 필요한 경우 모두 살펴볼 수 있습니다. 예를 들어, JOAL이 데이트를하고 새로운 JavaSound 버전을 대신 사용하고 싶다면 Audio에 대해 다른 스레드를 구현하면됩니다.

바라건대이 프로젝트에 이미 수천 줄이 있습니다. 샘플을 함께 긁어 내고 싶다면 내가 할 수있는 것을 볼 수 있습니다.


결국 직면하게 될 문제는이 설정이 멀티 코어 시스템에서 특히 잘 확장되지 않는다는 것입니다. 예, 일반적으로 오디오와 같은 자체 스레드에서 가장 잘 제공되는 게임의 측면이 있지만 나머지 게임 루프의 대부분은 스레드 풀 작업과 함께 직렬로 관리 할 수 ​​있습니다. 스레드 풀이 선호도 마스크를 지원하는 경우 동일한 스레드에서 실행되도록 렌더링 작업을 쉽게 대기열에 넣고 스레드 스케줄러가 스레드 작업 대기열을 관리하고 필요에 따라 작업을 수행하여 멀티 스레딩 및 멀티 코어 지원을 제공 할 수 있습니다.
Naros

1

일반적으로 그래픽 렌더링 패스 (및 일정, 실행시기 등)를 처리하는 논리는 별도의 스레드에서 처리됩니다. 그러나 해당 스레드는 게임 루프 (및 게임)를 개발하는 데 사용하는 플랫폼에 의해 이미 구현되어 실행 중입니다.

따라서 그래픽 새로 고침 일정과 독립적으로 게임 로직이 업데이트되는 게임 루프를 얻으려면 추가 스레드를 만들 필요가 없으며, 해당 그래픽 업데이트를 위해 기존 스레드를 탭하기 만하면됩니다.

사용중인 플랫폼에 따라 다릅니다. 예를 들면 다음과 같습니다.

  • 대부분의 Open GL 관련 플랫폼 ( C / C ++ 용 GLUT , JOLG for Java , Android의 OpenGL ES 관련 Action )에서 수행하는 경우 일반적으로 렌더링 스레드에서 주기적으로 호출하는 메소드 / 함수를 제공합니다. 게임 루프에 통합 할 수 있습니다 (해당 메소드 호출 시점에 따라 게임 루프의 반복을 만들지 않아도 됨). C를 사용하는 GLUT의 경우 다음과 같이하십시오.

    glutDisplayFunc (myFunctionForGraphicsDrawing);

    glutIdleFunc (myFunctionForUpdatingState);

  • JavaScript에서는 멀티 스레딩 (프로그램 방식에 도달 할 수 없음)이 없기 때문에 웹 워커 를 사용할 수 있으며 "requestAnimationFrame"메커니즘을 사용하여 새 그래픽 렌더링이 예약 될 때 알림을 받고 게임 상태 업데이트를 수행 할 수 있습니다 .

기본적으로 원하는 것은 혼합 단계 게임 루프입니다. 게임 상태를 업데이트하고 게임의 주요 스레드 내에서 호출되는 코드가 있으며 이미 주기적으로 이미 탭을 사용하거나 다시 호출하려고합니다. 그래픽을 새로 고칠 때가되면 기존 그래픽 렌더링 스레드가 표시됩니다.


0

Java에는 "synchronized"키워드가 있습니다.이 키워드는 변수를 스레드로부터 안전하게 만들기 위해 전달하는 변수를 잠급니다. C ++에서는 Mutex를 사용하여 동일한 것을 달성 할 수 있습니다. 예 :

자바:

synchronized(a){
    //code using a
}

C ++ :

mutex a_mutex;

void f(){
    a_mutex.lock();
    //code using a
    a_mutex.unlock();
}

변수를 잠그면 코드를 따르는 동안 변수가 변경되지 않도록하므로 렌더링하는 동안 업데이트 스레드에 의해 변수가 변경되지 않습니다 (사실은 변경되지만 렌더링 스레드의 관점에서 보면 변경되지 않습니다) 티). 변수 / 객체에 대한 포인터가 변경되지 않기 때문에 Java에서 동기화 된 키워드를 조심해야합니다. 포인터를 변경하지 않고 속성을 계속 변경할 수 있습니다. 이를 고려하여 객체를 직접 복사하거나 변경하지 않으려는 객체의 모든 속성에서 동기화 된 호출을 수행 할 수 있습니다.


1
OP는 게임 로직과 렌더링을 분리해야 할뿐만 아니라 다른 스레드가 현재 처리중인 위치에 관계없이 처리 중 앞으로 나아갈 수있는 스레드의 실속을 피하고 싶기 때문에 반드시 여기에 답이 될 필요는 없습니다. 고리.
Naros

0

논리 / 렌더러 스레드 통신을 처리하기 위해 일반적으로 본 것은 데이터를 3 중 버퍼링하는 것입니다. 이런 식으로 렌더 스레드는 읽은 버킷 0을 말합니다. 논리 스레드는 다음 프레임의 입력 소스로 버킷 1을 사용하고 프레임 데이터를 버킷 2에 씁니다.

동기 점에서, 세 개의 버킷 각각의 의미에 대한 인덱스가 교환되어 다음 프레임의 데이터가 렌더 스레드에 제공되고 논리 스레드가 계속 진행될 수 있습니다.

그러나 렌더링과 로직을 각각의 스레드로 분리 할 필요는 없습니다. 실제로 게임 루프 직렬을 유지하고 보간을 사용하여 논리 단계에서 렌더 프레임 속도를 분리 할 수 ​​있습니다. 이러한 종류의 설정을 사용하여 멀티 코어 프로세서를 활용하려면 작업 그룹에서 작동하는 스레드 풀이 있습니다. 이러한 작업은 단순히 0에서 100까지의 객체 목록을 반복하는 것이 아니라 5 개의 스레드에서 20 개의 5 개 버킷에있는 목록을 반복하여 성능을 효과적으로 향상 시키지만 주 루프를 복잡하게 만드는 것은 아닙니다.


0

이것은 오래된 게시물이지만 여전히 팝업이므로 여기에 2 센트를 추가하고 싶었습니다.

먼저 UI / 디스플레이 스레드 대 로직 스레드에 저장해야하는 데이터를 나열하십시오. UI 스레드에는 3D 메쉬, 텍스처, 라이트 정보 및 위치 / 회전 / 방향 데이터 사본이 포함될 수 있습니다.

게임 로직 스레드에서는 3D 게임 경계 크기, 경계 기본 요소 (구체, 큐브), 단순화 된 3D 메시 데이터 (예 : 자세한 충돌), 객체 속도, 회전 비율 등과 같은 움직임 / 동작에 영향을 미치는 모든 속성이 필요할 수 있습니다. 또한 위치 / 회전 / 방향 데이터.

두 목록을 비교하면 위치 / 회전 / 방향 데이터의 사본 만 로직에서 UI 스레드로 전달해야 함을 알 수 있습니다. 이 데이터가 속한 게임 오브젝트를 결정하기 위해 일종의 상관 관계 ID가 필요할 수도 있습니다.

작업 방법은 작업중인 언어에 따라 다릅니다. Scala에서는 Java / C ++에서 일종의 잠금 / 동기화에 소프트웨어 트랜잭션 메모리를 사용할 수 있습니다. 나는 불변의 데이터를 좋아하므로 매번 업데이트 할 때마다 새로운 불변의 객체를 반환하는 경향이 있습니다. 이것은 약간의 메모리 낭비이지만 현대 컴퓨터에서는 그렇게 큰 문제가 아닙니다. 여전히 공유 데이터 구조를 잠 그려면 할 수 있습니다. Java에서 Exchanger 클래스를 확인하십시오. 두 개 이상의 버퍼를 사용하면 속도가 향상 될 수 있습니다.

스레드간에 데이터를 공유하기 전에 실제로 전달해야하는 데이터의 양을 계산하십시오. 3D 공간을 분할하는 옥트리가 있고 10 개의 객체 중 5 개의 게임 객체를 볼 수있는 경우 논리가 10 개를 모두 업데이트해야하더라도 현재보고있는 5 개만 다시 그려야합니다. 자세한 내용은 다음 블로그를 참조 하십시오. http://gameprogrammingpatterns.com/game-loop.html 이것은 동기화에 관한 것이 아니라 게임 로직이 디스플레이에서 분리되는 방법과 극복해야 할 과제 (FPS)를 보여줍니다. 도움이 되었기를 바랍니다,

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