CDD 두 번째 예제 스크립트 분석입니다. 이번 스크립트는 렌파이 길라잡이 게임에 있는 미니게임 퐁의 스크립트입니다. 렌파이 설치 폴더/tutorial/game/demo_minigame.rpy 파일에서 찾아볼 수 있습니다. 일단 런처를 켜고 튜토리얼을 실행해서 minigames 메뉴를 누르고 퐁 게임을 실행해 몇 판 해본 뒤에 이 글을 읽으시는 게 이해에 도움이 될 겁니다.
또한 이 스크립트는 미니게임을 만들기 위한 스크립트라 지난번에 봤던 스크립트와 다른 점이 많습니다. 두 스크립트를 비교해서 살펴보세요.
expand source를 클릭하면 엄청난 양의 스크립트가 펼쳐집니다. 지난 글은 괜찮았지만 이번 글은 물흐르듯 읽으면 이해하기 힘듭니다. 옆에 스크립트를 펼쳐놓고 스크롤을 위 아래로 왔다갔다 하면서 그때 그때 내용을 확인하세요.
물론 그런다고 해서 단번에 이해되는 글은 아닙니다... 저도 이 글을 쓰겠다고 이틀 동안 이 스크립트만 붙잡고 있었네요-_-;
분석
image bg pong = "pong_field.png"
퐁 게임판을 bg pong 으로 배정했습니다.
게임판을 제작자 정의 디스플레이어블 자체에서 띄우지 않고 렌파이 명령문 scene으로 따로 띄우려고 이렇게 설정했나보네요.
class PongDisplayable(renpy.Displayable):
제작자 정의 디스플레이어블 PongDisplayable을 만들기 위해 renpy.Displayable 상속합니다.
이 클래스로 만든 디스플레이어블 자체를 통채로 add 문으로 스크린에 더해 사용할 것이라 따로 넘겨 받을 값이 필요 없습니다.
대신 따로 __init__ 에 미니게임에서 필요한 값을 모두 설정해두었습니다.
renpy.Displayable.__init__(self)
이 클래스를 호출할 때 renpy.Displayable의 __init__ 메소드를 호출합니다.
스타일이라든지 포커스 같은 걸 받아서 생성하는데...왜 호출하는지는 저도 잘 모르겠네요 ㅇ<-<
self.paddle = Image("pong.png") # 공 튀기는 판때기 self.ball = Image("pong_ball.png") # 공 self.player = Text(_("Player"), size=36) self.eileen = Text(_("Eileen"), size=36)
self.ctb = Text(_("Click to Begin"), size=36)
퐁에서 사용할 이미지와 텍스트 디스플레이어블을 정의합니다.
self.PADDLE_WIDTH = 8 #판때기 가로길이 self.PADDLE_HEIGHT = 79 #판때기 세로길이 self.BALL_WIDTH = 15 # 공 가로 길이 self.BALL_HEIGHT = 15 #공 세로 길이 self.COURT_TOP = 108 #게임이 진행되는 게임판의 위 (y 위치) self.COURT_BOTTOM = 543 #게임이 진행되는 게임판의 밑 (y 위치)
게임에서 사용하는 그림들의 크기값입니다.
self.stuck = True
게임 시작 전에 공이 판때기에 붙어 있는 상태.
뒤에 가면 나오겠지만 이 값이 False가 되면 공이 판때기에서 떨어지면서 게임이 시작되도록 스크립트가 짜여있습니다.
self.bx = 88 # 공의 x 위치 self.by = self.playery # 공의 y위치 = 플레이어 판때기의 y위치 self.bdx = .5 self.bdy = .5 self.bspeed = 300.0 #공 이동 속도
공 위치, 공 속도 등을 배정하기 위한 변수입니다.
각자 초기값이 정해져있는데 나중에는 화면 그릴 때마다 달라지게 됩니다.
중간에 주석을 안 단 self.bdx와 self.bdy는 뭔지 모르겠습니다; 공의 x와 y값..에 사용하는 것인 듯하지만 미니 게임 주석에서는 dental-position이라는데 이게 대체 뭔지..
아마도 공 이동 속도 및 위치 보정용 값인 듯합니다.
밑에 나오지만 공 위치값을 구할 때 이 값과 속도값을 곱하기도 하고, 공이 게임판 위나 아래에 부딪혔을 때 이 값을 음수로 바꾸더군요.
값을 바꿔봤더니 공 이동 속도가 확 증가하기도 하고요.
self.oldst = None 이전 프레임을 그리기까지 몇 초가 지났는지를 저장합니다.
뒤에 다시 등장합니다만 프레임 하나를 그리기 위해 걸린 시간을 계산하기 위해 필요한 값입니다.
그 값이 왜 필요한지는 render 메소드를 보면 알 수 있습니다.
self.winner = None 이긴 사람을 저장하는 변수입니다.
return [ self.paddle, self.ball, self.player, self.eileen, self.ctb ] visit 메소드는 하위 디스플레이어블 리스트를 반환해야 합니다. 따라서 여기에서도 PongDisplayable 에서 사용하는 모든 하위 디스플레이어블을 리스트 형태로 반환합니다.
렌더 메소드입니다. 디스플레이어블을 실제로 그리기 위해 반드시 재정의해야 하는 메소드죠.
r = renpy.Render(width, height)
디스플레이어블을 그릴 렌더 객체를 만듭니다.
if self.oldst is None: self.oldst = st
dtime = st - self.oldst self.oldst = st 이전 프레임을 그린 뒤로 시간이 얼마나 지나서 현재 프레임을 그렸는지를 계산합니다.
st 는 render 메소드가 기본적으로 받는 shown timebase. 이 디스플레이어블이 그려진 이후로 경과한 시간입니다.
1. 이전 프레임을 그린 시간이 self.oldst 에 저장되지 않았다면 디스플레이어블을 처음 그린 뒤 현재까지 경과한 시간(st)을 self.oldst 에 저장합니다.
2. 현재까지 경과한 시간(st)에서 이전 프레임을 그렸을 때까지 경과한 시간(self.oldst)을 빼면 새 프레임을 그리는 데 걸린 시간(dtime)이 되겠죠.
3. 계산이 다 끝났으니 이전 프레임을 그렸을 때 걸린 시간은 현재까지 경과한 시간을 맞춥니다.
그냥 쉽게 설명하자면 게임이 시작했을 때 아까 본 장면은 게임이 시작한지 29초만에 그려진 것이고
지금 보는 장면은 30초만에 그려진 것이라면 이전 장면에서 현재 장면이 그려지는 데에 걸린 시간은 1초가 될 겁니다.
그리고 현재 장면 이후의 다음 장면을 그리는 데에 걸린 시간을 계산하려면 현재 장면을 그리기까지 걸린 시간이 이전 장면을 그리기까지 걸린 시간으로 만들어야 계산이 가능하겠죠.
(y값은 위치 계산에서 배제하겠습니다.) (0, 0) 위치에 있는 공을 그린 첫 프레임을 표시한 이후로 두 번째 프레임을 표시하기까지 걸린 시간이 3초라고 칩시다. 그런데 공의 속도와 두 번째 장면을 그리는 데에 걸린 시간을 곱하지 않고 3초 뒤에도 300이라는 공의 기본 이동 속도만 위치값에 더하면 공은 두 번째 장면에서 (300, 0) 위치에 있게 됩니다.
다음 프레임, 그 다음 프레임을 그리는 데 걸리는 시간이 2초, 5초인데도 공이 새 프레임이 그려질 때마다 300씩 증가한 위치에 등장한다면 오히려 공의 속도는 일정하지 않은 것이 됩니다. 따라서 프레임을 그리기까지 걸린 시간을 구해서 공의 속도와 곱해주면 공이 1초마다 일정한 속도로 이동하게 되는 것이겠죠.
한 마디로 프레임을 그리기까지 걸린 시간(dtime)과 공 속도(self.bspeed)를 곱한 값을 속도값으로 쓰면 몇 초뒤에 프레임을 그린다한들 공의 속도는 항상 일정한 것으로 만들어주기 때문에 이 작업을 거치는 것입니다.
oldbx = self.bx
공의 현재 x위치는 oldbx에 저장합니다.
공이 판때기에 부딪혔을 때 공이 튕겨나가도록 처리하기 위해 필요한 값으로 밑에 있는 paddle 함수에서 사용합니다.
eileen = renpy.render(self.eileen, 800, 600, st, at) ew, eh = eileen.get_size() r.blit(eileen, (790 - ew, 25)) Eileen이라는 글씨를 그립니다. get_size를 이용한 건 글씨를 그릴 위치를 계산하기 위해서입니다.
if self.stuck: ctb = renpy.render(self.ctb, 800, 600, st, at) cw, ch = ctb.get_size() r.blit(ctb, (400 - cw / 2, 30)) 공이 판때기에 붙어있는 상태, 즉 게임을 시작하기 전이라면 Click to Begin 그림을 그립니다.
if self.bx < -200: self.winner = "eileen" renpy.timeout(0) 공의 x 위치가 -200이면, 즉 왼쪽 화면 밖으로 아예 나가버리면 승자는 아이린입니다. renpy.timeout(0) 은 승자를 알리는 이벤트 메소드가 확실히 호출될 수 있도록 합니다. 이게 딱 -200이 아니라 딱 0이었으면 아마 플레이어가 공을 놓쳤다는 걸 제대로 인식하기도 전에 미니 게임 화면이 사라지니 약간 당황스러울 수도 있을 겁니다.
elif self.bx > 1000: self.winner = "player" renpy.timeout(0) 오른쪽 화면으로 아예 나가버리면 플레이어가 승자입니다.
renpy.redraw(self, 0) 다음 프레임을 바로바로 표시할 수 있도록 즉시 화면을 다시 렌더링합니다.
return r Render 객체를 반환합니다.
_M#]
파이게임 이벤트를 CDD에 전달할 때 호출되는 메소드입니다.
import pygame 파이게임 모듈을 임포트합니다. 밑에 있는 if 문에서 사용하는 pygame.MOUSEBUTTONDOWN 때문에 임포트했습니다.
if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1: self.stuck = False 입력된 이벤트 종류가 마우스 버튼이고, 버튼이 눌렸다면, 즉 마우스 버튼이 눌렸다면, 판때기에 공이 붙어있는 상태를 해제합니다.
y = max(y, self.COURT_TOP) y = min(y, self.COURT_BOTTOM) self.playery = y 플레이어의 판때기가 게임판 밖으로 나가지 않도록 설정해주는 줄입니다. 마우스 포인터의 y 위치와 게임판의 맨 밑 중에 큰 값과 마우스 포인터의 y 위치와 게임판의 맨 밑 가운데에 큰 값을 마우스 포인터 y 값으로 설정하고 플레이어 판때기의 y값으로 이렇게 정리된 y값을 사용합니다.
if self.winner: return self.winner else: raise renpy.IgnoreEvent() 승자가 있다면 승자를 반환하고 (승자 처리는 render 메소드에서 처리) 디스플레이어블 표시를 종료합니다. 승자가 없으면 현재 미니게임에서 발생하는 이벤트를 무시합니다.
'이게 뭔 소리죠'라고 물으셔도 할 말이 없습니다; 글로만 하는 설명에도 한계가 있고 또 스크립트도 복잡해서 이것만 보고는 절대 전부 이해할 수 없을 겁니다. 이 내용은 참고만 하면서 스크립트를 이해하시는 게 훨씬 도움이 될 것 같네요.
0. 매뉴얼을 보고 CDD가 뭐하는 건지 대충만 읽어봅니다. 물론 처음 보면 무슨 소린지 알 수 없습니다. 그래도 그냥 읽습니다. 1. 미니 게임을 직접 해봅니다. 2. 이 게임을 CDD로 만들기 위해 필요한 것이 무엇인지 생각합니다. 3. 이 게임을 구현하기 위한 스크립트를 큰 줄기만 머릿속으로 대충 짜봅니다. 컴퓨터에 대충 끄적여도 봅니다. 4. 전혀 이해가 안 가는 이 글을 읽습니다 5. 게임을 다시 해보면서 어떤 변수가 무엇을 가리키는지, 게임에서 어떻게 적용되는지 정리합니다. 6. 스크립트를 다시 봅니다. 7. 이해가 안 가는 이 글을 읽고 그나마 손톱 만큼 도움이 되는 말을 부분적으로 참고하면서 스크립트를 분석합니다. 8. 5번부터 7번 과정을 반복합니다.