센로그

5. Game Engine Support System (1) 본문

게임/게임 엔진 기초

5. Game Engine Support System (1)

seeyoun 2022. 12. 8. 03:34

◆ Subsystem

모든 게임 엔진은 굉장히 많은 subsystem들이 합쳐져서 하나의 엔진으로 구성되어 있음!

 


◆ Subsystem 초기화 순서

subsystem들이 켜지고 꺼질 때 순서 중요함

sub1이 sub2에 의존하고 있다면,
sub2 → sub1로 켜고, sub1 → sub2로 끔.

게임 실행되면 물리엔진, 리소스관리, DB관리, 메모리관리가 같이 실행되어야함

근데 메모리관리 시스템을 로드하기 위해선 데이터 구조체나 수학 연산 이런애들이 같이 로드되어야 함

그러면 순서가!

Dependency가 있는 애들 먼저 로드가 된 후에, 메모리관리 시스템이 로드되어야 함.

서브시스템끼리 Dependency 있는거임. 얘의 정보를 이용해 얘를 가져와야하고

특정한 순서에 따라 얘를 로드해줌.

만약에 서브시스템 b가 서브시스템 a에 의존한다(a에 있는 걸 b사용하고 있다다)라고 한다면,

당연히 ab보다 먼저 초기화되어야 하겟죠?

반대로, 끌 때는 b를 먼저 꺼야함.

a를 먼저 끄면, 'a를 끄고 b를 끄기 직전' 상황에 ba에 접근하려 할 수 있기 때문.

 


▶ C++ static 초기화 순서

보통 이런 순서가 필요한 시스템들은 main()내에서 직접 순서대로 초기화해줌. 그러나...

static 변수는 main()에 오기 전에 먼저 선언됨
그리고 main()이 return 될 때 해제됨.

내가 헷갈렸던 포인트는, static 함수는 그렇지 않다는 것. 얘는 불리기 전까진 뭐 처리 안함.

 

subsystem들은 Depencency에 따른 순서대로 초기화 되어야 하기 때문에, 막 초기화되면 안됨.

그런데 static 변수의 경우, main 함수에 들어가기 전에 system에서 알아서 초기화해주기 때문에,

내가 얘네의 순서를 직접 정할 수 없어서 오류가 발생할 수 있음.

 

 

이 문제를 해결하기 위한 방법

function - static 방식 

 


▷ C++ static 초기화 순서 - 예시

 

Render Manager class → Video Manager class순으로 초기화해야 한다고 가정하자.

 

이때 두 클래스는 각각 static 멤버 instance를 갖고 있다.

따라서 두 static 멤버 변수는 모두 main() 실행 전에 초기화될 것이다.

때문에, 내가 원하는 순서대로 초기화할 수 없다.(오류 날 수 있음)

 


▷ 그럼 어떻게 해야 되느냐?

function - static 방식 

static instance를 만들 때, 멤버로 바로 선언하지 말고

멤버 function 안에서 선언하도록 함. (어차피 static 이므로 어디서 선언해도 값은 그대로 남아있음)

이렇게!

그리고 순서 바로 정해주고 싶으면 파란색 화살표 위치에서 바로 다음 클래스 함수 불러주면 됨

 

 

 

메모리 할당이 필요할 땐 아래와 같이 하면 됨~

 


◆ Subsystem Destruction 순서

subsystem들이 켜지고 꺼질 때 순서 중요함

죽이는 건 main() 끝날 때 순서대로 죽이면 됨~.

근데 일일이 순서 적어주고 나열해놓기 불편하잖아.

그래서 생각한 방법이 바로

Global Priority Queue~

 

우선순위 가중치 어느정도로 줄지만 결정하면,

생성자때 Que에 넣고 소멸할 때 빼고 하면 될 듯!

 

Fancy한 코드~

 


◆ Memory management

메모리를 할당하는 방법

죽이는 건 main() 끝날 때 순서대로 죽이면 됨~.

 

 


▶ Stack allocator

int a = 7;

연속적으로 임시 변수들 쭉 올리고, life time 끝나면 알아서 쭉 지우고

연속적인 메모리 공간을 차지하기 때문에, 우리가 stack 메모리에 접근해서 특정한 애를 지울 방법은 없음.

(a, b, c 순서대로 쌓았는데 b만 지울 수는 없음. 지울거면 통째로 지워야 함)

 

구현하기 쉽고, 이 메모리 공간이 사용되고 있는지 알기도 쉬움.


▶ Heap allocator

int a = new int(7);

Heap 메모리 공간에 직접 할당. 직접 골라서 지울 수도 있음.

 

메모리를 세로로 쓰고 있어서, 느림

 

Heap 공간은 공용 메모리 공간이라서, a도 접근할 수 있고 b도 접근할 수 있고 c도 접근할 수 있음.

모든 스레드에게 보여지고 접근 가능하기 때문에 Stack 공간에 비해 안정적이지 않음!

 

만약 c가 new로 할당하려고 했는데, 이미 다 정의가 되어있으면 

heap에 메모리 없어! 하고 죽어버림

 


◆ Memory management and game performance

