이 챕터에서는 게임 엔진 subsystem 이 무엇이며 이를 구현하기 위해서 C++ static oeder가 어떻게 이루어지는지 학습합니다.
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되면 해제가 되는데
무조건 main 함수에 접근하기 전에 선언하는 형태가 된다.
subsystem을 필요로 하는 순간부터 만드는 것이 아니라 main함수에 접근하기 전에 그냥 만들어 주어야 한다. 필요하지 않은 subsystem이 먼저 선언이 되어 버린다. 또한, 중간에 필요없어지면 꺼야 하는데 main이 끝나야 언리얼 엔진이 끝나야만 해제가 가능하다.
These behaviors are not desirable for initializing and shutting down the subsystems of a game engine.
싱글톤 클래스를 이용하면 어디서든 접근이 가능하기 때문에 이것을 이용해 subsystem을 구현을 한다.
✅ Example1
✅ Example2
✅ dynamic allocation
✅ 한번만 호출해도 되도록
global priority queue
Stack allocation
함수가 호출되었을 때 메모리에 어느정도 사용할지를 컴파일러에게 알려주고 사용이 끝나면 바로 deallocate를 해준다.
스택에 스택에 사용하는 코드들을 우리가 고민할 필요가 없다.
프로그래밍을 할 때 new 라던가 meloc 같은 걸 이용해서 정확리 어딘간에 무엇을 쓰겠다라고 이야기한 것 이외에는 전부 stack에 저장된다.
임시 메모리 할당으로도 알려져 있다. Temporary memory allocation
왜냐하면 실행이 다 끝나고 필요 없어지면 자동으로 flushes out(제거하다 쫓아내다) 되기 때문이다.
데이터가 없어지는 것이 아니라 stack pointer의 위치를 그냥 조정해주는 것이기 때문에 deallocation한다고 해서 데이터가 없어지는 것이 아니라 그냥 포인터르 ㄹ위로 올리는 것이다. 그래서 heap이 올라갈 때 위에 있는 남아있던 stack과 만날 수 있다. 그래서 이것을 악용해서 해킹을 하거나 에러가 나는 원인이 될 수 있기 때문에 이 가운데를 잘 관리 해줘야 한다.
Heap allocation
Memory management and game performance
- Dynamic memory allocation via malloc() or new() 메모리에 무언가 쓰겠다
- 매우 느린 operation 이다. 커널에 요청을 해서 써야 하기 때문
→ Dynamic memory allocation 이 느리니까 쓰지 말자
→ custom memory allocators 을 사용해서 allocation cost를 줄이자.- 최근 CPU들은 메모리 접근을 어떻게 하느냐에 따라 소프트웨엉 성능이 많이 결정된다.
메모리 영역에서 옆을 길게 붙어있는 것을 접근하는게 더 효율적이다. 연속된 메모리엥 접근하면 성능적 이득이 있다.
void *malloc(size_t size)
사이즈 = 0 → 아무것도 안한다
실패 - 0을 리턴하고 error
memory fracmantation이 발생
→ 링크드 리스트처럼 비어있는 chunk들을 연결해 사용할 수 있지만 너무 비효율적이다.
pre-allocated memory block
This allows it to run in user mode and entirely avoid the cost of context-switching into the operating system.
사용 패턴을 알고 있으면 general-purpose heap allocator에 비해 훨씬 효율적이다
많은 게임에서 스택에 기반한 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.
메모리를 중간에 있는 것을 지울 수 없다.
양쪽에서 쌓을 수 있다.
두개로 나누어 조금 더 유연하게 쓸 수 있고 stack allocator와 같은 장점을 가짐
Midway’s Hydro Thunder arcade game
오른쪽에서는 레벨, 왼쪽에서는 캐릭터 넣깅
-> 근데 스테이지랑 캐릭터만 구분하면 될까? 너무 간단... 요즘 게임은 변수가 엄청 많은데 겨우 더블 엔디드로 될까?
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.
프리 할당을 미리 해서 큰 메모리를 잡아두고 자주 사용할 것 같은 메모리 크기의 공간의 연속된 블락으로 구성된 라지 메모리를 잡아둔다.
pool은 많이 사용하고 있지 않은 것들의 링크드 리스트 형태로 메모리를 관리
할당을 하면 free 메모리로 연결이 되어서
해제시키고 싶을 때는 할당의 과정 반대로 한다.
단) 100바이트 단위로 잘라두었는데 데이터가 다 100보다 작으면 메모리 낭비가 되고 반대로 잘라둔 메모리의 크기가 데이터보다 작아도 비효율
게임루프에 항상 일시적인 메모리 할당이 필요하다.
이게 매 프레임마다 사용할 수 도 있고 다음 프레임에서 버리고 사용안 할 수도 있다.
이러한 할당 패턴은 너무 흔해서 게임 엔진이 single-frame and double-buffered allocator을 지원을 한다.
현재 프레임에서 캐릭터 가 있는데 다음 프레임에 약간 왼쪽으로 이동함. 이 두개를 한 번에 클리어 해버리는것 보다 한 프레임 정도는 가지고 있는 것이 효율적일 때가 있다. 예를 들어 잔상 효과를 남기고 ㄱ싳으면 이전 프레임의 데이터를 기반으로 연상을 해야하는데 프레임이 남아 있으면 약간 투명하게 만들어서 덮어 씌울 수도 있다. 프레임을 다 지우면 눈에 순식간에 다 보일 수 있다.
해당 포스트는 강형엽 교수님의 게임엔진기초 [GameEngine-22-2] 수업을 수강하고 정리한 내용입니다. 잘못된 내용이 있다면 댓글로 알려주시면 감사하겠습니다😊