본문 바로가기
IT/Game

게임 프로그래밍 패턴 8장 ~ 10장 (22. 05. 24)

by YEON-DU 2022. 8. 15.
반응형

Chapter8. 이중 버퍼

의도 : 여러 순차 작업의 결과를 한번에 보여준다.

동기 : 본질적으로 컴퓨터는 순차적으로 동작한다. 하지만 사용자 입장에서는 순차적으로 혹은 동시에 진행되는 여러 작업을 한 번에 모아서 봐야할 때(EX. 게임에서의 렌더링 - 유저에게 보여줄 게임 화면)가 있다. 이중 버퍼는 이런 문제를 해결한다.

렌더링 도중 나타나는 테어링

화면이 그려질 때 깜빡거린다. ⇒ 더블 버퍼링을 사용하지 않아서.

progressive : 한번에 한 줄씩 그린다. 모든 라인을 한번에 그린다.

interace : 반 씩 그린다 (짝수 / 홀수) (대역폭을 많이 확대하지 못하던 시기에 사용하던 방법) 읽을 메모리와 sync를 맞춘다.

한 곳에만 메모리를 그리다보면 모니터의 읽는 속도와 실제 그리는 속도가 달라서 보여주는 그림이 달라지는 문제를 해결하기 위해 버퍼를 2개 사용한다.

테어링 : 그리는 동안 도화지를 뺏어가서 생기는 현상.

유저를 우리가 연출한 연극을 보는 관객이라고 생각해보자.

1장이 끝나면 2장이 시작하기 전에 무대 설치를 바꿔야 한다.

장면이 바뀔 때마다 무대 담당자들이 올라와 소도구를 옮긴다면 몰입감이 깨질 것이다. 다음 장면을 준비하는 동안 조명을 어둡게 할 순 있지만 관객은 뭔가가 진행된다는 걸 알 수 있다. 끊김 없이 바로 장면을 전환할 방법은 없을까?

공간을 투자해서 괜찮은 해결책을 얻을 수 있다.

무대를 두 개 준비해놓고 무대 A와 B가 변경될 때 무대를 교체하는 것이다.

무대 조명만 바꾸면 (무대 A가 끝나면 A의 조명을 끄고 B의 조명을 킨다) 장면을 전환할 수 있기 때문에 기다리지 않고 다음 장면을 볼 수 있다. 관객은 무대 담당자를 볼 일이 없다.

이중 버퍼가 바로 이런 식이다.

프레임버퍼를 두 개 준비해, 하나의 버퍼에는 지금 프레임에 보일 값을 둬서 GPU가 원할 때 언제든지 읽을 수 있게 한다.

그동안 렌더링 코드는 다른 프레임 버퍼를 채운다. 렌더링 코드가 장면을 다 그린 후에는 버퍼를 교체한 뒤 지금부터 두 번째 버퍼를 읽으라고 알려준다. 화면 깜빡임에 맞춰 버퍼가 바뀌기 때문에 테어링은 더이상 생기지 않고 전체 장면이 한 번에 나타나게 된다.

패턴

버퍼 클래스는 변경이 가능한 상태인 버퍼를 캡슐화한다.

버퍼는 점차적으로 수정되지만 밖에서는 한 번에 바뀌는 것처럼 보이게 하고 싶다. 이를 위해서 버퍼 클래스는 현재 버퍼와 다음 버퍼, 이렇게 두 개의 버퍼를 갖는다.

정보를 읽을 때는 항상 현재 버퍼에 접근한다. 정보를 쓸 때는 항상 다음 버퍼에 접근한다.

변경이 끝나면 다음 버퍼와 현재 버퍼를 교체해 다음 버퍼가 보여지게 한다. 현재 버퍼는 새로운 다음 버퍼가 되어 재사용된다.

언제 쓸 것인가?

이중 버퍼 패턴은 언제 써야할 지 그냥 알 수 있는 패턴 중 하나이다. 이중 버퍼 시스템이 없으면 시각적으로 이상하게 보이거나 시스템이 오작동하기 때문이다.

구체적으로는 다음과 같은 상황에서 적합하다.

  • 순차적으로 변경해야 하는 상태가 있다.
  • 이 상태는 변경 도중에도 접근 가능해야 한다.
  • 바깥 코드에서는 작업 중인 상태에 접근할 수 없어야 한다.
  • 상태에 값을 쓰는 도중에도 기다리지 않고 바로 접근할 수 있어야 한다.

주의사항

다른 대규모 아키텍처용 패턴과 달리 이중 버퍼는 코드 구현 수준에서 적용되기 때문에 코드 전체에 미치는 영향이 적은 편이고 다들 비슷하게 쓰고 있다. 그래도 몇 가지 주의할 점은 다음과 같다.

교체 연산 자체에 시간이 걸린다.

이중 버퍼 패턴에서는 버퍼의 값을 다 입력했다면 버퍼를 교체해야 한다. 교체 연산은 원자적atomic이어야 한다. 즉 교체 중에는 두 버퍼 모두에 접근할 수 없어야 한다. 대부분은 포인터만 바꾸면 되기 때문에 충분히 빠르지만, 혹시라도 버퍼에 값을 쓰는 것보다 교체가 더 오래 걸린다면 이중 버퍼 패턴이 아무런 도움이 안 된다.

