소개

게임 루프는 모든 게임의 핵심입니다. 어떤 게임도 게임 루프 없이는 실행될 수가 없습니다. 그러나 불행히도 새로운 게임 프로그래머에게 인터넷 상에서 이러한 내용이 적절하게 나와있는 좋은 곳을 찾을 수 없습니다. 그러나 두려워하지 마세요. 왜냐하면 게임루프에 주목해 볼만한 글을 방금 발견했기 때문에요.


나의 직업이 게임 프로그래머인 것에 감사합니다. 소형 모바일 게임들을 위한 많은 코드를 접하게 되었습니다. 몇 몇의 게임 루프들이 나를 놀라게 만들었습니다. 아마 당신도 게임 루프들이 어떻게 다르게 쓰여지는 것에 대해 궁금할것입니다. 가장 인기있는 게임 루프 방법에 대해 찬반토론을 하겠습니다. 또한 게임루프 실행의 최고 방법을 알려 드리겠습니다.( 제 생각으로요.)


(브라질의 포루투칼어로도 이용하게 해줄수 있게 한 Kao Cardoso Felix 감사드립니다.)

게임 루프
모든 게임들은 사용자의 입력, 게임의 상태 수정, AI 조작, 배경음과 효과음들의 연속으로 이루어져 있다. 이 연속된 동작들은 게임 루프를 통해 처리된다. 소개에서 말한대로 게임 루프는 모든 게임의 핵심이다. 앞서 언급한 작업에 대해 자세하게 설명하지 않고, 게임루프 하나에 집중하도록 하겠다. 그래서 작업들을 두개의 기능으로 단순하게 했다 : 게임 업데이트와 디스플레이

최고로 단순한 게임 루프 예제 코드:

bool game_is_running = true;
while( game_is_running ) {
update_game();
display_game();
}


이 단순한 게임 루프의 문제는 시간을 고려하지 않고 있다. 그저 게임을 실행할 뿐이다. 느린 하드웨어에서는 느려지고, 빠른 하드웨어에서는 빨라진다. 예전 하드웨어의 속도를 알고 있다면 상관이 없다. 그러나 오늘날 많은 하드웨어 플랫폼이 존재한다. 우리는 속도를 다른 어떤 방법으로 실행해야 한다. 여기에 많은 방법들이 있고, 이 방법들에 대해 이어지는 섹션에서 토의해 보자

첫째, 이 글에서 사용하는 두 단어에 대해 설명하겠다 :

FPS
FPS는 Frames Per Second의 약어이다. display_game()의 초당 호출 횟수이다.

게임 속도
게임 속도는 초당 게임 상태의 갱신 속도이다. 다른 말로는 초당 update_game()의 호출 횟수이다.

FPS가 게임속도에 의존적

구현
타이밍 문제를 해결하기 위한 쉬운 방법은 단지 초당 25 프레임으로 게임을 실행하면 된다. 코드는 다음과 같다.

const int FRAMES_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / FRAMES_PER_SECOND;

DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started

int sleep_time = 0;
bool game_is_running = true;


while( game_is_running ) {
update_game();
display_game();
next_game_tick += SKIP_TICKS;
sleep_time = next_game_tick - GetTickCount();
if( sleep_time >= 0 ) {
Sleep( sleep_time );
}
else {
// Shit, we are running behind!
}
}

이 방법은 큰 혜택이 있다. 참 쉽죠! update_game()이 초당 25번 불려진다는 것은 알기에 게임 코드를 작성하기에 쉽다. 예를 들면 이러한 게임 루프는 응답 구현 기능의 구현이 쉽다. 만약 게임 안에서 랜덤 값을 사용하지 않는다면, 단지 유저의 입력 변화 혹은 재생들을 기록하기만 하면 된다.

테스트하는 하드웨어에 FRAMES_PER_SECOND를 이상적인 값으로 적응시킬 수 있다. 하지만 보다 빠르거나 느린 하드웨어서는 무슨 일이 벌어질것인가? 그럼 이제 알아보자.

느린 하드웨어

하드웨어서 정해놓은 FPS를 다룰 수 있다면 문제가 없다. 그렇지만 하드웨어에서 FPS를 다룰 수 없을 때 문제가 발생한다. 게임은 느려질 것이다. 가장 나쁜 케이스는 게임이 무거운 부분에서는 느리게 움직이고 어떤 부분에서 정상적이게 게임이 돌아가는 것이다. 타이밍은 왜곡되어서 게임을 할 수 없게 만든다.

