Game Engine Support System 1

SYiee·2023년 1월 3일
0

게임엔진

목록 보기
5/5
post-thumbnail

Introduction

이 챕터에서는 게임 엔진 subsystem 이 무엇이며 이를 구현하기 위해서 C++ static oeder가 어떻게 이루어지는지 학습합니다.

Subsystems and Their Initialization

Subsystem (start-up and shut-down)

  • Subsystem 끼리 엄청 연결이 많다
  • start-up을 할 때 specific order에 따라 해야한다
  • shut-down을 할 때도 순서를 지켜 주어야 하고 보통 specific order에 반대.

C++ static initialization order

C++ static initialization order

  • c++에는 global static object가있음

    → 프로그램이 시작되기 전에 main이 불리기 전에 먼저 만들어짐
    → deconstructor은 main이 return되고 불린다
    → static 이나 global object로 subsystem을 구현하는게 desirable하지 않다

  • subsystem은 어디서든 접근이 가능해야 한다.

    → 어디서든 바라보게 만들기 가장 좋은건 싱글톤으로 만들어서 static으로 만드는 것이다.
    → 그런데 start-up 이나 shut-down을 global object로 만들면 문제가 생긴다
    →main이 불리기 전에 start-up 이나 shut-down main()이 불리고 main이 return되면 해제가 되는데

    1. 무조건 main 함수에 접근하기 전에 선언하는 형태가 된다.

      subsystem을 필요로 하는 순간부터 만드는 것이 아니라 main함수에 접근하기 전에 그냥 만들어 주어야 한다. 필요하지 않은 subsystem이 먼저 선언이 되어 버린다. 또한, 중간에 필요없어지면 꺼야 하는데 main이 끝나야 언리얼 엔진이 끝나야만 해제가 가능하다.
      These behaviors are not desirable for initializing and shutting down the subsystems of a game engine.

    2. 싱글톤 클래스를 이용하면 어디서든 접근이 가능하기 때문에 이것을 이용해 subsystem을 구현을 한다.

      • construction and destruction order을 조종할 수 없다. 컴파일러가 알아서 한다.
      • trick을 사용한다 → global함수를 main 이 불리기 전에 만들지 마라 라고 바꿀 수 있다. first invocation of that function 처음 사용하려고 하는 순간 subsystem을 global하게 선언한다

✅ Example1

  • 싱글톤 패턴 사용
  • RenderManager → VideoManager 그런데 출력 결과를 보면 예상과는 달리 VideoManager 가 먼저 생성 (랜덤하게 바뀜)

✅ Example2

  • static 선언을 get() 함수 안으로 집어 넣어서 함수가 호출될 때 만들어지도록 함.

✅ dynamic allocation

✅ 한번만 호출해도 되도록

  • Rendermanager에서 VideoManager도 호출해버리면 main에서 Renfermanger하나만 호출해도 둘 다 생성된다.

C++ static destruction order

  • 생성자에 때려박으면 다른 사람들이 보았을 때 따라가기 어렵다.
  • main함수에 때려박는 방식도 있긴 하다

global priority queue

  • 간지가 나기 위해 더 복잡한 것을 만듬
  • 이 큐를 통해 초기화를 할 수 있다!
  • queue를 미리 설정해서 만들어 두면 subsystem을 넣어서
  • write some code to calculate the optimal start-up order
  • 함수를 통해서 우선 순위를 결정을 해준다.

Memory management

Stack allocation vs. Heap allocation

Stack allocation

  • 함수가 호출되었을 때 메모리에 어느정도 사용할지를 컴파일러에게 알려주고 사용이 끝나면 바로 deallocate를 해준다.

  • 스택에 스택에 사용하는 코드들을 우리가 고민할 필요가 없다.

  • 프로그래밍을 할 때 new 라던가 meloc 같은 걸 이용해서 정확리 어딘간에 무엇을 쓰겠다라고 이야기한 것 이외에는 전부 stack에 저장된다.

  • 임시 메모리 할당으로도 알려져 있다. Temporary memory allocation

    왜냐하면 실행이 다 끝나고 필요 없어지면 자동으로 flushes out(제거하다 쫓아내다) 되기 때문이다.

  • 데이터가 없어지는 것이 아니라 stack pointer의 위치를 그냥 조정해주는 것이기 때문에 deallocation한다고 해서 데이터가 없어지는 것이 아니라 그냥 포인터르 ㄹ위로 올리는 것이다. 그래서 heap이 올라갈 때 위에 있는 남아있던 stack과 만날 수 있다. 그래서 이것을 악용해서 해킹을 하거나 에러가 나는 원인이 될 수 있기 때문에 이 가운데를 잘 관리 해줘야 한다.