malloc이랑 new는 user mode → kernal mode로 들어가서 처리한다음 다시 user mode로 돌아와야 함.
그래서 엄청 느림. 

 

 

ㆍnew() / delete()

- 앞으로 얼마나 공간을 쓸지 모르기 때문에, 알아서 일반적으로 할당함

 

malloc() / free() 

- 얼만큼 사이즈를 할당할건지 변수로 넘김.

이 변수가 얼마가 될지는 모르기 때문에, 어떤 변수가 들어오더라도 그거에 대한 메모리를 할당할 수 있도록 짜여져야 함.

 

 

app 단에서는 절대로 driver나 HW 단계로 코드를 통해 직접 명령할 수 없음.
해킹이나 오작동을 막기 위한 이유임. 항상 OS를 거치게 됨. 
그래서 OS에게, 나 이제 driver에게 명령할거야. 니가 좀 해줘! 라고 부탁하는 게 kernal mode임
kernal mode로 들어가는 과정이 context switch인데, 이 과정에서 리소스와 시간이 들어감.
malloc, free 할 때마다 user mode ↔ kernal mode 왔다갔다 하면서 시간이 소요됨.

 


◆ Memory Fragmentation

동적 할당의 문제점 - 메모리 파편화

메모리가 중간중간 조각나있어서, 분명 크기상으로는 존재하는데 할당 못 하는거.

마지막 줄에서, 

시스템 상으로는 3개의 공간이 존재하지만

서로 떨어져 있어서 못 넣는 상황 발생 ㅠㅠ

 


▶ Memory Fragmentation - Custom allocator

Heap 할당은 너무 느리고, 메모리 파편화 문제도 있고 .. 이를 해결하기 위해 custom allocator 만들어서 사용

처음에 미리 큰 공간을 받아놓고, kernel모드 전환 없이 직접 그 공간을 관리하는 것

 


▷ Custom allocator - Stack allocator

Stack-based allocator - stack allocator

스택 포인터를 조정해서 연속적으로 관리

맨 위를 가리키는 포인터를 만들고, 10바이트를 할당할거면 이 포인터를 10바이트만큼 내려줌

 

 

구현하기 쉬움

총 어느정도 사용되고 있는지, 이 공간이 사용되고 있는지 아닌지 알기도 쉬움

지우는 게 쉬움. (그러나 특정한 애만 지울순 없음)

 


▷ Custom allocator - Double-ended stack allocator

Stack-based allocator - Double-ended stack allocator

양쪽에서 스택 포인터를 조정해서 연속적으로 관리

내가 엄청나게 큰 메모리 공간을 사용하게 되는데, 그냥 stack allocator 사용하면

위에서부터 채우며 내려가니깐.. 언제 다 차? 끝까지 내려오질않고 아래에 있는 메모리공간이 자꾸 남음

, 내가 중간에 필요없는 애가 생겨서 지우고 싶어도, 걔만 지울수없고 걔 아래에 있는애들 다지워야함

 

→ 그래서 Double ended stack allocator. 끝을 두개 갖고있는거.

아래쪽은 레벨,씬 설정 이런거 넣고, 위에는 임시적으로 빨리빨리 쓰는 애들 넣어두곤함

 


▷ Custom allocator - Pool allocator

데이터를 모두 같은 크기의 메모리 블록들로 나눠서 linked-list처럼 이어서 관리

stack-based allocator은 청크 단위로 관리해야 하고, 처음 시작할 때부터 뭐가 큰지? 있는지? 다 알고 있어야 함.

그래서 이걸로만 하는건 좀 힘듦! 그래서 나온 게 Pool allocator

 

전체 메모리를 같은 사이즈 블록으로 나눔 → 데이터의 처음과 끝을 판단하기가 쉬움(사이즈 단위로 보면 됨)

그리고 linked-list 처럼 데이터들 이어줌. 중간에 지울수도 있음!

 


▷ Custom allocator - Single-frame and double bufferd memory allocator

두 프레임 간 버퍼 살아있음

게임은 루프를 도는 동안 이번프레임에서 그려진 데이터와 다음 프레임에서 그려질 데이터가 비슷할 수밖에 없음

그러므로 어떤 변수는 이번, 다음 프레임에 쭉 사용해야 되는데, Temporary로 저장해버리면 이게 지워져버릴 수 있음

이걸 막기 위해서 double bufferd allocator 사용.

버퍼를 두 개 만듦. (temporary 저장용)

그러면 한 프레임에서 버퍼 하나 쓰고, 지우지않고 그 다음 프레임에서 걔를 씀.

그 다음에 다시 돌아와서 지우고 다시 씀. 그럼 2개의 프레임동안 데이터가 살아있음(swap buffer)

프레임만에 지워지지 않고 두 개의 프레임에서 번갈아가면서 쓰는거. 이미지 처리 같은데서 많이 사용

'게임 > 게임 엔진 기초' 카테고리의 다른 글

7. Game Engine Support System (3)  (0) 2023.01.03
6. Game Engine Support System (2)  (0) 2022.12.08
4. Object-oriented programming (2)  (0) 2022.12.07
3. Object-oriented programming (1)  (0) 2022.12.07
2. Game Engine Architecture (2)  (1) 2022.12.07
Comments