빠른 하드웨어

빠른 하드웨어에서는 문제가 없을 것이다. 하지만 정말로 많은 귀중한 클럭 사이클을 낭비하는 것이다. 쉽게 300 FPS를 만들 수 있을 때 25 또는 30FPS를 돌리는 것은... 부끄러운 줄 알아라! 특히 빠르게 움직이는 객체에 대해 시각적인 효과를 낭비하는 것이다.

다른 한편으로 모바일 장치에서 이것은 효과적일 수 있다. 게임을 계속 돌리는게 아니라서 배터리 시간을 유지할 수 있다.

결론

일정한 게임 속도에 FPS를 의존적이게 만드는 것은 쉽게 구현할 수있고 코드를 심플하게 하지만 문제가 약간 있다. 높은 FPS는 느린 하드웨어에서 문제를 야기할 수 있다. 그리고 느린 FPS는 빠른 하드웨어에서는 시각적 효과의 낭비로 이어진다.

게임속도가 가변FPS에 의존적

구현

또 다른 게임 루프의 구현은 가능한 빠르게 게임 루프를 도는 것이다. 그리고 FPS가 게임 속도를 결정한다. 바로 전 프레임과 다른 시간으로 게임은 갱신된다.

DWORD prev_frame_tick;
DWORD curr_frame_tick = GetTickCount();

bool game_is_running = true;
while( game_is_running ) {
prev_frame_tick = curr_frame_tick;
curr_frame_tick = GetTickCount();

update_game( curr_frame_tick - prev_frame_tick );
display_game();
}


게임코드가 약간 더 어려워 지고 있다. 왜냐하면 우리는 update_game()의 기능에 대해 고려해야 하기 때문이다. 그러나 아직까지 어렵지는 않다. 첫눈에 봐서 우리의 문제를 해결하기에 이상적인 방법으로 보인다. 나는 많은 똑똑한 프로그래머들이 이런 종류의 게임루프를 구현하는 것을 봤다. 그들 중 일부는 아마도 이러한 루프를 구현하기 전에 이 글을 봤어야 했는데 할 것이다. 내가 즉시 느린 하드웨어나 빠른 하드웨어( 맞다! 빠른!) 둘 모두에 심각한 문제를 발생하는 것을 보여주겠다.

느린 하드웨어

느린 하드웨어서 때때로 어떤 곳에서 확실하게 느려진다. 게임 내 무거운 곳에서. 이러한 현상은 3D게임에서 수많은 폴리곤들이 보여지는 일정한 시간동안 확실하게 느려진다. 이러한 프레임 하락은 입력 반응 시간에 영향을 미치게 되고 마찬지로 플레이어의 반응 시간에도 영양을 끼친다. 게임 업데이트에 딜레이를 느낄 것이고 게임 상태는 큰 시간뒤에 갱신될 것이다. 그 결과 플레이어 및 AI의 반응 시간 또한 느려진다. 그래서 단순한 이동 실패 심지어 이동 불가능이 될 것이다. 예를 들어 정상적인 FPS에서는 장애물을 피할 수 있는데 낮은 FPS에서는 불가능하게 된다. 좀 더 심각한 문제는 시뮬레이션이 폭발할 수도 있다는 것이다!

빠른 하드웨어
위에 나와있는 게임코드가 어떻게 빠른 하드웨어에 문제인지 궁금할 것이다. 불행하게도 가능하다. 첫째로 컴퓨터 위에서의 계산에 대해 설명하겠다.
float 또는 double형의 메모리 공간 한계 때문에 몇 몇 값들을 나타낼 수가 없다. 예를 들어 0.1은 이진수로 나타낼 수가 없어서 double에서 저장될때 어림수로 된다. pyton을 통해 보여주겠다. :


>>> 0.1
0.10000000000000001


이것은 별로 극적이지 않다. 예를 들면 레이싱카가 밀리세컨드당 0.001의 속도로 움직이는데 10초 후에 움직인 거리는 10.0이다. 만약 게임공간 내에서 쪼개서 계산을 한다면 초당 프레임으로 입력을 받는 다음과 같은 함수를 사용할 것이다.