Heap allocation

  • heap에서 데이터를 만듬. new 이것에 대한건 heap에 저장이 됨. 이것에 대한 referencing data는 stack에 저장이 됨.
    어떤 캐릭터 value, name은 다 heap에 저장이 되어 있고 이것에 접근을 해야한 referencing data는 다 stack에 저장이 된다.
  • heap메모리는 스택만큼 안정적이지 않다. 여기에 저장이 된 것은 모든 스레드에 보여지고 접근이 가능하기 때문
    heap 메모리는 쓰려고 했던 heap메모리 넘어서 접근할 수 있기 때문 ex 5개 array인데 6번째 인덱스에 접근
  • heap 메모리를 제대로 관리하지 않으면 메모리 누수가 발생 → 힙에 얼마 쓰겠다고 하고 그냥 안쓰는 형태로 두면 영원히 그 영역을 차지하고 있는 상태가 됨. 그렇게 힙 메모리가 위로 올라오다 보면 스택이 만나게 되고 이제 우리 메모리 없으니까 그만써 하고 프로그램 꺼버린다.

Memory management and game performance

  • 메모리 영역을 어떻게 관리하느냐에 따라 성능이 달라진다.
  • 메모리에 접근을 하려면 어떠한 권한을 얻어야 한다.
  • 메모리를 할당하고 지우는 것이 cost가 발생하게 된다.

Memory management and game performance

  1. Dynamic memory allocation via malloc() or new() 메모리에 무언가 쓰겠다
    • 매우 느린 operation 이다. 커널에 요청을 해서 써야 하기 때문
      → Dynamic memory allocation 이 느리니까 쓰지 말자
      → custom memory allocators 을 사용해서 allocation cost를 줄이자.
  2. 최근 CPU들은 메모리 접근을 어떻게 하느냐에 따라 소프트웨엉 성능이 많이 결정된다.
    메모리 영역에서 옆을 길게 붙어있는 것을 접근하는게 더 효율적이다. 연속된 메모리엥 접근하면 성능적 이득이 있다.

Dynamic memory allocation

  • Dynamic memory allocation via malloc()-free() or new()-delete() operators is typically very slow.
  1. heap allocator 은 general-purpose facility
    general-purpose facility :일반적으로 소프트웨어를 만들 때는 대다수의 환경에서 잘 작동하도록 만드는 것이 중요한데 게임은 어느정도 적립된 형태 내에세서 최선의 퍼포먼스를 빼내려고 만든 것이기 때문에 general purpose 보다는 최선의 purpose를 뽑아내고자 함
    사용자가 어떻게 메모리를 할당할지 모르기 때문에 이것을 관리하는데 많으 오버헤드가 발생한다
  2. user mode와 kernel mode로의 context switch가 필요하다
    → os에게 요청을 해서 kernel모드로 전환해서 ~~
    → These context switches can be extraordinarily expensive

void *malloc(size_t size)

  • 사이즈 = 0 → 아무것도 안한다

  • 실패 - 0을 리턴하고 error

  • memory fracmantation이 발생

→ 링크드 리스트처럼 비어있는 chunk들을 연결해 사용할 수 있지만 너무 비효율적이다.

  • A custom allocator can have better performance characteristics than the operating system’s heap allocator for two reasons:
    1. pre-allocated memory block
      This allows it to run in user mode and entirely avoid the cost of context-switching into the operating system.

      • 투기장에 들어오는 인원이 대략 10명 정도 될테니 나는 이 10명 정도에 해당하는 데이터를 미리 heap에다가 올려두고 써야지.
        → 캐릭터 a가 들어올 때, b가 들어올 때마다 다이나믹 할당을 안해주어도 되고 처음 한 번에 빵 해주면 된다.
    2. 사용 패턴을 알고 있으면 general-purpose heap allocator에 비해 훨씬 효율적이다

Stack-based allocators

  • 많은 게임에서 스택에 기반한 allocation을 사용한다.

  • Whenever a new game level is loaded, memory is allocated for it. Once the level has been loaded, little or no dynamic memory allocation takes place.
    한 번 로드하면 추가적인 로드는 필요가 없다.

  • 레벨이 끝나는 순간 다 해제해버려도 문제가 없다.

  • A stack allocator is very easy to implement.

  • 맨 위에 해당하는 것을 설정함

  • 메모리를 해제할 때 그냥 포인터를 위로 올려 버린다.

  • The top pointer is initialized to the lowest memory address in the stack

  • 게임에서 사용할 애들이 할당이 되면 새로 meloc을 하는 것이 아니라 그냥 미리 MELOC이 된 데이터에 쭉쭉 집언 허고 포인터를 내려 버린다.

  • Memory cannot be freed in an arbitrary order.
    메모리를 중간에 있는 것을 지울 수 없다.

