본문 바로가기

렌파이/시스템 추가하기

제작자 정의 디스플레이어블 - 1 단순 디스플레이어블 생성 소스 분석

정의

렌파이의 기본 기능들로는 구현할 수 없는 디스플레이어블 - 미니 게임 등을 직접 만들기 위해서는 제작자 정의 디스플레이어블, 줄여서 CDD을 이용합니다. 명칭이 붙어 있어서 특별하고 특정한 기능처럼 보이지만 그냥 사용자가 임의로 필요한 디스플레이어블을 직접 만들기 위해 필요한 방법 정도로 보면 됩니다. CDD를 이용해 만든 기능이 튜토리얼에 있는 미니 게임인 퐁[각주:1]이고 또 하나는 제가 만들었던 한글 입력기입니다.


저도 잘 모르는데다 ㅇ<-< 어찌 설명해야 할지 몰라 그동안 건드리지 않고 기능인데 더는 별로 설명할 만한 게 안 보여서 적어봅니다; 클래스에 대해서는 매뉴얼에 상세히 적혀있고 이 글은 매뉴얼에 있는 예제 스크립트 분석입니다. 참고로 파이썬에서 클래스에 대한 지식까지는 알고 있어야 그나마 이해하기가 편할 겁니다. 전달하는 입장에서도 전혀 모르는 분들에게 설명하기에는 한계가 있으므로 클래스까지는 알고 있는 분들을 대상으로 글을 적으려 합니다.


제작자 정의 디스플레이어블은 그와 관련된 클래스를 설명하기 보다는 이를 이용한 스크립트를 여러 개 보고 분석하는 식으로 글을 적어보도록 하겠습니다.


스크립트

이 스크립트는 매뉴얼에서 제작자 정의 디스플레이어블을 설명하고자 사용된 예제 스크립트입니다. 매뉴얼에 실려있긴 한데 거기 있는 거 똑같이 긁어온 거라 직접 매뉴얼 가서 확인하실 필요는 없을 듯.


아무튼 이 스크립트는 마우스 포인터가 접근하면 불투명해지고 마우스 포인터가 멀어지면 사라지는 하위 위젯을 만들어내는 제작자 정의 디스플레이어블을 만듭니다.



이렇게 만든 디스플레이어블은 다음과 같이 사용하게 됩니다.




설명


옆에 전체 스크립트를 띄워놓고 설명을 보시면 조금 더 이해하기에 도움이 될 겁니다.


import math

math 모듈을 임포트. 아래에 나오는 math.hypot() 을 사용할 때 필요하니 불러오는 것이죠. 해당 함수는 다시 등장할 때 자세히 적겠습니다. 이 줄은 CDD를 만들 때 꼭 필요한 건 아닙니다.


class Appearing(renpy.Displayable):

새로운 디스플레이어블을 만들기 위해 renpy.Displayable을 상속하는 Appearing 클래스를 생성.

디스플레이어블을 직접 만들기 위해서는 renpy.Displayable 클래스를 상속하는 자식 클래스를 만들어야 합니다. 


def __init__(self, child, opaque_distance, transparent_distance, **kwargs):

이 디스플레이어블에서 받아야 할 값들을 적습니다.


    child 디스플레이어블을 사용할 때 사용자에게서 임의로 하위 위젯 정보를 받아 마우스 포인터와의 거리에 따라 투명도를 조절하는 디스플레이어블이므로 하위 위젯을 입력 받아야겠죠. 마치 텍스트 버튼을 만들 때마다 텍스트를 입력하는 것처럼요.


    opaque_distance 하위 위젯이 불투명하려면 하위위젯에서 마우스 포인터가 얼만큼 멀리 떨어져 있어야 하는지 결정하는 값. 이것도 Appearing 디스플레이어블을 사용할 때 받아야 하는 값입니다.


    transparent_distance 하위 위젯이 투명 상태가 되는 마우스 포인터와의 거리.


    **kwargs 위에 적은 child, opaque_distance, transparent_distance 를 제외한 모든 인수는 기타 키워드 인수(keyword arguments)에 해당하게 됩니다.


super(Appearing, self).__init__(**kwargs)

이 디스플레이어블에서 사용하지 않는 기타 키워드 인수는 모두 Appearing의 부모 클래스, 즉 renpy.Displayable의 생성자에 넘겨줍니다.

미니 게임을 만들 때에는 필요 없는 경우도 있습니다. 자세한건 미니게임 핑퐁 스크립트를 살펴볼 때 한 번 더 설명하도록 하겠습니다.


self.child = renpy.displayable(child)

하위 위젯은 일반 디스플레이어블 객체이니 이렇게 적어줍니다. child를 renpy.displayable로 처리해서 self.child 에 배정. 만약 하위 위젯을 "eileen happy.png" 처럼 적었다면 문자열을 그림 파일 이름으로, "eileen happy"처럼 적었다면 이미지 이름으로 읽습니다. Text("Text") 로 적었다면 글씨로 표시하겠죠


self.opaque_distance = opaque_distance
self.transparent_distance = transparent_distance

받은 값을 클래스에서 써야겠죠.


self.alpha = 0.0
하위 위젯의 투명도. 처음 값을 0으로 줬습니다.

self.width = 0
self.height = 0