>>> def get_distance( fps ):
... skip_ticks = 1000 / fps
... total_ticks = 0
... distance = 0.0
... speed_per_tick = 0.001
... while total_ticks < 10000:
... distance += speed_per_tick * skip_ticks
... total_ticks += skip_ticks
... return distance

이제 우리는 초당 40프레임 때의 거리를 계산 할 것이다. :


>>> get_distance( 40 )
10.000000000000075


잠시만 기다려봐라... 10.0이 아니네?? 무슨일인가? 흠, 왜냐하면 우리는 400 추가를 나누어서 계산했고 작은 에러들은 커진 것이다. 초당 100 프레임에서 무슨 일이 일어나는가...


>>> get_distance( 100 )
9.9999999999998312


머라고? 심지어 에러가 더 커졌다!! 흠, 왜냐하면 우리는 100fps에서 좀 더 더했기 때문이다. 라운딩 에러가 크게될 가능성이 있다. 그래서 게임에서 초당 40 이나 100프레임이 틀린것이다.

>>> get_distance( 40 ) - get_distance( 100 )
2.4336088699783431e-13


게임에서 이러한 차이가 너무 작다고 느낄 것이다. 그러나 진짜 문제는 잘못된 값으로 무엇을 계산하려고 할 때 생긴다. 이 방법으로는 작은 에러가 크게 된다. 그리고 높은 프레임 레이트에서는 게임을 불능으로 만들어 버린다. 이러한 일이 생기냐고? 고려하기에 충분하다! 이러한 게임 루프를 사용하는 게임을 보아왔고 정말로 높은 프레임 레이트에서 문제를 일으켰다. 후에 프로그래머가 게임 코어에 숨어있는 문제를 발견했다. 결국 많은 약간의 코드들을 수정했다.

결론
얼핏보면 이러한 게임 루프들은 좋다. 하지만 어리석게 굴지 마라. 느리거나 빠른 하드웨어 둘 모두 게임에 심각한 문제를 발생시킨다. 그리고 그거 외에도 고정 프레임 레이트를 사용할 때보다 구현이 더 어렵다. 그래서 굳이 이것을 사용 하겠는가?

최대FPS에 대한 일정한 게임 속도
구현

우리 첫번째 방법인 일정한 게임 속도에 의존적인 FPS의 경우 느린 하드웨에서 문제를 일으켰다. 그 경우 프레임레이트와 게임속도 둘 모두 저하됬다. 만일 그렇다면 가능한 방법은 게임 갱신은 그대로 두고 랜더링 프레임레이트를 줄이는 것이다. 이 것은 다음과 같은 게임루프를 사용한다 :

const int TICKS_PER_SECOND = 50;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 10;

DWORD next_game_tick = GetTickCount();
int loops;

bool game_is_running = true;
while( game_is_running )
{

loops = 0;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)
{
update_game();

next_game_tick += SKIP_TICKS;
loops++;
}

display_game();
}

이 게임은 일정하게 초당 50번 갱신을 하고 랜더링은 가능한 빠르게 완료한다. 랜더링은 초당 50번보다 많게 이루어진다는 것을 알 수 있고 이어지는 몇몇 프레임도 비슷해진다. 그래서 실제 영상 프레임은 초당 50프레임의 최대로 표현되어 질것이다. 느린 하드웨어서 돌아갈 경우 프레임레이트는 게임 업데이트가 MAX_FRAMESKIP에 도착할 때까지 떨어질 것이다. 실제로 render FPS가 5까지 (= FRAMES_PER_SECOND / MAX_FRAMESKIP)까지 떨어진다는 것을 의미한다. 실제 게임은 느려질 것이다.


느린 하드웨어
느린 하드웨어에서 초당 프레임은 떨어질것이다. 그러나 게임은 아마도 정상적인 속도로 돌아갈 것이다. 만약 하드웨어가 여전히 정상적인 속도를 따라가지 못한다면 게임은 느려지고 프레임레이트는 전혀 부드럽지 않을 것이다.

빠른 하드웨어
빠른하드웨어에서 게임은 문제가 없을 것이다. 그러나 처음 해결책과 비슷하다. 높은 프레임 레이트로 쓸 수 있는 많은 귀중한 클럭 사이클을 낭비하고 있다. 빠른 갱신율과 느린 컴퓨터와의 균형을 발견하는게 중요하다.