Double-ended stack allocators

  • 양쪽에서 쌓을 수 있다.

  • 두개로 나누어 조금 더 유연하게 쓸 수 있고 stack allocator와 같은 장점을 가짐

  • Midway’s Hydro Thunder arcade game

    • The bottom stack is used for loading and unloading levels (race tracks)
    • The top stack is used for temporary memory blocks that are allocated and freed
      every frame.
  • 오른쪽에서는 레벨, 왼쪽에서는 캐릭터 넣깅

    -> 근데 스테이지랑 캐릭터만 구분하면 될까? 너무 간단... 요즘 게임은 변수가 엄청 많은데 겨우 더블 엔디드로 될까?

Pool allocators

  • It’s quite common in game engine programming to allocate lots of small blocks of memory, each of which are the same size.

    게임 프로그래밍에서는 같은 크기의 작은 사이즈의 메모리들을 계속 할당하고 해제한다. 굉장히 많고 작은 메모리들을 할당하는데 비슷한 사이즈를 갖는다ex) 자주 올렸다

  • A pool allocator works by pre-allocating a large block of memory whose size is an exact multiple of the size of the elements that will be allocated.
    프리 할당을 미리 해서 큰 메모리를 잡아두고 자주 사용할 것 같은 메모리 크기의 공간의 연속된 블락으로 구성된 라지 메모리를 잡아둔다.

    • 4×4 matric가 기본으로 필요하다. 이게 많이 쓰이니까 이걸 기준으로 메모리를 관리하면 편하겠다!
  • pool은 많이 사용하고 있지 않은 것들의 링크드 리스트 형태로 메모리를 관리

  • 할당을 하면 free 메모리로 연결이 되어서

  • 해제시키고 싶을 때는 할당의 과정 반대로 한다.

  • 단) 100바이트 단위로 잘라두었는데 데이터가 다 100보다 작으면 메모리 낭비가 되고 반대로 잘라둔 메모리의 크기가 데이터보다 작아도 비효율

Single-frame and double-buffered memory allocators

  • 게임루프에 항상 일시적인 메모리 할당이 필요하다.

  • 이게 매 프레임마다 사용할 수 도 있고 다음 프레임에서 버리고 사용안 할 수도 있다.

  • 이러한 할당 패턴은 너무 흔해서 게임 엔진이 single-frame and double-buffered allocator을 지원을 한다.

Single-frame allocators

  • 먼저 block of memory를 차지를 하고 있는 상태 만들고 stack allocator 사용
    → 일단 탑 포인터를 cleared 하게 함
    → 할당이 프레임마다 쌓인다
  • 장점
    • 할당된 메모리가 free 될 필요가 없다
    • 빠르다
  • 단점
    • 프로그래머가 레벨에 대해
      몇명이 들어올지 모르는 게임에서 사용할 수 없다 모든것 파악하고 있는 경우에만 사용이 가능하다.
    • 다시 쓸 수가 없다.
      어떤 메모리 블락에서 다음 메모리 블락 저장할 필요가 없어 근데 저장을 못하니까 접근도 못해

Double-buffered allocators

  • frame i에서 쓸 메모리와 frame i+1에서 쓸 메모리를 분리를 함.
  • two single-frame stack allocators 를 만들고
  • 옛날에 메모리가 부족했을 때는 못 썼지만 지금은 이걸 쓰는게 좋아짐

    현재 프레임에서 캐릭터 가 있는데 다음 프레임에 약간 왼쪽으로 이동함. 이 두개를 한 번에 클리어 해버리는것 보다 한 프레임 정도는 가지고 있는 것이 효율적일 때가 있다. 예를 들어 잔상 효과를 남기고 ㄱ싳으면 이전 프레임의 데이터를 기반으로 연상을 해야하는데 프레임이 남아 있으면 약간 투명하게 만들어서 덮어 씌울 수도 있다. 프레임을 다 지우면 눈에 순식간에 다 보일 수 있다.

  • 보통 왼쪽 위에서 오른쪽 아래로 픽셀이 좌라라 뜨는데 이전 프레임이 클리어되면 다음 프레임이 뜰 때 아무리 빨라고 오른쪽 아래에 약간 깜빡이는 것처럼 보일 수 있다. 그래서 이런것을 없애기 위해 사용함.
  • On frame (i+1), the buffers are swapped.
    active하지 않은 버퍼에 저장을 해두고 어딘가에 쓸수는 있다. 현재 프레임에서는 clear하진 않는데 다음 프레임으로 넘어가면 clear한다.

🖇 Reference

해당 포스트는 강형엽 교수님게임엔진기초 [GameEngine-22-2] 수업을 수강하고 정리한 내용입니다. 잘못된 내용이 있다면 댓글로 알려주시면 감사하겠습니다😊

profile
게임 개발자

0개의 댓글