하위 위젯의 가로 세로 값입니다. 처음 값은 0이지만 아래 등장할 스크립트 코드를 이용하여 하위 위젯의 크기에 따라 값이 달라지게 됩니다.


def render(self, width, height, st, at):

제작자 정의 디스플레이어블을 만든다면 반드시 재작성해야 하는 메소드가 이 render 메소드입니다.

render 메소드는 CDD를 사용했을 때 게임 화면에 무엇이 어떻게 나타나야할지 결정하는 메소드입니다.

width, height 는 이 렌더의 크기이고 st, at는 표시시간과 동작시간. 자세한 건 매뉴얼을 읽는 게 빠릅니다;;


t = Transform(child=self.child, alpha=self.alpha)

하위 위젯의 투명도 값(alpha)를 조정하는 트랜스폼 t를 만듭니다. 이미지나 텍스트만 덜렁 추가한다면 그 자체만으로 투명도를 조절할 수는 없으므로 트랜스폼을 씌우는 거죠.


child_render = renpy.render(t, width, height, st, at)

위에서 만든 트랜스폼 t를 renpy.render로 그리는 child_render 를 만듭니다.


self.width, self.height = child_render.get_size()

child_render를 get_size()를 통해 크기를 알아내고 그 값을 self.width, self.height 에 저장합니다.


render = renpy.Render(self.width, self.height)

렌더 객체 render를 만듭니다.

renpy.Render 는 CDD를 렌파이 게임 화면에 그리기 위해 필요한 기능입니다.


render.blit(child_render, (0, 0))

Appearing 이라는CDD에 child_render 라는 하위 위젯을 그리기 위해 render.blit(child_render, (0, 0))처럼 적어줍니다. (0, 0) 튜플은 이 렌더를 그릴 위치입니다.


return render 

renpy.Render 객체인 render 를 반환합니다. render 메소드는 renpy.Render 를 반드시 반환해야 합니다.


def event(self, ev, x, y, st):

특정한 상황이 발생했을 때 디스플레이어블을 처리하는 방식을 정의하는 메소드입니다.

ev 는 이벤트 객체이며 x, y는 이벤트의 x, y 좌표값입니다. st 는 이벤트가 시작한 이후 경과한 시간입니다.


distance = math.hypot(x - (self.width / 2), y - (self.height / 2))

마우스 포인터와 하위 위젯 사이의 거리를 계산해 distance에 배정합니다.

아까 임포트했던 math를 여기서 사용하네요. math.hypot()은 원점에서 어느 좌표 사이의 거리를 계산하는 함수입니다. (x * x + y * y) 를 루트 씌워서 계산하네요.

( 마우스 포인터의 x 위치 - 하위 위젯의 가로길이 / 2, 마우스 포인터의 y 위치 - 하위 위젯의 세로 길이 / 2 ) 를 계산하는군요.


if distance <= self.opaque_distance:
    alpha = 1.0

elif distance >= self.transparent_distance:
    alpha = 0.0
else:
    alpha = 1.0 - 1.0 * (distance - self.opaque_distance) / (self.transparent_distance - self.opaque_distance)


위에서 계산한 거리 distance가 하위 위젯이 불투명해야 할 거리보다 적으면 투명도 값이 1.0, 투명해야 할 거리라면 0.0이 됩니다.

이도 저도 아니면 투명도 값은

1.0(불투명상태) 에서 (마우스 포인터와 하위 위젯 중심부와의 거리 - 불투명해야 할 거리) / (투명해야 할 거리 - 불투명해야 할 거리) * 1.0을 뺍니다.


예를 들어 하위 위젯이 불투명해야 할 거리가 100, 투명해야 할 거리가 200이고 현재 마우스 포인터와 하위 위젯의 거리가 50이라고 한다면 투명도는 1.0 - 1.0 * (50 - 100) / (200 - 100) = 0.5 가 되겠네요.


if alpha != self.alpha:

알파값이 변하면 


    self.alpha = alpha

새 알파값을 하위 위젯에 적용하고(트랜스폼 t가 self.alpha 값을 이용하니 self.alpha 값에 새 값을 배정하면 하위 위젯의 알파값이 바뀌게 되겠죠)


    renpy.redraw(self, 0)

화면을 새로 그립니다.


return self.child.event(ev, x, y, st)

하위 디스플레이어블의 이벤트 메소드를 반환합니다... 인데 이 작업은 왜 하는지 모르겠네요;


def visit(self):
    return [ self.child ]

visit 메소드에서는 하위 위젯의 리스트를 반환해야 합니다... 여기서는 하위 위젯이 하나뿐이지만 여러 하위 위젯을 사용하는 디스플레이어블이라면 여러 개를 반환해야합니다.


참고

제작자 정의 디스플레이어블


기타

저조차도 잘 모르는 점이 많아서 제대로 잘 설명했는지 모르겠습니다..-_-;; 언제나 그렇지만 질문과 피드백 모두 환영입니다. 저도 나중에 다시 보고 잘못된 부분이나 보충 설명이 필요한 곳은 거듭 수정해나갈 생각입니다.

  1. 튜토리얼에서 minigames 메뉴를 클릭하면 확인할 수 있습니다. 스크립트는 렌파이 설치 폴더/tutorial/game/demo_minigame.rpy [본문으로]