-
Notifications
You must be signed in to change notification settings - Fork 10
/
0438.txt
202 lines (202 loc) · 17.1 KB
/
0438.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
예 안녕하세요. 포프입니다.
이 비디오는 제가 지난.. 포프 TV를 4년 5년을 해왔어요.
굉장히 초반에 만들려고 했던 비디오인데
그냥 밀렸어요.
그러다가 오늘에야 말하게 되는데
재가 이 패턴을 쓴 지가 벌써 좀 한 1~2년 정도가 된 거 같아서 재가 엔진을 처음부터 build 한 게
마지막 해본 게 한 1~2년은 된 것 같아요.
그래서 제대로 설명할 수 있을지 모르겠지만 노력을 해볼게요.
재가 예전에 interface 떡칠하지 말라는 그런 식의 비디오를 만든 적이 있어요.
그때 재가했던 예기가
야 Implementation이 하나면 왜 interface 하고 Implementation을 나누냐 그거는 그냥 하나의 class가 맞다.
interface라는 거는 기본적으로 다형성을 위해 있는 거다.
그 범위를 넘어가서 엉뚱한데 쓰기 시작하면은 굉장히 피곤해진다라는 얘기를 했었어요.
그래서 그거랑 약간 일맥상통하는 예기예요.
특히 이제 멀티 플렛폼용 게임 엔진을 만들 때
그런 문제가 많이 생겨요.
저희가 이제 그래픽 엔진을 재가 주로 만들었으니까.
그래픽 쪽이라고 얘기를 할게요.
그러면은 각 플랫폼마다 쓸 수 있는 API는 보통 하나 정도가 돼요.
물론 PC게임에서 OpenGL 하고 DirectX를 동시에 지원하는 게임들도 있지만 대부분은 이제 그냥 DirectX로 유일하게 가죠.
그러면 PC에서는 DirectX 써요.
Xbox에서는 또 Xbox용에 그래픽 SDK가 있어요. DirectX랑 비슷하지만 약간 다르죠.
PS PlayStation 가면은 사람들이 아 PlayStation OpenGL 지원하잖아요.
그러지만 사실은 PlayStation에도 자제 그래픽 SDK가 있어요.
지금 애플도 그렇죠. Metal 뭐 이런 거?
그러면 그러고 뭐 다른 게 뭐 있을까? 닌텐도(Nintendo)?
어... 스위치(Nintendo Switch) 스위치도 자체 그래픽 SDK가 있어요.
그래서 모든 플랫폼마다 자체 그래픽 SDK가 있어요.
그러나 이 그래픽 엔진을 제가 만드는 그래픽 엔진이 멀티 플랫폼에서 돌 때
게임 쪽에서는 각 플랫폼이 뭔지 신경을 안 쓰고 아. 이거 그려 저거 그려 이거 그려줘 뭐 이 텍스쳐 로딩해줘 이런 거만 하면은 되는 거잖아요.
그래서 추상화를 통해서
실제 나는 저 밑에 있는 플랫폼이 뭔지 모른 체 이거를 그려줘를 하는 것 만으로
이 데이터를 그려줘 그려달라고 해주는 것만으로도 그게 화면에 나올 수 있어야 하는 게 정상적인 방법이죠.
그럼 사람들이 일반적으로 생각할 때 이게 좀 약간...
생각은 있는데 아주 충분히 아직 익 익지 않은 프로그래머들 그래픽 프로그래머에 대한 생각 중에 하나가
어! 그래 interface는 똑같아야 하고 구현체는 여러 가지니까 그러면 interface 하나 만들고 Abstract class 정도가 되겠죠. 아니면 Parent class가 되거나
그러고 밑에 실체 구현체 DirectX OpenGL 리눅스 용으로 OpenGL 그리고
아 뭐 PS4 XboxOne 이렇게 구현체를 여러 개 만들면 되겠지 라고 interface 그리고 상속을 만들어서 실제 구현을 해요.
그러나 저는 이렇게 말하거든요.
어차피 compile 할 때는 한 플랫폼마다 한 Implementation compile 하는 거 아니냐고
내가 PC용 compile 하는데 PS4를 compile 할 일은 없잖아요. 같이
그러면 이거는 PC용으로 빌드할 때는 compile이 안 되는 코드죠. .
그러면 결과적으로 실행 도중에는
그 interface 하나당 구현체가 딱 하나만 있는 거예요
그러니까 코드 레벨에 코드를 처음 짤 때 아 이 코드를 처음 짤 때 interface를 강요하기 위해 이렇게 만들면 된다.
라고 생각을 했지만 실제 다형성이라는 거는 실행 중에 구현체가 몇 개가 있냐는 문제일 뿐이에요. 사실은
그 개념으로 봤을대는 이미 다형성을 제대로 쓰고 있지가 않은 거죠.
그리고 당연히 누구나 아시겠지만
그런 virtual 함수를 쓰고 다형성을 하려면 virtual 함수를 쓸 수밖에 없죠 가상 함수
그러면 이제 뭐 게임단에서 돌리기에는 그 정도는 해도 상관이 없는데 다형성을 써서 뭐 해도
그래픽 엔진 같은 경우는 한 함수가...
프레임 한번 돌 때 1천 번 2천 번 호출되는 경우가 되게 많아요.
그런 거를 이제 가상 함수로 만들어서 중간에 점프 테이블을 넣어가지고 한번 더 뛰어야 되면 메모리를
거기서 오는 저하가 게임 도는 시간에 10프로 20프로가 잡히는 경우가 20프로는 좀 구라다 10프로 정도가 잡히는 경우도 봤어요.
그래서 그런 가상 함수를 필요 없는데 넣는 거 자체가 그래픽 엔진 쪽에선 굉장히 큰 문제가 돼요.
그러면 이거를 어떻게 구현을 하느냐
이제 그 예기를 오늘 하고 싶은 거예요.
이제 정확히 이거의 이름이 무엇인지 모르겠어요.
이거에 대한 이름이 있는지 조차도 모르겠어요.
왜냐하면은 이거는 C++에서만 가능한 거거든요. 사실은 물론 C#에서도 이젠 가능할 거도 같긴 한데
뭐 C#에서 그렇게 다른 플랫폼용 다른 그래픽 엔진을 만드는 경우는 별로 없으니까
저는 패턴이라고 그냥 불러서 Reverse OOP패턴이라고 해요.
그러니까 OOP에서 OOP를 뒤집었다고 하는 거예요.
그러니까 반전된 OOP패턴이라고 하는데 이걸 어떻게 만드냐.
음...... 어 되게 귀찮은데 설명하기 보통은 이런 식으로 만들잖아요.
음...... interface를 위에 만들죠. 그래픽에서 한 가지만 예를 들죠. 그러면
draw 함수가 있다고 얘기를 하고 그죠. 그러면 제목을 지어야 될 거 같아요.
RendererBase라고 해놓고 class 이름을 함수 이름을 draw라고 정해 놓아요. 그러고 이거는 virtual 함수 일수밖에 없어요.
그러면 이 밑에 PCRenderer가 있고 그 PCRenderer가 이걸(RendererBase) 상속한 다음에 그죠?
draw 함수를 override를 하게 되죠.
그러면 이게 아까 재가 말했던 약간 좀 덜 익은 아...
프로그래머들이 만드는 방식이 그게 돼요.
그런데 이거를 이렇게 안 하는 방법이 있어요.
어떻게 하냐
일단 이거는 그냥 헤더 파일과 cpp파일이 분리되어 있다는 C++의 이제 단점이자 장점을 이용한 거예요.
그러면 어떻게 만드냐 일단 RendererBase를 RendererCommon으로 바꿀게요.
공통된 렌더러라는 개념이에요.
그러면 RendererCommon으로 하고 거기에 draw 함수가 이제 있어요.
가상 함수가 아니라 실제 그냥 함수로 void draw가 있고
그리고 거기에 이제 이거는 헤더 파일에 있는 거죠. 그러면 선언만 한 거예요. 선언만
그러고 이거에 대한 구현이 RendererCommon.cpp에 없어요.
그러면 어디에 두느냐 이거를 RendererPC.cpp안에 넣어 두는 거예요.
단 그 draw 함수를 이제 구현체를 넣을 때
원래 RendererPC.cpp를 보면은
원칙상은 그 안에 class가 뭐든지 RendererPC가 달리고 앞에 ::draw 넣어가지고 구현체를 만들어야 하잖아요.
그게 아니라 그 안에 RendererCommon::draw를 그 안에 구현을 하는 거예요.
그러면 PC를 빌드할 때는
아... RendererPC.cpp를 빌드하게 되잖아요.
그러면 거기서 아까 Common에서 선언을 해놓았던 draw 함수가 거기서 보이기 때문에
compile이 compile이 나고 무리없이 넘어가요.
그리고 PC가 아니라 XboxONE용으로 또 draw 함수가 들어 있겠죠.
그 안에도 똑같이 RendererCommon::draw 함수가 있지만
PC를 빌드할 때는 그 Xbox용이 빌드를 안 하기 때문에 아무 문제가 없이 넘어갈 수가 있는 거예요.
재밌죠.
그래서 이런 식으로 구현을 하면은 상속이...
그러니까 가상 함수를 안 쓰고 상속을 받아가지고 만들 수 있는 방법이 생겨요.
그러면 상속은 왜 그래도 필요하냐
RendererCommon이 있지만 그 안에 함수 지금은 얘기를 했어요.
하지만 이제 렌더러에서 RendererPC에서 돌리려면은 뭐 Device도 있어야 하고 Context도 있어야 하고 이게 그 플랫폼 전용의 데이터 타입이잖아요.
그런 데이터 타입을 정의해야 하는데 Common에 넣을 수는 없다는 거죠.
그러면 이 데이터 타입은 어디에 있냐 이 데이터 타입은 RendererPC.h에 들어가요. 그렇죠?
이건 어차피 PC용으로 빌드하는 거기 때문에 상관이 없어요.
그런데 여기서 재밌는 게 뭐냐면
지금까지 예기를 해온 건 앞에서 얘기를 했던 RendererCommon.h파일에 draw 함수를 넣고
RendererPC.cpp파일에 draw 함수를 구현한다가 전부였어요. 그죠?
그런데 정확히 상속관계가 어떻게 되는지 예기를 안 했어요.
일반적으로 저희가 생각할 때는 아까 가상 함수일때처럼
Common이 위에 있고 부모를 있고 그 밑에 RendererPC가 상속을 받아야 하는 것 같잖아요.
그런데 이렇게 하면은 안돼요.
왜 안 되냐
아까 말했던 Context가 있잖아요.
그... 뭐 Device라던가 ContextPC용에만 들어가는 그런 데이터 타입
내가 draw 함수를 호출을 했는데 이 draw 함수 호출 구현을 RendererPC.cpp에서 하지만
여전히 소유하고 있는 class는 앞에 :: 박히 그 class RendererCommon이에요.
그래서 여기서 Device를 호출할 방법이 없어요.
왜냐하면은 Device는 그 자식이 되는 RendererPC에 들어가는 거니까.
그래서 상속관계를 뒤집어 줘야 돼요.
이게 재가 Reverse OOP라고 말하는 이유예요.
그럼 다시 정리를 하면 어떻게 되는 거냐면
RendererPC.h파일에 뭐 DeviceContext 이런 게 데이터 멤버로 들어가 있어요. h파일에 , 그렇죠?
그리고 RendererPC.cpp파일에 그 뭐 데이터를 뭐 초기화해주거나 이런 것들이 어느 어딘가 함수에 들어가 있느거에요.
그리고 그 class는 당연히 RendererPC class여야만 하는 거고 그죠.
자 그럼 여기 있어요. 그럼 Common이 그죠?
Common이 RendererPC를 상속을 받아요.
물론 그래서 RendererPC를 상속을 받아야 되고
뭐…
음…..
Renderer… 뭐 Xbox를 상속받아야 되고 이래야 되잖아요. 플랫폼마다 그래서 요 부분만은 #ifdef가 들어갈 수밖에 없어요.
상속을 할 때 #ifdef를 만들거나 아니면 플랫폼 렌더러라고 만들어서
플랫폼 랜더러가 아 뭐 뭐 #define이 된 게 PC에서는 PC RendererPC 아 Xbox에서는 RendererXbox 이런 식으로 하면 되는 거예요.
그러면은 결과적으로 렌더러 Common이 상솓을 받는 애는 플랫폼 랜더러 실제로는 어느 플랫폼을 빌드하냐에 따라 PC가 될 수도 있고 Xbox 될 수도 있다.
그러면은 RendererCommon은 아까 말했던 Device나 Context에 대한 접근 권한이 있죠.
protected 아 Access Modifier가 되어 있다는 가정하에 접근권한이 있어요.
그러면 아까 말했던 draw 함수 draw 함수는 이 함수…..
cpp 아니 함수 구현체는 RendererPC.cpp안에 있지만
실제 이거를 가지고 있는 class는 RendererCommon이잖아요. 그죠.
그럼 그 RendererCommon에서 어 나 Device 써가지고 그려야 되는데 그래서 mDevice 뭐 SetVertexBuffer 이런 거 할 때 mDevice에 접근할 수가 있느거에요. 왜?
Common은 아까 말했던 pc의 자식이 되니까
그래서 이런 식으로 구조를 하면은 그리고 뭐 모든 함수를 그렇게 구조를 할 수밖에 없죠. 구축을
이런 식으로 구축을 하면은 아..
아까 말했던 가상 테이블? 이런 게 필요가 없이 음….
이런 식으로 예.
그냥 compile시에 모든 거를 부모 자식 관계?
그러나 가상 함수 없이 빌드가 되는 거고
그리고 실제 호출할 때도 가상 함수를 써가지고 뭐 이렇게 느려지는 점프 테이블을 쓰면서 두 번의 Indirection 이런 게 없으면서
음…. 성능.. 그런 게 있어서 오는 성능 저하도 없어지기 때문에
그냥 손으로 처음부터 코딩 짜는 거랑 크게 다를 바가 없는 결과가 나오는 거죠.
그러…엄.~~
그럼 손 코딩과 다르지 않지만
장점은 OOP로 구조를 쌓았기 때문에 관리가 좀 편해진다는 것도 있고
그리고 원래부터 Polymorphism(다형성) 제대로 써가지고 뭐 가상 함수 써가지고 하려고 했던 것처럼 interface에서 강제가 되는 거죠. 왜냐.
내가 RendererCommon에 draw 함수를 만들어 놨어. 그렇죠?
근데 그 구현체가 PC에만 있잖아요.
Xbox용으로 만드는데 그 Xbox파일 안에 그거를 또 구현을 안 해버리면 compile 자체가 안되잖아요.
구현체가 없으니까 그래서 compile시에 이거를 강요하는 그 입장에서는
interface에 똑같은걸 강조하기 위해 썼던 그런 가상 함수 체계 그런 게 그래도 enforce(강제)가 되는 거죠.
그래서 실제 멀티플랫폼용으로 엔진을 만드는 회사가 되게 많잖아요. 북미 쪽에는 여전히
그런 회사에서는 이런 구조를 되게 많이 써요.
EA 쪽에서도 썼고 캡콤에서도 썼고 생각보다 멀티플랫폼을 좀 덜 만들었던 한국에서는 이런 고민을 할 필요가 없었던 거 같은데.
저는 심지어는 이게 예전에 모바일 쪽 엔진을 직접 만들 때도 요즘은 유니티를 많이 쓰지만 예전에 만들 때도 이방식으로 해서 많은 부분을 좀 해결해 나갔던 적이 있어요.
그래서 뭐 지금 과연 유용성이 얼마나 될지 모르겠지만
Reverse OOP 패턴은 생각보다 게임업계에서 많이 썼고
그에 비해 뭐 패턴 책이라던가 나오지 않았던 이유가
패턴 책을 주로 만들었던 사람들은 Java 쪽 사람들이었다고 생각을 하고 옛날에 OOP...
OOP에 추상화가 만능이라고 생각했던 잘못된 생각을 하셨던 분들에 문제였고
그 사람들이 C++에서만 어찌보면은 가능한 이런 부분이 할 수는 없는 거죠.
그냥 여담으로 끝내기 전에 이제 아까 처음에 예기할 때 C#으로는 가능할 수 있다는 얘기를 했던 이유가 뭐냐면
C#에 있는 Partial class라는 게 있어요.
class 이름은 하나인데 class를 여러 파일로 나누는 법이 있고
그리고 각 C#은 자바나 이런 거하고 다르게 compilation switch라고 해서
아… 그 C++에 보면은 #ifdef 해가지고 아예 compile을 안돼게 코드를 빼는 방법이 있잖아요.
그거를 C#에도 할 수가 있어요.
그래서 Renderer를 하나 만들어 두고
그.. 그 Renderer에 함수들을 이제 선언 부분 자체를 아예 다 각 플랫폼별로 뽑아버리면
그리고 내가 어떤 플랫폼용을 빌드하냐에 따라
그 #ifdef가 바뀌게 해놓으면 각 파일마다 그러면 pc용으로 있던 RendererPC에서 draw 함수가 빠졌어
Xbox용으로 이제 drawPC를 하려.. draw를 하려고 그러는데 어 거기 아직 그 함수를 안 만든 거야
그리고 이미 그거를 호출하는 코드가 어딘가에 있겠죠. 그럼 그 순간에 이 함수가 없다고 compile 에러가 날수밖에 없죠.
그래서 그런 식으로 약간은 이제 강제하는 법은 있어요.
Partial class가 있고 아까 그 파일 전체 단위를 그냥
#ifdef 해서 빼버리는 법이 있기 때문에 그래서 C#도 가능은 하지만
뭐 C#에서 멀티플랫폼 돌려봐야 몇 개나 돌린다고?
자마린(Xamarin) 돌릴 때 안드로이드 ios 뭐 윈도우스용 뭐 이런 거 할 때는 저도 그런 좀 쓰기 썼죠.
Partial class 해서 하는 거 근데 게임 쪽에서는 별로 없지 않을까? 라고 생각을 해요. 음…
그리고 뭐
자마린 쪽에서 만드는 게 보통 엡이라고 생각하면 게임이 아니라 뭐 가상 함수 좀 떡칠한다고 해서 크게 문제가 생길 것도 없을 수도 있고
그래서 그래픽 엔진 쪽 멀티플랫폼 만들 때 음…
이런 쓸데없는 가상 함수 호출에 대한 오버해드를 피하기 위해서 그리고 OOP적으로도 말이 안되니까
그 말도 안 되는 Reverse OOP패턴을 썼던 것들 경험들?
그런 걸 그냥 말하고 싶었고 멀티 플랫폼으로 개발하시는 분들은 한 번쯤 생각해 볼만 해요.
내가 과연 실행 도중에
뭐 PC를 예를 들면 실행 도중에 OpenGL과 DirectX를 그렇게 바꿔서 할 일이 있는가
차라리 exe파일을 두 개 만드는 게 훨씬 낮지 않은가 compile 두 번 해가지고 주면 되는 거니까
그런 생각을 하면은 그냥 재가 말했던 패턴으로 가고
Game.exe Game.OpenGL.exe 두 개를 exe파일 compile 따로 해서 주는 게 훨씬 낮죠.
그래서 음…..
네 그 정도면 될 거 같아요. 그래서
지금은 잘 안 쓸지 모르지만 한때
재 삶에 활력을 줬던 그런 패턴? Reverse OOP 패턴
정식 이름이 있으면 알려주세요.
저는 정식 이름을 들어본 적도 없어요.
예! 포프였습니다.