결론
최대 FPS를 쓴 일정한 게임속도는 쉽게 구현을 할 수 있고 코드가 심플하다. 그러나 여전히 약간의 문제를 가지고 있다 : 높은 FPS를 설정할 경우 여전히 느린 하드웨어에서의 위험성을 가지고 있다(그러나 첫번째 해결책만큼 심각하지 않다) 그리고 낮은 FPS를 설정할 경우 높은 하드웨어에서는 영상효과의 낭비이다.


가변FPS에 비의존적인 일정한 게임 속도
구현

느린하드웨어서는 한층 더 빠르고 빠른 하드웨어에서는 시각적으로 좀 더 매력적이게 향상시키는 게 가능한가? 그럼 우리에게 행운이다! 가능하다. 게임 상태는 초당 60번의 갱신이 필요하지 않다. 사용자의 입력이나 AI 그리고 게임 상태는 초당 25프레임으로 충분하다. 그래서 초당 update_game()를 더적게 더 많이도 말고 25번 부르도록 하자. 한편으로 랜더링은 하드웨어가 다룰 수 있으면 있을 만큼 빠르게 되도록하자. 그러나 느린 프레임 레이트는 게임의 갱신을 방해할 것이다. 이것을 해결하는 하는 방법은 이 게임루프를 따르는 것이다.


const int TICKS_PER_SECOND = 25;
const int SKIP_TICKS = 1000 / TICKS_PER_SECOND;
const int MAX_FRAMESKIP = 5;

DWORD next_game_tick = GetTickCount();
int loops;


float interpolation;

bool game_is_running = true;


while( game_is_running ) {

loops = 0;
while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP) {
update_game();

next_game_tick += SKIP_TICKS;
loops++;

}

interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick )
/ float( SKIP_TICKS );
display_game( interpolation );
}

이러한 게임 루프에서 update_game() 부분은 여전히 쉽다. 그러나 불행하게도 display_game() 함수는 좀 더 복잡하게 되었다. 보간 변수를 쓰는 예상함수를 구현해야 한다. 그러나 걱정하지 마라. 어렵지 않다. 단지 조금의 일을 더 하는 것 뿐이다. 보간과 예상하는 일을 어떻게 하는지에 대해 설명해 주겠다. 그러나 첫째 왜 이게 필요한지 보여주겠다.

보간의 필요성

게임 상태는 초당 25번 업데이트를 한다. 그래서 만약 랜더링에 보간을 사용하지 않는다면 마찬가지의 속도로 보여질 것이다. 예를 들어 영화의 경우 초당 24 프레임으로 실행되기에 25FPS가 어떤사람들은 느리지 않다고 생각한다. 그래서 25FPS가 시각적으로 만족스럽다고 생각한다. 그러나 빠르게 움직이는 객체에 대해서는 여전히 좀더 빠른 FPS를 쓰는게 개선된다는 것을 알 수있다. 그러므로 프레임 사이 마다의 빠른 움직임을 좀 더 부드럽게 하려면 무엇을 해야 한다. 그래서 보간과 예상 함수는 해결책을 제시해 쥔다.

보간과 예상

게임 코드는 초당의 현재 프레임으로 돌고 있다고 말했다. 그래서 프레임들 마다 draw/render할 때 2개의 게임시간이 가능하다. 예를 들어 게임상태를 10번 업데이트하고 난 후 장면을 나타내려고 한다. 랜더링은 10번째와 11번째 게임업데이트 사이에 될 것이다. 그래서 10.3 쯤에 랜더링이 가능 할 것이다. 보간 값은 그 때 0.3을 유지하게 된다. 예들 들자면 : 차가 다음과 같은 게임 시간으로 움직인다 :

position = position + speed;

만약 10번째 게임시간에 500이라는 위치에 있을때 속도는 100이다. 그 후 11번째 게임 시간에 위치는 600일 것이다. 그렇다면 차는 랜더링 시에 어느 위치에 있겠는가? 마지막 게임시간의 위치에 있게 할것이다(여기서는 500). 하지만 더 좋은 방법은 정확한 10.3 위치를 예상하는 것이다. 이와 같은 것이다 :

view_positon = position + (speed * interpolation))


