게임 프로그래밍 패턴 정리

2022년 06월 14일
제작기간 2022년 06월 14일
태그 Game

0. 도입

  • 좋은 소프트웨어 구조란, 수정이 쉽고 직관적인 구조
  • 프로토타이핑은 기능을 빠르게 보는데 도움을 주지만 유지가 힘듦
    • 버릴 코드는 반드시 버려야 함
    • 버릴 코드에 시간 쓰지 말기
  • 깔끔한 구조, 최적화, 빠른 구현 중 적절히 비중을 두어 개발해야함
    • 세마리 토끼 다 잡기 없음
  • 유연할 필요가 없다면 디커플링은 오버 엔지니어링임
  • 저수준 핵심 최적화는 가능하면 늦게 작업

1. 디자인 패턴

Command Pattern

  • method 호출을 데이터, 즉 객체화한 것
    • 변수에 저장하거나, 함수에 전달할 수 있음
  • callback, 함수 포인터, 클로저, 부분 적용 함수…

  • example
    • Closure를 사용하여 함수형으로 구현 가능
    • first-class object(일급 객체: 대부분의 연산을 지원)를 사용
    • 대상을 객체로 전달받으면 제어의 대상을 편하게 변경할 수 있음 (AI 구현에 용이)
    • 명령을 queue나 stream으로 관리
      • producer와 consumer를 디커플링할 수 있음
      • undo 가능

경량 패턴

  • sharing을 통해 많은 수의 fine-grained 객체를 지원
  • 객체를 마구 늘리지 않고 객체지향의 장점만 취할 수 있음
  • pointer를 통해서 객체의 값을 얻으려고 하면 캐시 미스가 발생할 수 있음

  • example
    • GPU에 같은 모양의 오브젝트들을 렌더링하기 위한 정보(메시)를 1회만 보내고 남은 정보(위치)만을 개별 처리함

Observer

  • 보고받는 대상을 지정하지 않고 이벤트를 보고하는 패턴
  • Observer class가 알림을 기다림
  • 관찰이 되는 대상이 이벤트 발생 시, Observer class에 알림을 줌
    • 대상은 옵저버를 복수개 들고 있음
  • 동기적인 함수 호출
  • example

Prototype

  • 원형이 되는 인스턴스를 사용하여 객체 종류를 명시, 견본을 복사해 새로운 객체를 생성
  • class기반 언어에서 객체에서는 일부를 상위 객체에 위임할 수 있음 (상속)
  • JS는 복제없는 프로토타입 기반 언어
  • JSON 데이터를 작성할 때, prototype을 사용하면 중복되는 데이터양을 줄일 수 있음

Singleton

  • 오직 하나의 클래스 인스턴스만 갖도록 보장하는 패턴
  • 전역 접근을 제공
    • 코드를 이해하기 힘들게 만들고, 커플링을 조장함
  • 싱글턴 상속: 여러 플랫폼을 지원해야할 때, 플랫폼 별로 클래스를 만들 수 있음
  • 정적 플래그를 써서 인스턴스 수를 제한할 수 있음

상태

  • 객체 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴
  • finite state machine
  • 가질 수 있는 상태는 한정되며, 입력이나 이벤트에 의해 변경됨
  • 계층형 상태 기계: state machine의 각 상태는 상태를 상속할 수 있음

2. 순서 패턴

이중 버퍼

  • 그래픽스 랜더링에서 사용
    • 하나의 버퍼를 쓰는 시간이 오래 걸리는 상황에서, 언제나 온전한 버퍼를 필요로 함
  • 순차적으로 변경해야하며, 변경 도중 접근이 가능해야하는 상황에 용이
  • 한 프레임에 가해진 상태가 그 프레임동안 유지되어야 하는 상황에 용이
  • 작업 중인 상태에는 접근할 수 없어야함
  • 교체에 비용이 들고, 메모리가 배로 필요

Game loop

int App::OnExcute() {
  if (OnInit() == false) {
    return -1;
  }

  SDL_Event event;
  while (isRunning) {
    while (SDL_PollEvent(&event)) {
      // 이벤트 처리
      OnEvent(&event);
    }
    // 게임 업데이트
    OnLoop();
    // 렌더링
    OnRender();
  }
  OnCleanUp();
  return 0;
}