버퍼가 두 개 필요하다.

메모리가 부족해 버퍼를 두 개 만들기 어렵다면 이중 버퍼 패턴을 포기하고 상태를 변경하는 동안 밖에서 접근하지 못하게 할 방법을 찾아야 한다.

디자인 결정

버퍼를 어떻게 교체할 것인가?

  • 버퍼 포인터나 레퍼런스를 교체
    • 빠르다.
    • 버퍼 코드 밖에서는 버퍼 메모리를 포인터로 저장할 수 없다는 한계가 있다.
    • 버퍼에 남아있는 데이터는 바로 이전 프레임 데이터가 아닌 2프레임 전 데이터다.
  • 버퍼끼리 데이터를 복사 
    • 다음 버퍼에는 딱 한 프레임 전 데이터가 들어있다.
    • 교체 시간이 더 걸린다.

얼마나 정밀하게 버퍼링할 것인가?

  • 버퍼가 한 덩어리라면 간단히 교체할 수 있다.
  • 여러 객체가 각종 데이터를 들고 있다면 교체가 더 느리다.

 

Chapter9. 게임 루프

의도

게임 시간 진행을 유저 입력, 프로세서 속도와 디커플링한다.

동기

Loop 도는 모든 Application에서 사용한다고 볼 수 있다.

게임 시간과 실제 시간이 어느 하드웨어에서 실행되건 일정한 속도로 실행될 수 있도록 하는 것이 게임 루프의 핵심 업무이다.

패턴

게임 루프는 게임하는 내내 실행된다. 한 번 돌 때마다 멈춤 없이 유저 입력을 처리한 뒤 게임 상태를 업데이트하고 게임 화면을 렌더링한다. 시간 흐름에 따라 게임플레이 속도를 조절한다.

주의사항

게임 루프는 전체 게임 코드 중에서도 가장 핵심에 해당한다.

게임 루프 코드는 최적화를 고려해 깐깐하게 만들어야 한다.

간단한 게임 루프

가변 시간 간격에 영향을 받지 않는 부분 중 하나가 렌더링이다. 따라서 핵심 루프로부터 렌더링을 분리하여 렌더링 간격을 유연하게 만들어 프로세서 낭비를 줄인 것이 위의 루프이다.

 

디자인 결정

게임 루프를 직접 관리하는가, 플랫폼이 관리하는가?

  • 플랫폼 이벤트 루프 사용
    •  간단하다.
    • 플랫폼에 잘 맞는다.
    • 시간을 제어할 수 없다.
  • 게임 엔진 루프 사용
    • 코드를 직접 작성하지 않아도 된다.
    • 코드를 직접 작성할 수 없다.
  • 직접 만든 루프 사용
    • 완전한 제어
    • 플랫폼과 상호작용해야 한다.

전력 소모 문제

  • 최대한 빨리 실행하기
  • 프레임 레이트 제한하기

게임플레이 속도는 어떻게 제어할 것인가?

  • 동기화 없는 고정 시간 간격 방식
    • 간단하다.
    • 게임 속도는 하드웨어와 게임 복잡도에 바로 영향을 받는다.
  • 동기화하는 고정 시간 간격 방식
    • 그래도 간단한 편이다.
    • 전력 효율이 높다.
    • 게임이 너무 빨라지진 않는다.
    • 게임이 너무 느려질 수 있다.
  • 가변 시간 간격 방식
    • 너무 느리거나 너무 빠른 곳에서도 맞춰서 플레이할 수 있다.
    • 게임플레이를 불안정하고 비결정적으로 만든다.
  • 업데이트는 고정 시간 간격으로, 렌더링은 가변 시간 간격으로
    • 너무 느리거나 너무 빨라도 잘 적응한다.
    • 훨씬 복잡하다.

 

Chapter10. 업데이트 메서드

의도

컬렉션에 들어 있는 객체별로 한 프레임 단위의 작업을 진행하라고 알려줘서 전체를 시뮬레이션 한다.

패턴

게임 월드객체 컬렉션을 관리한다.

각 객체는 한 프레임 단위의 동작을 시뮬레이션하기 위한 업데이트 메서드를 구현한다.

언제 쓸 것인가?

  • 동시에 동작해야 하는 객체나 시스템이 게임에 많다.
  • 각 객체의 동작은 다른 객체와 거의 독립적이다.
  • 객체는 시간의 흐름에 따라 시뮬레이션되어야 한다.

주의사항

코드를 한 프레임 단위로 끊어서 실행하는 게 더 복잡하다.

다음 프레임에서 다시 시작할 수 있도록 현재 상태를 저장해야 한다.

모든 객체는 매 프레임마다 시뮬레이션 되지만 진짜로 동시에 되는 건 아니다

업데이트 도중에 객체 목록을 바꾸는 건 조심해야 한다.

반응형

댓글