차는 530 위치에 그려질 것이다. 그래서 기본적으로 보간 값은 이전 게임시간과 다음(previous = 0.0, next = 1.0) 사이에 값을 포함하고 있다. 랜더링 시간에 차/카메라/…가 배치될 때 "예상"함수를 어디에 만들어야 하는가? 예상함수를 객체의 속도 또는 조정, 회전 속도에 기초를 둬야 한다. 이것은 복잡함을 필요로 하지 않다. 왜냐하면 우리는 단지 프레임사이를 부드럽게 하려고 할 뿐이기 때문이다. 충돌을 감기 되기 전에 객체가 다른 객체로 표현되어지는 것이 실제로 가능하다. 그러나 이전에 봐왔듯이 게임은 초당 25프레임으로 업데이트 되고 있다. 그러므로 이와 같이 일이 일어날때 에러는 순식간에 보여진다. 사람 눈으로는 거의 알아차리기 어렵다.

느린 하드웨어

대부분의 경우 update_game()은 display_game()보다 훨씬 적은 시작을 필요로 한다. 사실상 우리는 심지어 느린 하드웨어 위에서도 update_game() 함수가 초당 25번 실행된다고 예상한다. 비록 게임이 초당 15프레임으로 화면을 보여줄지라도 많은 문제없이 플레이어의 입력을 다루고 게임상태를 업데이트할 것이다.

빠른 하드웨어

빠른 하드웨어 위에서 게임은 일정하게 초당 25프레임을 유지 할 것이다. 그러나 화면에 업데이트는 되는 것은 이것보다 빠를 것이다. 보간/예상 함수는 높은 프레임레이트에서 돌아가는 멋진 효과를 만들어 낼 것이다. FPS를 속이는 좋은 방법이다. 왜냐하면 매 프레임 레이트마다 게임 상태를 업데이트 하지 않는다. 내가 설명한 두번째 방법보다 높은 FPS를 가지고 있다.

결론

FPS에 독립적인 게임상태를 만드는 것은 가장 이상적인 게임루프 구현으로 보인다. 하지만 disply_game() 함수를 예상하는 것을 구현해야 한다. 그러나 어렵지 않다.

종합 결론

게임루프는 생각한 것보다 좀 더 많은 것을 가지고 있다. 우리는 4가지 가능한 구현을 살펴 보았다. 그 중 하나는 확실하게 피해야 할 것이다. 그 하나는 가변FPS에 따르는 게임속도이다. 모바일 장치에게 일정한 프레임레이트는 좋고 간단한 해결책이다. 그러나 모든 하드웨어에 쓰이기에는 별로이다. 가장 좋은 게임루프는 높은 프레임레이트를 위해 예상을 사용하는 게임속도에 독립적인 FPS이다.

만약 일부로 예상 함수를 사용하지 않는다면 최대 프레임레이트에서 작업할 수 있다. 하지만 적당한 게임 업데이트 레이트가 느리거나 빠른 하드웨어 모두 고장을 내는 것을 발견 할 수 있을 것이다. 이제 멋진 게임을 위한 코드를 생각해 보자!

Koen Witters

------------------------------------------------------------------------------------------

번역출처 : http://www.devpia.com/Maeul/Contents/Detail.aspx?BoardID=30&MAEULNo=12&no=235&ref=235 

출처 : http://dewitters.koonsolo.com/gameloop.html

저작자 표시
신고

'프로그래밍 > Game' 카테고리의 다른 글

게임 루프 (Game-Loop)  (1) 2011.08.05
by Viewide 2011.08.05 05:13

싱글톤 패턴은?

 싱글톤 패턴은 자신이 만든 프로그램 속에서 하나의 클래스에 대해서는 단 하나의 인스턴스만을 생성 할 수 있도록 제한하는 패턴이다.

 만약 클래스의 인스턴스가 존재하지 않는다면 클래스 함수 호출시 새로운 인스턴스를 생성하는 방법으로 클래스를 만든다. 이미 존재한다면 그 인스턴스에 대한 레퍼런스를 반환한다.

 단 하나의 인스턴스만을 가져야 하기 때문에 객체의 인스턴스를 생성하지 못하도록 생성자는 private 또는 protected 로 정의 한다. 싱글톤은 하나의 정적 인스턴스로 구현될 수도 있고, 필요할 때는 생성되는 방식으로 만들 수도 있다. 