🔗 위의 코드에 대한 추가 설명(SDL 게임 개발)

  • 한 번 Loop를 돌 때마다 유저 입력을 처리한 뒤 게임 상태를 업데이트, 화면을 렌더링하는 루프
  • HW에 상관없이 일정한 속도의 실행을 보장해야함
  • 전 프레임에 걸린 시간에 따라 다음 프레임 시간을 유동적으로 조정할 수 있음
    • 실제 시간에 기반하여 연산함
  • 시간 간격에 예민한 렌더링만 고정 프레임 시간을 사용할 수 있음
    • 렌더링 타이밍과 게임 업데이트 타이밍을 잘 비교해야함

Update method

  • 각 객체에 프레임 단위의 작업 진행하라고 알림
  • 가변시간 Loop를 사용한다면 작업 진행과 렌더링에 걸린 시간 간격을 알려야함
  • 유니티의 MonoBehaviour에서 사용함: MonoBehaviour
    • Update: 한 프레임 간격으로 실행됨 (TIme.deltaTime: 전 프레임 업데이트 후로부터 지난 시간)
    • FixedUpdate: 고정된 프레임 레이트 간격으로 실행됨 (시간이 고정됨)

3. 행동 패턴

Byte code

  • virtual machine 명령어를 인코딩한 데이터
  • 행동을 데이터로 정의하고 코드는 그것을 읽기만 하는 방식
  • 행동을 스택으로 관리하여 실행하는 방식이 있음

하위 클래스 sandbox

  • 상위 클래스가 제공하는 기능을 통해서 하위 클래스 행동 정의
  • 상속 이점
    • 하위 클래스들의 겹치는 행동을 공유할 수 있음
  • 초기화 상위 클래스 상태를 하위 클래스로부터 지키고 싶은 경우
    • 생성자에 상위 클래스 초기화에 관련된 인자를 넘기지 않고 별도의 Init 메소드를 추가함
    • 모든 클래스에서 같은 상태를 쓴다면 정적 멤버로 만들면 쉬움
    • 직접 생성자에서 접근하여 참조할 객체를 가져올 수 있음

type object

  • 타입 객체 인스턴스는 논리적으로 서로 다름
  • 타입 사용 객체는 자신의 타입을 나타내는 타입 객체를 참조함

4. 디커플링 패턴

Component

  • 커플링을 줄일 수 있는 패턴
  • 컴포넌트는 하나의 분야를 담당하고, 한 개체는 여러 컴포넌트를 사용함으로 여러 일을 커플링없이 처리할 수 있음
  • 컴포넌트 끼리의 통신
    • 컨테이너 객체를 거쳐서 전달
    • 컨테이너에게 메시지를 전달하면, 컨테이너가 모든 컴포넌트에 메시지 전달

Event queue

  • 이벤트를 보내는 시점과 처리하는 시점을 디커플링함
  • 단순 호출부와 실행부 분리는 옵저버 패턴으로 커버 가능
  • 원형 버퍼에 저장하면 큐 앞에서부터 순차적으로 이벤트를 가져올 수 있어 좋음

service provider

  • 일종의 싱글턴 패턴
  • 인스턴스가 요청되는 순간에 반드시 인스턴스를 가지고 있어야함
    • null인 경우 반환할 아무 일을 하지 않는 null 객체를 반환
  • 로그 데커레이터
    • 서비스 제공자를 필요에 따라 래핑하여 사용
    • 비활성화를 원하는 기능은 널 객체로 서비스를 전환시켜두면 됨

5. 최적화 패턴

데이터 지역성

  • CPU 캐시 히트를 최대로 하기 위해 적절한 자료구조 선정이 필요함

더티 플래그

  • 값이 전과 다름을 알리는 플래그
  • 상위 상태에 의존적인 상태를 가지고 있는 하위 상태의 값 계산을 뒤로 미룰 수 있음
    • 하이라키상 가장 상위에 있는 객체의 위치가 바뀌면 상위 객체부터 하위 객체까지 반복적인 위치 계산이 이루어짐 -> 낭비

객체 풀

  • 반복된 할당 해제로 인한 메모리 단편화를 방지하기 위함
  • 미리 사용할 객체를 생성하고 필요에 따라 객체를 제공하는 패턴
  • 사용 중이지 않은 객체가 메모리에 남아있음