4. 파이게임의 기본구조 익히기
Last updated
Last updated
우리가 라이브러리를 사용할 때 장점도 많지만 그에 따른 제약도 함께 있다는 것을 함께 이해해야 한다. 라이브러리를 사용한다는 것은 해당 라이브러리가 요구하는 어떤 전형적인 코드구조(틀) 안에서 맞춰서 코딩을 해야 한다는 것이다. 그래서, 해당 라이브러리의 사용법을 익히는 과정의 추가적인 시간과 노력을 요하게 된다는 점도 기억해두어야 하겠다. 이번 장에서는 pygame 라이브러리의 기본 사용법을 배우도록 하겠다.
파이게임 라이브러리를 활용한 코딩은 크게 다음과 같은 크게 4부분으로 구성된 프로그램 구조의 틀을 갖는다. 이 기본 틀을 잘 기억해 두어야 하는데, 왜냐면 항상 이 기본 틀 안에서 코딩하게 될 것이기 때문이다.
4개 구성의 실제 코드분석을 시작하자. 위에 [게임구조의 실제 코드] 라는 탭을 선택해 코드화면을 열어보자. 크게 4개의 구성을 구분하기 위해 주석에 part 1~4까지 넘버링을 붙혀놨다.
9~11라인은 게임화면의 크기를 정하는 코드로, 9, 10번 라인은 우리 게임이 실행될 화면크기(픽셀단위)의 가로길이와 세로길이(여기서는 가로 400픽셀, 세로 300픽셀)를 결정한 후 이를 각각의 2개의 변수(S_WIDTH, S_HIGHT)에 담고 있다. 여기서 화면의 가로길이와 세로길이를 일부러 변수를 담아놓아다는 것에 주목하자. 이렇게 하는 의도는 앞으로 해당 값은 변수이름을 통해서 읽어 오겠다는 것이다. 이런 식의 테크닉을 구사하는데는 두 가지 목적이 있다.
15번 라인에서 기존 코드의 연장선으로 이번에 게임의 이름을 정하는 것으로, set_caption 함수에 우리 게임의 이름값을 문자열(스트링) 타입의 값으로 해서 인자값으로 전달해 설정해주면 된다. 크게 어려운게 없다.
이제 18-19라인으로 넘어가자. 사실 이 코드는 30번 라인하고 같이 묶어서 봐야 한다. 여기서 새로 등장하는 개념이 있는데 바로 FPS(Frames per second, 초당 프레임 출력횟수)다. FPS에서 프레임은 화면에 표시되는 정적 이미지 한장, 한장을 의미한다. 우리가 정지된 이미지를 모아 움직이는 애니메이션을 만들 때를 생각해보면 된다. 정지 이미지에 매 한장(프레임)마다 아주 조금씩의 이미지의 변화를 주고, 이 각각의 이미지를 1초 안에 최소한 30번(인간의 눈의 평균 인지능력 기준으로) 이상을 보여주면, 사용자에게는 정지된 이미지가 아니라 자연스럽게 움직이는 애니메이션처럼 보이게 되는 것이다.
따라서, 게임화면에서 자연스러운 애니메이션 효과를 얻기 위해 일반적으로 60fps 속도를 화면갱신의 최대속도로 사용한다. 여기서 최대속도로 한다는 말에 주목하자. 만약 우리가 코드로 FPS를 설정하지 않는다면? 일반적으로 고사양의 성능좋은 컴퓨터에서 우리 게임을 실행한다면 60fps 이상으로 화면을 갱신(업데이트)하게 되는데 60fps면 되는데 그 이상으로 화면 업데이트 하느라 불필요한 컴퓨팅 자원이 낭비될 수 있고, 무엇보다 너무 지나친(?) 업데이트로 인한 오히려 부자연스러운(속도 빨라보이는) 애니메이션이 되게 되고, 게임사용자의 흥미와 조작성을 떨어뜨린다.
게임에서 먼저 고려할 것은 사용자가 언젠가는 게임을 고만하기 위해서 게임 프로그램을 완전 종료하고 싶을 것으로, 우리는 이에 대한 안전한 정상 종료하는 방법을 제공하는 것이다. 이 코드가 24~27번 라인까지의 코드이다. 이전 장에서 실행시켜 본 이 게임앱의 완전 종료는 어떻게 할 수 있는가? 그렇다 우리가 원래 잘 알고 있는 방법인 게임화면 상단의 윈도우 타이블바에 위치한 창을 닫는 x모양의 버튼을 눌러 완전 종료시킬 것이다.
24번 라인부터 다시 for-in 문를 사용한 반복문에 진입하는데, 우리의 의도는 우리 프로그램에서 발생하는 모든 이벤트를 캡쳐하고, 25번 라인에서는 그 이벤트들 중에서(for-in 문에서 in 이 쓰이는 이유) 이벤트의 종류(타입, type)이 만약, QUIT(윈도우 타이틀바의 x버튼을 누름)라는 이벤트가 있을 경우엔, 26-27번 라인을 실행시켜 게임을 정상 완전종료 하라는 것이다. 26번 라인의 quit 함수는 5번 라인의 init 함수와 쌍을 이루는 것으로 프로그램의 정상종료를 위해 파이게임 라이브러리 내의 전반적인 활동의 종료절차(사용중인 메모리의 해제 등)을 밟도록 지시하는 것이고, 27번 라인의 exit 함수의 목적과 용도는 part 1에서 이미 밝혔다.
30번 라인 코드에 대한 설명은 이미 이루어졌고, 이제 진짜 마지막으로 29번의 코드만 이해하면 이번 장은 마무리 된다. display 모듈 안에 update 함수는 사용자가 눈에 보이는 실제 게임화면(위에서 언급된 프레임이라고 볼수 있음) 한장 한장을 사용자 화면에 그려내는 코드이다. 사용자의 눈에 보이는 게임화면이 그려지는 과정은 실제는 아래와 같은 내부적인 동작을 거치게 된다.
게임화면에는 다양한 여러종류의 정지 또는 움직임이 있는 여러 이미지들이 동시다발적으로 등장하는데, 그 이미지 하나하나를 사용자가 보고 있는 화면 그 자체에 매번 한개씩 일일이 다 직접 그리고 화면을 업데이트(갱신) 한다라고 가정하면, 사용자는 너무 많은 화면 깜빡임으로 도저히 게임을 할 수 없을 지경이 될 것이다. 그래서, 내부적으로는 사용자 눈에 보이지 않는 메모리 공간(비디오 버퍼)에 각각이 이미지들을 개별적으로 그려놓고, 각각 그려진 개별 이미지들을 사용자한테 보여줘도 되는 완전한 한 장(프레임) 형태로 합쳐 완성되면, 그제서야 사용자 눈에 보이도록 비디오 메모리에 준비된 게임화면 한장을 통으로 화면으로 보내 업데이트하는 방식의 테크닉을 사용하고 있다.
이로써 파이게임 라이브러리를 사용한 게임개발시 가장 기본이 되는 구조의 틀과 코드의 분석을 마쳤다. 파이게임 라이브러리를 쓴다면, 항상 이러한 규격화된 코드틀(템플릿(templete)이라 부름)에서부터 코딩을 시작한다고 보면 되겠다. 그래서, 프로그래밍 세계에서는 이러한 코드틀을 지칭하는 용어가 있는데, 보일러플레이트 코드(Bolierplate code)라고 불린다. 간단히 맨 처음 코딩시작할 때 일단 저 틀을 코드편집기에 복붙하고 나서 코딩을 시작한다 정도로 생각하면 되겠다.
상용구 코드(Boilerplate code): 컴퓨터 프로그래밍에서 상용구 코드 또는 상용구는 수정하지 않거나 최소한의 수정만을 거쳐 여러 곳에 필수적으로 사용되는 코드를 말한다.
참고: 위키피디아
지금까지 파이게임의 기본 구조를 익였으니, 다음 장부터 본격적으로 실제 게임을 제작하면서 심화된 코딩을 배워나가도록 하자.
part 1은 2~3번 라인으로 젤 먼저 해야할 일은 코딩을 위해선 우리가 사용할 라이브러리를 불러오는게 필요하며, import라는 키워드를 통해 라이브러리를 호출하는데, 2개(pygame, sys)의 라이브러리를 불러드리고 있다. pygame이 필요한 것은 당연하며, sys 라이브러리는 왜 필요할까? 이것은 27번 라인에서 사용하는 exit 함수 때문에 필요하다. exit 함수(혹시 함수 자체가 무엇인지에 관해 사전지식이 없는 분들은 저자의 를 참고하기 바람)는 우리가 만든 게임 프로그램 자체를 완전히 안전하게 종료하는 용도이다.
part 2에 5번 라인은 파이게임 라이브러리의 내부적인 초기화 목적으로 라이브러리를 불러오는 동시에 단 일회성 호출을 요구한다. 이것은 라이브러리 활용시 응당 그렇게 사용해야 한다는 라이브러리 자체가 요구하는 요구사항이기 때문에 사실상 우리는 고민없이 요구사항에 맞춰 해당 초기화 목적 init 함수를 부르면 그만이다. 그럼에도 불구하고 이러한 과정이 왜 필요한가를 유추해 본다면, 기본적으로 라이브러리는 다시 수많은 모듈로 구성되어 있고, 그러한 각각 모듈(Module)들이 각각 잘 초기화되어야 이후 원활한 동작을 보장할텐데 그런 차원에서 호출을 요구한다고 볼 수 있다.
part 3은 7~18번 라인으로 우리 게임의 기본환경(게임화면의 크기, 게임의 타이틀, 게임화면 업데이트 최대속도 등)을 정해야 한다. 각 라인별로 더 쪼개서 자세히 살펴보자.
첫째, 화면크기를 변수명으로 표현하지 않고 숫자값 그대로 사용했다고 하면, 당장은 몰라도 시간이 지나 나중에 나 자신 또는 내 코드를 읽게 되는 다른 동료의 코드 가독성(해독성)을 떨어뜨리게 된다. 코드를 해독하기 위해 눈으로 한 라인 한 라인 읽어나가다가 갑자기 등장한 숫자(400과 300)을 보고, 그 숫자값은 도데체 어떤 값을 의미하는 숫자인지 잠시 고민하는 시간이 필요하고, 이렇게 한눈에 한번에 빠르게 읽어나가지 못하고 멈추게 하는 그 지점이 바로 가독성을 떨어뜨리는 지점인 것이다. 그런데, 우리가 이런 식으로 그 숫자값을 화면의 가로와 세로길이라는 의미를 부여한 변수이름(S_WIDTH, S_HIGHT)과 매칭해 놓았다면, 우리는 변수이름만 보고() 곧바로, 그 숫자값의 의미를 파악할 수 있다. 또 의도적으로 변수이름을 대문자로 표현한 것도 특징인데, 물론 변수가 변화하는 값을 저장하는 기본적인 목적이 있으나, 이 경우는 그러한 목적보다는 특정값을 대치하는 용도로서의 용도가 더 크기 때문에 그 의미를 부여하기 위해 대문자 표기를 하였다. 참고로 이는 일종의 프로그래머들 사이의 관습적 표현(Conding conventions)이므로 적극 활용하면 좋을 것이다.
두번 째는 앞으로 써내려 갈 우리의 코드 안에서 해당 숫자값(여기서는 게임화면의 가로와 세로크기)을 상당히 여러 곳에서 사용할 것이고, 그때마다 해당변수명을 사용해 가로,세로길이값을 읽어오도록 코딩해 놓았다고 생각해보자. 이렇게 할 경우, 장점은 무엇이 있을까? 나중에 어떤 이유에 의해서(에서 언급했지만, 소프트웨어 특성상 생각보다 자주 코드수정이 빈번하고) 우리 게임을 두배 더 큰 화면(가로 800픽셀, 세로 600픽셀)으로 실행해야 하는 상황이 되었고, 이에 맞춰 전반적인 코드의 재수정이 필요하다고 가정하자. 우리는 이미 똑똑하게 이러한 가능성을 대비해 해당 값을 변수명을 통해 코딩해 놓았지 않는가? 따라서, 딱 두 값(S_WIDTH, S_HIGHT)의 현재 값인 400, 300을 각각 800, 600으로만 바꿈으로써 이러한 코드 수정 요구사항의 빠른 대응이 가능하게 되는 것이다.
이제 11번 라인에서 set_mode 함수를 호출하는 방법을 살펴보자. 해당 함수 이름을 단지 이름으로 호출할 수 없고, 이런식의 pygame.display.set_mode 해당 함수의 위치까지 포함한 표현법으로 호출하고 있다. 이유는 이 함수가 우리가 지금 코딩하는 있는 파일 안에 위치한 우리가 직접 만든 사용자 함수가 아니며, 동시에 파이썬 언어 자체 안에 기본내장된 내장함수도 아니기 때문이다. 따라서, 우리는 이미 에서 배웠듯이 해당 함수가 실제 어디에 위치해 있는지의 위치정보를 나타내는 마침표(.) 표기법을 이용해 호출해야만 파이썬이 실행시 그 함수를 정확히 찾아내어 활용할 수 있게 되는 것이다. set_mode 함수이름 이전까지의 pygame.display. 의 의미는 결국 set_mode 함수는 계층적(hierarchical) 구조 안에 위치해 있다는 것이고, 먼저 최상단의 pygame이라는 라이브러리 안에 있으며 다시 그 라이브러리 안에 있는 display란 모듈(module) 안에 위치한다는 것을 의미한다.
set_mode 함수에서 더 알아야 것들이 남아있다. 해당 함수를 set_mode((S_WIDH, S_HIGHT)) 이렇게 호출하고 있고, 함수로 넘겨야 하는 값(이를 아규먼트(argument) 또는 인자값 이라는 용어로 지칭)이 이전 서에서 배우지 못한 데이터의 형태(type)를 띠고 있다. 일단, 함수로 넘기는 인자값은 1개가 아닌 복수개의 2개(화면의 가로와 세로크기)를 한꺼번에 넘기고 있는데, 우리가 이전 서에서 배웠듯이 단 하나의 값이 아니라, 여러 개의 복수의 값을 저장할 수 있는 있는 공간은 무엇이었는지 기억하는가? 그렇다 에 담아서 보내면 되겠다라는 생각이 들 것이다. 리스트의 문법형식이 기억나는가? 큰 대괄호 안에 값을 채우면 된다. 즉, 이 경우에 [S_WIDH, S_HIGHT] 이렇게 값을 채워서 set_mode 함수로 전달하면 되지 않겠는가? 그렇다, 가능은 하다. 그런데, 이 함수를 만든 사람은 그렇게 말고, 해당 값들을 소괄호()에 담아 보내달라는 함수 이용자인 우리에게 제약을 걸어두었다. 대괄호에 담는 것과 소괄호에 담는 것에 차이는 무엇인가? 점하나도 공백하나도 다 의미가 있는가 있는 프로그래밍 세계에서 괄호종류가 다른 것은 당연히 의미가 달라진다. 소괄호에 데이터들를 담는 것을 우리가 그렇다 이런 데이터 타입을 튜플(Tuple) 이라고 부른다.
이 값을 설정하는 이유는 우리가 나만의 게임을 만들어 주변에 여러 지인들에게 해보라고 나눠줄 수 있고(이런 과정을 배포(deployment)라고 부름), 그렇다면, 결국에는 우리 게임이 각 개인이 가진 다양한 사양(성능)의 컴퓨터에서 실행될텐데 그때 일정한 게임속도를 보장하고자 하는 장치즘으로 이해하면 되겠다. 서두에서 이 코드는 30번 라인의 코드와 연결되어 있다고 언급했는데, 18번 라인은 설정할 FPS값을 변수로 만들어 값을 저장한 것이고, 19번 라인은 화면갱신의 타이밍을 알기위한 시간계산에 사용한 시계를 time 모듈 안에 Clock 객체() 생성을 통해 만들어 역시 clock이란 변수에 저장해 놓은 것이고, 30번 라인에서야 그 시계 안에 tick 함수를 통해 사전 설정한 FPS 의 화면갱신을 유지하도록 실제적인 속도조절을 하게 된다.
이제 드디어, 마지막 남은 part 4를 이해해 보도록 하자. 우리가 이미 블록코딩에서 게임을 만들 때도 많이 활용해 본 무한루프(무한반복) 구조라 크게 낯설지는 않다. 게임이 종료되기 전까지 우리의 게임은 이 무한루프 안을 끝없이 돌면서 실행하게 된다. 파이썬에서 무한루프를 만들기 위해서 20번 라인의 while True: 으로 만들 수 있다는 것은 우리가 이라는 장에서 이미 배웠고 어려운게 없다.
그런데, 생각해보면 그 x모양 버튼을 누른다고 무조건 자동종료가 되는게 정상인가? 사실은 아니다, 사용자가 그 버튼을 누르게 되면 어떤 동작을 하라(이 경우 프로그램을 정상 완전종료하라)는 코드가 우리 프로그램 안에 씌여져 있기 때문에 그것에 맞춰 동작했던 것 뿐이다. 자 그럼 이 부분을 코딩해 보자. 사용자가 x모양 버튼을 눌렀다는 것은 게임개발하는 우리 입장에서는 어떻게 알 수 있는가? 파이게임 라이브러리 에서는 이를 알 수 있도록 우리에게 제공한 함수가 있다. 24번 라인에 event(이벤트, 사건) 모듈 안에 get 이라는 함수이고, 이벤트 라는 단어는 어디서 들어본 것 같지 않은가? 우리가 키보드를 누를 때, 마우스를 움직이거나 버튼을 누를 때, 지금과 같이 우리 게임앱의 타이틀바의 버튼을 누를 때 등 이런 모든 하나하나가 다 프로그램 입장에서는 이벤트인 것이고, 이런 이벤트가 발생하는 상황에 맞추어 어떤 동작을 하도록하는 스타일의 프로그래밍 패러다임을 이미 살펴본 바가 있다. 그렇다 지금의 코딩이 바로 그 패러다임을 따르는 코딩스타일이다.