싱글톤 패턴의 장점은
?

 객체가 하나만 생성된다는 것 자체도 큰 장점이지만, 위에서 살짝 언급했듯이 객체를 사용하는 그 순간에 생성된다는 장점도 존재한다. 

 조금 자세히 들어가 보자면 프로그램에 한 개만 존재해야 하는 클래스 (예를 들면 DB_Management 클래스나 Object Management 클래스) 는 싱글톤 패턴으로 구현하게 된다면 가장 처음 사용할 때 한번만 생성해줌으로써 효율적인 메모리 관리가 가능해진다. 그리고 한번의 호출 뒤 그 이후의 호출부터는 이미 생성된 인스턴스만을 반환하기 때문에 생성자도 한번만 호출하고 소멸자도 한번만 호출하게 된다.

 구조상에서 갖게 되는 이점도 크다. 지금 글을 작성하고 있는 나도 싱글톤 패턴을 알게 됨으로써 게임의 엔진부분부터 싹 갈아엎고 다시 작성했다. 내가 겪었던 문제가 됬던 구조를 예를 들어 설명하자면,

 Object Management 클래스에 있는 Vector 에 다형성을 띄는 Object 상속 클래스들(캐릭터, NPC, ...)이 충돌처리를 위해서는 Object Management 클래스에 접근해야만 했다. 그렇다면 결국 서로 include 를 하는 최악의 형태가 발생하여 컴파일 조차 불가능해진다. 난 그때 다른 외부 cpp 파일에
 void Object_Manager_Collision (...) 식의 함수로 빼고 프로토 선언식으로 가져와서 처리를 하는 땜빵식 프로그래밍을 했었다. (물론 자바에서 비슷한 프로젝트를 할 때는 그냥 부모 클래스를 인자로 받아버리면 끝나더라.) 그러나 싱글톤 패턴을 알게 된 뒤에는 구조적인 부분에서 크게 개선되었었다. (개개인의 환경에 따라 다르므로 자세한 예는 생략하겠다.)


주의할점
?

 싱글톤은 멀티스레드(Multi-Thread) 환경에서는 주의 하여 사용해야 한다. 만약 인스턴스가 존재하지 않는 상태에서 두개의 쓰레드가 동시에 생성 메소드를 실행하려고 하는 경우, 단지 하나의 쓰레드만이 새로운 인스턴스를 생성 할 수 있도록 해야 한다.




인터넷에 보면 많은 예제들이 돌아다닙니다.
그러나 생각없이 아무 예제나 갖다가 쓰면 자신의 코드랑은 맞지 않을 가능성이 높습니다.

아래의 예제는 제가 싱글톤 패턴을 처음 접했을때 수많은 삽질을 겪고 깨우치게 되면서 작성한 포인터 싱글톤 패턴 예제 입니다.

    EngineCore.h

#pragma once


class C_EngineCore

{

private:

C_EngineCore(void);


public:

~C_EngineCore(void);


static C_EngineCore& GetInstance(void);

};

 

    EngineCore.cpp

#include "EngineCore.h"


C_EngineCore::C_EngineCore(void)

{

}


C_EngineCore::~C_EngineCore(void)

{

}


C_EngineCore& C_EngineCore::GetInstance(void)

{

static C_EngineCore instance;

return instance;

}



    사용 예)

C_EngineCore *m_Engine = &C_EngineCore::GetInstance();


잘못된 부분 지적해주시면 고치겠습니다 ^~^
저작자 표시
신고

'프로그래밍 > Patterns' 카테고리의 다른 글

Singleton with C++ & Pointer  (0) 2011.08.03
by Viewide 2011.08.03 20:49
출처: http://frompt.egloos.com/2770424

▣ 클래스 내에서 람다 사용하기

★ 클래스 내부의 멤버 함수 내에서 람다를 사용할 수 있다.

→ 여기서 this (자기 클래스에 대한 포인터)를 캡쳐하면 자기 클래스 내의 멤버 변수, 멤버 함수를 람다 내에서 모두 사용할 수 있다.
→ 클래스 내에서의 람다 식은 클래스에서 프렌드 함수로 인식하게 되므로 this 캡쳐시에 private 멤버까지 접근할 수 있다.

#include <stdio.h>
#include <vector>
#include <algorithm>
#include <functional>
 
class CCharacterList
{
private:
    std::vector<int> _viCharacterLevel;
    
