스터디 그룹

스터디 한 것

게임엔진 아키텍처 3판 3장 - “게임을 위한 소프트웨어 엔지니어링 기초” 중

에러 감지와 처리

에러의 종류

  • 크게 두가지가 있음
    • 사용자 에러 : 사용자가 뭔가 잘못해서 나는 에러
    • 프로그래머 에러 : 프로그램의 잘못된 버그
  • 사용자 개념은 상황에 따라
    • 플레이어
    • 게임 제작자(기획, 아트)
    • 프로그램(미들웨어)을 사용하는 프로그래머

에러처리

플레이어 에러 처리

  • 시각, 청각적 효과로 알림

개발자 에러 처리

  • 에셋을 빨리 발견해 수정 or 에러를 대비한 예외처리 코드
  • 저자는 두 가지 절충 하란다

프로그래머 에러 처리

  • 가장 좋은 방법은 에러 감지 코드를 소스 곳곳에 넣어서 프로그램을 멈추게 하는 것 -> Assertion System
  • 모든 에러를 다루는데 Assertion을 사용 할 수는 없음

에러 감지와 에러 처리 구현

에러 리턴 코드

  • 널리 쓰이는 기법
  • 처음 에러를 감지한 함수에서 특정한 에러 코드를 리턴하게 하는 방법
  • TRUE/FALSE or Sentinel Value

예외 처리

  • 리턴 코드의 단점 - 해당 에러 코드를 에러 감지한 함수에서 처리 할 수 없는 경우에는?
    • 에러 코드 처리 위치가 main 함수에 위치 해 있고 에러 발생한 함수의 콜 깊이가 40개 넘는다면..
  • Throw exception - 어떤 함수가 처리할지 전혀 신경 안쓰고도 에러 전달 가능
  • 대략 적인 순서
    1. 예외를 던지면 예외 객체(프로그래머가 정의 가능) 자료구조에 추가
    2. try-catch 블록에 도달할 때까지 콜 스택을 자동으로 펼침
    3. try-catch 블록 찾으면 예외 객체를 처리할 수 있는 catch 블록을 찾아 실행
  • 단점은 프로그램 성능 저해
    • try-catch 블록을 포함하는 모든 함수의 스택 프레임에는 스택 펼치기(stack unwinding)를 위한 별도의 정보를 추가 해야 함
    • 프로그램 단 한 부분에서만 사용해도 프로그램 전체를 예외 처리 사용하게 설정 해야 함
    • 예외 사용하는 라이브러리 사용하면서 게임 코드는 예외 처리 사용 안하려면?
      • 격리
      • 해당 라이브러리 API 호출을 새로운 함수로 감싸고 이 함수들이 구현된 번역 단위에는 예외 처리를 활성화
      • 이 함수들은 발생 가능 한 모든 예외에 대해 try-catch 블록으로 감싸 처리 그 결과로 에러 코드 리턴
  • 제일 문제는 goto 와 별반 다를 게 없으며 더 나쁠지도 - 조엘 스폴스키
  • 모든 예외 상황을 고려하고 맞게 처리 하지 않는다면 비정상적인 상태에 빠질 가능성이 있음
  • 컴파일러는 코드 크기를 증가 시키고 이로 인해 캐시 성능이 떨어 질 수 있고 인라인 처리 될 것도 안될 가능 성이 있음
  • 게임 엔진 단에서는 예외처리리를 사용하지 않는 것이 유리

Assertion

  • 어떤 표현을 검사하는 코드 : 표현이 거짓일 경우 프로그램 중단 후 메시지 출력
  • 프로그래머가 가정한 조건을 점검하는 역할
  • 버그 발생시 확실하게 알려주도록 하여 이 타이밍에 고치는게 최적
  • 개발단계에는 별 문제 없지만, 릴리즈에서는 성능을 위해 코드를 제거
    • 내 assert() 매크로는 DEBUG 전처리기 심볼 유효 시 동작
  • 엔진에서는 좀 더 세밀하게 조절 필요
    • UNREAL -> Debug / Debug Game / Development / Test / Shipping
구현
#if ASSERTIONS_ENABLED
   #define debugBreak() { __asm int 3; } // CPU 마다 다를 수 있음
   #define ASSERT(expr) \
   { \
      if (expr) {} \
      else { \
         report(#expr, __FILE__, __LINE__); \
         debugbreak(); \
      } \
   }
#else
   #define ASSERT(expr)
#endif
  • 코드에 Assertion 을 적극 사용하는 것은 좋음
  • 잘 사용하는 것도 매우 중요
    • 실패하면 무조건 실행 정지
    • 치명적 에러를 잡는데 사용해야 함
컴파일시
  • 컴파일 타이밍에 에러 검출
  • C++11 부터는 static_assert() 지원
  • 지원이 안될 경우는 직접 구현
#define _ASSERT_GLUE(a, b) a ## b
#define ASSERT_GLUE(a, b) _ASSERT_GLUE(a, b)

#define STATIC_ASSERT(expr) enum { ASSERT_GLUE(g_assert_fail_, __LINE__) = 1 / (int)(!!(expr)) };

STATIC_ASSERT(sizeof(int) == 4);   // 성공
STATIC_ASSERT(sizeof(float) == 1); // 실패
  • 템플릿을 활용한 구현
#define _ASSERT_GLUE(a, b) a ## b
#define ASSERT_GLUE(a, b) _ASSERT_GLUE(a, b)

#ifdef __cplusplus
   #if __cplusplus >= 201103L
      #define STATIC_ASSERT(expr) static_assert(expr, "static assert failed:" #expr)
   #else
      template<bool> class TStaticAssert;
      template<> class TStaticAssert<true> {};

      #define STATIC_ASSERT(expr) enum { ASSERT_GLUE(g_assert_fail_, __LINE__) = sizeof(TStaticAssert<!!(expr)>) };
   #endif
#endif

STATIC_ASSERT(sizeof(int) == 4);   // 성공
STATIC_ASSERT(sizeof(float) == 1); // 실패