    void PrintLevel( int nLevel )
    {
        printf( "Level : %d\n", nLevel );
    }
public:
    CCharacterList()
    {
        _viCharacterLevel.push_back( 1 );
        _viCharacterLevel.push_back( 54 );
        _viCharacterLevel.push_back( 99 );
    }
 
public:
    void PrintAll()
    {
        std::for_each( _viCharacterLevel.begin(), _viCharacterLevel.end(), 
            [this](int i){ PrintLevel(i); } );
    }
};
 
void main( void )
{
    CCharacterList *pcharacterlist = new CCharacterList;
    pcharacterlist->PrintAll();
    delete pcharacterlist;
}

→ for_each() 알고리즘은 컨테이너 내부의 모든 원소들에 대해 인수 3에 해당하는 펑터를 적용시킨다. (단, 컨테이너의 원소 값을 변경해서는 안 된다.)
클래스 내부 멤버함수 PrintAll()에서 for_each()를 호출하여 그곳의 세 번째 인수에 람다를 쓰고 있다.
람다 내부에서 private 멤버 함수인 PrintLevel()을 호출하는데 이것은 이 람다가 this를 캡쳐하였기 때문에 가능한 것이다.


★ 또한 람다를 클래스 내의 멤버 그 자체로 선언할 수도 있다.


: 이 때에는 auto를 쓰면 안되고, std::function을 써서 선언해야 하며, 선언하자 마자 바로 초기화가 불가능하며, 선언 이후 생성자 등의 멤버함수에서 람다를 정의해 주어야 한다.



class CCharacterList
{
private:
    std::vector<int> _viCharacterLevel;
    std::function<void(int)> _funcPrintOne;
    
    void PrintLevel( int nLevel )
    {
        printf( "Level : %d\n", nLevel );
    }
public:
    CCharacterList()
    {
        _viCharacterLevel.push_back( 1 );
        _viCharacterLevel.push_back( 54 );
        _viCharacterLevel.push_back( 99 );
        _funcPrintOne = [this](int i){ PrintLevel(i); };
    }
public:
    void PrintAll()
    {
        std::for_each( _viCharacterLevel.begin(), _viCharacterLevel.end(), _funcPrintOne );
    }
};

→ 여기에서도 this를 캡쳐하면 private 멤버까지 접근 가능하다.



▣ 람다를 STL 컨테이너에 저장하기


★ std::function을 이용한다면 람다를 STL 컨테이너에 통째로 담아버릴 수도 있다.



std::vector< std::function<int(void)> > vfuncInContainer;
                    
vfuncInContainer.push_back( []() -> int { return 100; } );
vfuncInContainer.push_back( []() -> int { return 200; } );
 
printf( "RetValue : %d\n", vfuncInContainer[0]() );
printf( "RetValue : %d\n", vfuncInContainer[1]() );

→ vfuncInContainer라는 STL vector 컨테이너 안에 람다를 넣어서 그 내부에서 호출하는 코드이다.



▣ 함수에서 인수 혹은 리턴 값으로 람다 사용



#include <stdio.h>
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <functional>
 
void funcWithLambda( std::function<int(int, int)> func, int lhw, int rhw )
{
    printf( "Function Value = %d\n", func( lhw, rhw ) );
}
 
std::function< void() > funcWithRetLambda( void )
{
    std::string str("lambda!!!");
    return [=] { std::cout << "Hello, " << str << std::endl; };
}
 
void main( void )
{
    funcWithLambda( []( int a, int b )->int{ return a + b; }, 3, 5 );
 
    auto funcTemp = funcWithRetLambda();
    funcWithRetLambda()();
    funcTemp();
}

→ funcWithLambda()는 std::function에 대한 객체를 인수로 받는다.
즉, 람다 자체를 인수로 줄 수 있다는 이야기이고, funcWithLambda() 호출 시 람다 인수를 줄 때 매개변수에 명시된 리턴 값 및 람다의 매개변수의 타입을 동일하게 줘야 한다.


→ funcWithRetLambda()는 람다를 리턴 받는 함수인데 복사 캡쳐를 사용한 점을 주목하기 바란다.
함수 내에서의 지역변수는 함수 리턴 시 소멸되어 사용하면 안 되는 메모리 영역이 되어버린다. 만약 코드상의 캡쳐가 = 대신 &이었다면 차후 람다 호출 시 문제가 생길 공산이 매우 크다. (당장에는 문제가 안 생길수도 있지만 아주 위험하다. 없어져버린 변수를 참조하고 있기 때문이다.)
: 그러므로 함수 내 람다 리턴 시 캡쳐가 필요할 때는 (지역변수에 대해서는) 꼭 복사 캡쳐를 사용하기 바란다.
: 포인터 캡쳐의 경우에도 잘 판단해야 한다. 만약 함수 내에서 선언한 포인터가 해당 함수의 지역변수를 가리키고 있다면, (즉, 함수가 리턴되면 사라져 버리는 변수들) 그 포인터는 캡쳐해서는 안될 것이다. 꼭 주의해서 캡쳐하도록 하자.



▣ 람다에서의 재귀호출


★ 일반 함수에서도 재귀호출이 가능하듯이 람다에서도 재귀호출이 가능하다. 단, 재귀호출로 람다를 작성시 반드시 std::function으로 써야 한다.
(auto는 안된다.)


→ 재귀호출의 상징적 함수인 팩토리얼을 예로 들어보겠다.



std::function<int(int)> funcFactorial = 
[&funcFactorial](int x) -> int
{
    return x == 0 ? 1 : x * funcFactorial(x - 1);
};
 
int Value = 5;
printf( "Factorial(%d) = %d\n", Value, funcFactorial( Value ) );

→ 자기 자신에 대한 객체 (funcFactorial)의 참조를 캡쳐하여 재귀호출하고 있다.



▣ 람다 (혹은 람다의 배열)에 대한 포인터


★ 람다는 std::function 타입으로 볼 때, 템플릿 클래스이다.
즉, 객체를 만들 수 있기 때문에 뒷장에서 해왔던 람다를 매개변수, 리턴 값, 클래스 멤버 변수, 컨테이너 원소 등으로 쓸 수 있게 되는 것이다.
그에 따라서 람다를 std::function으로 쓴다고 한다면 이걸로 배열도 만들 수 있고, 이것에 대한 포인터 또한 선언 가능하다.
< 참고로 std::function으로 만들어진 객체의 크기는 sizeof() 확인 결과 24바이트였다. (상황에 따라 틀릴지도 있습니다.) >



std::function<int(void)> *pfuncPtr = new std::function<int(void)>[3];
pfuncPtr[0] = []() -> int { return 100; };
pfuncPtr[1] = []() -> int { return 200; };
pfuncPtr[2] = []() -> int { return 300; };
 
for ( int i = 0; i < 3; i++ )
{
    printf( "pfuncPtr[%d]() => %d\n", i, pfuncPtr[i]() );
}
 
delete []pfuncPtr;

→ std::function 객체에 대한 포인터를 선언하고 3개 요소에 대한 공간을 new로 할당 받아서 사용하는 코드이다.
→ 결국 람다의 포인터를 사용할 수 있음으로써 이것 또한 힙 영역에서 관리가 가능하다는 것을 파악할 수 있다.



▷ 결국 람다 부분까지 마무리 하고 말았습니다. 이것으로 C++ 0x부분에 대한 포스팅은 마치겠습니다.
STL 알고리즘에서는 확실히 펑터 사용하기가 귀찮죠. 이것을 쓴다면 보다 더 쉽게 알고리즘들을 사용할 수 있을 것 같습니다.
( 많이 쓰일 수만 있다면 참 좋겠지만 아직 C++ 0x 자체가 대중화가 덜 되었다 보니 ㅠㅠ )

다음은 무엇을 포스팅 할까 하다가 역시나 DirectX 11 부분을 포스팅 하는 걸로 잠정 확정된 것 같습니다.
DirectX9 조차 포스팅을 안 했는데 감히 11을 포스팅 하는 게 다소 난해할 수도 있겠다만 어떻게든 배워서 열심히 올려 볼렵니다. 언젠가는 다들 쓸지도 모르니깐요.


※ 참고자료 출처 : Microsoft Visual Studio 「Visual C++ 10과 C++0x」 ( 최흥배님 저 )


저작자 표시
신고

'프로그래밍 > STL/C++ 0x' 카테고리의 다른 글

C++ 0x: 람다 응용  (0) 2011.08.03
C++ 0x: 람다 캡쳐 (Lambda Capture)  (0) 2011.08.03
C++ 0x: 람다  (0) 2011.08.03
STL : Functor (함수자)  (0) 2011.08.03
C++ 0x: nullptr  (0) 2011.08.03
C++ 0x: decltype  (0) 2011.08.03
by Viewide 2011.08.03 19:53
| 1 2 3 4 ··· 6 |