Python 문
Ren'Py는 Python 프로그래밍 언어로 작성되었으며, Ren'Py 스크립트 안에서 Python을 사용할 수 있습니다. Python은 플래그 설정부터 새로운 디스플레이어블 생성까지 다양한 작업에 활용할 수 있습니다. 이 장에서는 여러 Python 문을 통해 Ren'Py 스크립트에서 Python을 직접 실행하는 방법을 설명합니다.
Ren'Py는 버전 8.4부터 Python 3.12를 지원합니다. 앞으로 출시되는 버전에서는 사용하는 Python 버전이 올라갈 수 있습니다. Ren'Py 스크립트는 Python 3.12 이상과 호환되도록 작성해야 합니다.
참고
Python을 알고 있다면 그 지식을 활용할 수 있습니다. 다만 Python에 대해 알고 있는 모든 내용이 그대로 적용되는 것은 아닙니다. 예를 들어 Ren'Py에 포함되지 않은 Python 패키지는 Ren'Py 안에서 작동하지 않을 수 있습니다.
작동은 하지만 저장 과정에서 문제를 일으킬 수 있는 Python 구문도 있습니다. 자세한 내용은 저장, 불러오기, 롤백 페이지, 특히 저장할 수 없는 것 절을 읽어 보세요. 파일, 소켓, 이터레이터, 태스크, 퓨처, 제너레이터를 사용할 때는 주의해야 합니다.
또한 많은 문에 Python으로 된 동등한 기능이 있지만, 그 기능을 사용하는 쪽이 불리할 수 있습니다. 예를 들어 Ren'Py는 show 문을 예측해 이미지를 미리 불러올 수 있지만, renpy.show() 함수는 예측할 수 없습니다.
Python
python 문은 Python 코드 블록을 받아, 실행 흐름이 해당 문에 도달하면 블록을 실행합니다. 기본적인 Python 문은 다음처럼 아주 간단하게 작성할 수 있습니다.
python:
flag = True
필요에 따라 더 복잡한 Python 문을 작성할 수도 있습니다.
python:
player_health = max(player_health - damage, 0)
if enemy_vampire:
enemy_health = min(enemy_health + damage, enemy_max_health)
Python 문에는 동작을 바꾸는 두 가지 한정자가 있습니다.
hidehide 한정자를 지정하면 Python 코드 블록이 익명 스코프에서 실행됩니다. 이 스코프는 Python 블록이 끝나면 사라집니다.
이를 통해 저장할 수 없는 임시 변수를 Python에서 사용할 수 있습니다. 다만 스토어에 직접 접근하는 대신 스토어 객체의 필드로 접근해야 합니다.
inin한정자는 이름을 받습니다. Python 코드는 기본 스토어 대신 해당 이름의 스토어에서 실행됩니다.
한 줄 Python 문
기본 스토어에서 Python 코드 한 줄을 실행하는 경우는 흔합니다. 예를 들어 한 줄짜리 Python 코드로 플래그를 초기화하거나 갱신할 수 있습니다. 이런 코드를 더 편리하게 작성할 수 있도록 한 줄 Python 문이 제공됩니다.
한 줄 Python 문은 달러 기호 $로 시작하며, 그 줄의 나머지 내용을 모두 포함합니다. 다음은 한 줄 Python 문의 예입니다.
# Set a flag.
$ flag = True
# Initialize a variable.
$ romance_points = 0
# Increment a variable.
$ romance_points += 1
# Call a function that exposes Ren'Py functionality.
$ renpy.movie_cutscene("opening.ogv")
한 줄 Python 문은 항상 기본 스토어에서 실행됩니다.
init python 문
init python 문은 게임을 불러오기 전 초기화 시점에 Python 코드를 실행합니다. 클래스와 함수를 정의하거나 스타일, 설정 변수, 영구 데이터를 초기화하는 등 다양한 용도로 사용할 수 있습니다.
init python:
def auto_voice_function(ident):
return "voice/" + ident + ".ogg"
config.auto_voice = auto_voice_function
if persistent.endings is None:
persistent.endings = set()
init 1 python:
# The bad ending is always unlocked.
persistent.endings.add("bad_ending")
init과 python 사이에 우선순위 숫자를 지정할 수 있습니다. 지정하지 않으면 0을 사용합니다. 초기화 문은 우선순위가 낮은 것부터 높은 것 순서로 실행됩니다. 우선순위가 같으면 파일 경로의 유니코드 순서로 실행하고, 같은 파일 안에서는 위에서 아래로 실행합니다.
Ren'Py와의 충돌을 피하려면 제작자는 -999부터 999까지의 우선순위를 사용해야 합니다. 0보다 작은 우선순위는 일반적으로 라이브러리와 테마 설정에 사용합니다. 일반적인 초기화 문의 우선순위는 0 이상이어야 합니다.
init python 문에도 hide 또는 in 절을 사용할 수 있습니다.
init python 블록에서 값을 설정한 변수는 저장하거나 불러오지 않으며 롤백에도 참여하지 않습니다. 따라서 초기화가 끝난 뒤에는 이러한 변수를 변경하지 않아야 합니다.
경고
Ren'Py 안에서 생성한 클래스 중 아무것도 상속하지 않거나 object를 명시적으로 상속한 클래스와 그 하위 클래스는 __slots__를 지원하지 않습니다. 이를 사용하려 하면 이전 Ren'Py 버전에서는 롤백이 잘못 작동하고, 최신 버전에서는 오류가 발생합니다.
슬롯을 사용하는 클래스를 만들려면 롤백을 지원하지 않는 python_object를 명시적으로 상속해야 합니다.
define 문
define 문은 초기화 시점에 하나의 변수에 값을 설정합니다. 이 변수는 상수로 취급하므로 값을 설정한 뒤 변경하지 않아야 합니다. 예를 들어 다음 코드는
define e = Character("에일린")
아래 코드와 같습니다. 다만 몇 가지 장점이 있으며, 아래에서 설명합니다.
init python:
e = Character("에일린")
define 문에서는 변수 이름 앞에 이름 있는 스토어와 점을 붙여 스토어를 선택적으로 지정할 수 있습니다. 아래에서 자세히 설명합니다. 스토어가 아직 없으면 생성합니다. 예를 들면 다음과 같습니다.
define character.e = Character("에일린")
define 문에 인덱스를 선택적으로 지정하면 딕셔너리에 항목을 추가할 수 있습니다.
define config.tag_layer["eileen"] = "master"
define에는 = 외에도 두 연산자를 사용할 수 있습니다. += 연산자는 더하기를 수행하며, 일반적으로 리스트를 이어 붙이는 데 사용합니다. OR 연산자인 |=는 일반적으로 집합을 합치는 데 사용합니다. 예를 들면 다음과 같습니다.
define config.keymap["dismiss"] += [ "K_KP_PLUS" ]
define endings |= { "best_ending" }
define 문은 define과 변수 이름 사이에 초기화 우선순위를 선택적으로 받습니다. 지정하지 않으면 0을 사용합니다. 예를 들면 다음과 같습니다.
define -1 config.image_directories = [ "images", "dlc/images" ]
define 문을 사용하면 대입이 이루어진 파일명과 줄 번호를 기록하고, 런처의 탐색 기능에서 이 정보를 활용할 수 있습니다. 또 Lint가 정의된 값을 검사할 수 있습니다. 예를 들어 같은 변수가 두 번 정의되었는지, 그 값이 서로 다를 가능성이 있는지 감지할 수 있습니다.
define 문으로 정의한 변수는 상수로 취급하며 저장하거나 불러오지 않으므로 변경하지 않아야 합니다. 이러한 상수 특성은 해당 변수에서 필드 접근과 인덱싱을 통해 도달할 수 있는 객체에도 적용됩니다. Ren'Py가 이를 강제하지는 않지만, 지키지 않으면 동작이 정의되지 않습니다.
default 문
default 문은 게임을 시작하거나 새 게임을 불러온 뒤 해당 변수가 정의되어 있지 않으면 하나의 변수에 값을 설정합니다. 예를 들면 다음과 같습니다.
default points = 0
게임을 시작할 때 points 변수가 정의되어 있지 않다면 이 문은 대략 다음과 같습니다.
label start:
$ points = 0
실제로 변수에 값을 설정하는 시점은 시작 화면과 메인 메뉴를 실행하기 전입니다. 게임을 불러올 때 points 변수가 정의되어 있지 않다면 다음과 같습니다.
label after_load:
$ points = 0
default 문에서는 변수 이름 앞에 이름 있는 스토어와 점을 붙여 스토어를 선택적으로 지정할 수 있습니다. 아래에서 자세히 설명합니다. 스토어가 아직 없으면 생성합니다. 예를 들면 다음과 같습니다.
default schedule.day = 0
define 문과 마찬가지로 Lint는 default 문과 관련된 검사와 최적화를 제공합니다.
참고
게임에서 변경될 수 있는 모든 변수를 default로 선언할 것을 강력히 권장합니다. init python이나 define으로 변수를 선언한 경우, 플레이어가 게임 중 그 값을 바꾼 뒤 메인 메뉴로 돌아가 새 게임을 시작하면 변수는 init python에서 설정한 값으로 돌아가지 않습니다. 그 결과 이전 게임의 상태가 새 게임으로 새어 들어갑니다. 대신 start 레이블에서 이러한 변수를 만들면, 변수가 추가되기 전에 만든 저장 파일을 불러올 때 변수가 존재하지 않게 됩니다.
스토어의 이름
Ren'Py가 Python 변수를 저장하는 기본 위치를 스토어라고 합니다. 스토어에서 사용하는 이름이 서로 충돌하지 않도록 해야 합니다.
define 문은 캐릭터를 정의할 때도 변수에 값을 대입합니다. 따라서 캐릭터와 플래그에 같은 이름을 사용할 수 없습니다.
다음의 잘못된 스크립트는
define e = Character("에일린")
label start:
$ e = 0
e "안녕, 세상아."
$ e += 1
e "1점을 얻었어요!"
e 변수를 캐릭터와 플래그로 동시에 사용하므로 작동하지 않습니다. 화면 전환과 트랜스폼도 일반적으로 스토어에 저장합니다.
밑줄 _로 시작하는 이름은 Ren'Py 내부 용도로 예약되어 있습니다. 예약된 이름 색인도 참고하세요.
그 밖의 이름 있는 스토어
이름 있는 스토어를 사용하면 Python 함수와 변수를 모듈 단위로 정리할 수 있습니다. Python 코드를 이름 있는 스토어에 넣으면 이름 충돌 가능성을 줄일 수 있습니다. 각 스토어는 Python 모듈 하나에 해당합니다. 기본 스토어는 store이며, 이름 있는 스토어는 store.named 형태로 접근합니다.
이름 있는 스토어는 python in 블록이나 그 변형인 init python, python early 블록으로 만들 수 있습니다. default, define, transform 문으로도 만들 수 있습니다. 스토어의 변수는 from store.named import variable로 개별적으로 가져올 수 있고, 스토어 자체는 from store import named로 가져올 수 있습니다.
python, init python, python early에 in 절을 지정하면 이름 있는 스토어에 접근할 수 있습니다. 이 문들은 모두 포함된 Python 코드를 지정한 스토어에서 실행합니다.
예를 들면 다음과 같습니다.
init python in mystore:
serial_number = 0
def serial():
global serial_number
serial_number += 1
return serial_number
default character_stats.chloe_substore.friends = {"Eileen",}
label start:
$ serial = mystore.serial()
if "Lucy" in character_stats.chloe_substore.friends:
chloe "루시는 내 친구예요!"
elif character_stats.chloe_substore.friends:
chloe "친구는 있지만, 루시는 내 친구가 아니에요."
python in character_stats.chloe_substore:
friends.add("Jeremy")
Python과 마찬가지로 가져오기(import)는 별칭을 만드는 것이 아니라 이름을 다시 바인딩합니다. 이름 있는 스토어에서 가져온 변수의 값을 갱신해도 스토어 안의 변수는 갱신되지 않습니다. 예를 들면 다음과 같습니다.
init python in mystore:
value = 0
init python:
from store.mystore import value
label start:
$ value = 1
"[value]은 1이지만, [mystore.value]은 여전히 0이에요."
python in 블록에서는 renpy.store 또는 import store로 기본 외부 스토어에 접근할 수 있습니다.
이름 있는 스토어는 기본 스토어와 같은 방식으로 저장, 불러오기, 롤백에 참여합니다. persistent, config, renpy 등의 특수 네임스페이스는 내부에 하위 스토어를 생성하는 기능을 지원하지 않으며, 이전에도 지원한 적이 없습니다.
상수 스토어
이름 있는 스토어에서 _constant라는 변수에 참인 값을 설정하면 해당 스토어를 상수로 선언할 수 있습니다. 예를 들면 다음과 같습니다.
init python in mystore:
_constant = True
스토어가 상수이면 그 안의 변수는 저장하지 않으며, 오직 그 변수들을 통해서만 도달할 수 있는 객체는 롤백에 참여하지 않습니다.
상수 스토어의 변수는 초기화 단계에서 변경할 수 있습니다. define, transform 등의 문을 포함한 초기화가 모두 끝난 뒤부터 스토어를 상수로 취급해야 합니다.
Ren'Py는 이를 강제할 방법이 없으므로, 초기화 단계가 끝난 뒤 상수 스토어의 변수가 바뀌지 않도록 보장할 책임은 제작자에게 있습니다.
각 스토어와 변수는 저장, 불러오기, 롤백을 지원하기 위해 약간의 부가 비용이 발생합니다. 스토어를 상수로 선언하면 이 비용을 없앨 수 있습니다.
다음 스토어는 기본적으로 상수로 선언되어 있습니다.
_errorhandling
_gamepad
_renpysteam
_warper
audio
achievement
build
director
iap
layeredimage
updater
JSONDB
- class JSONDB(filename, default=None)
JSONDB는 데이터를 JSON으로 저장하는 2단계 데이터베이스입니다. 게임 개발자가 게임 스크립트의 일부로 버전 관리할 수 있는 데이터베이스에 데이터를 저장하도록 설계되었습니다. 예를 들어 각 say 문과 관련된 정보를 저장해 해당 say 문의 표시 방식을 바꿀 수 있습니다.
JSONDB는 플레이어의 행동을 통해 또는 그 결과로 바뀌는 데이터를 위한 것이 아닙니다. 이러한 데이터에는 영구 데이터나 일반 저장 파일이 더 적합합니다.
데이터베이스에는 Python이 JSON으로 직렬화할 수 있는 데이터만 넣어야 합니다. 리스트, 문자열 키를 사용하는 딕셔너리, 문자열, 숫자, True, False, None이 여기에 포함됩니다. 상호 운용성, 두 형식 사이의 데이터 변환, 관련 주의 사항은 Python 문서를 참고하세요.
데이터베이스의 두 단계는 모두 문자열을 키로 사용하는 딕셔너리입니다. 첫 번째 단계는 읽기 전용입니다. 첫 번째 단계 딕셔너리의 키에 접근하면 두 번째 단계 딕셔너리가 만들어지며, 기본 내용을 지정할 수도 있습니다. 두 번째 단계 딕셔너리는 읽고 쓸 수 있습니다. 두 번째 단계 딕셔너리의 키 중 하나를 변경하면 게임을 종료할 때 변경 사항이 데이터베이스에 저장됩니다.
다른 영구 데이터와 마찬가지로 JSONDB는 롤백에 참여하지 않습니다.
JSONDB는 초기화 시점에 init python 블록이나 define 문으로 생성해야 합니다. 딕셔너리에서 하나 이상의 키에 값을 설정하면 자동으로 디스크에 저장됩니다. 예를 들면 다음과 같습니다.
define balloonData = JSONDB("balloon.json", default={ "enabled" : False })
- 이 코드는 balloon.json 파일에 저장되는 JSONDB를 생성하며,
enabled의 기본값을 False로 설정합니다. 두 번째 단계의 값은 일반 딕셔너리처럼 사용할 수 있습니다.
screen say(who, what): default bd = balloonData[renpy.get_translation_identifier()] if bd["enabled"]: use balloon_say(who, what) else: use adv_say(who, what) if config.developer: textbutton "말풍선 대사 모드": action ToggleDict(bd, "enabled")
JSONDB 생성자는 다음 인수를 받습니다.
- filename
데이터베이스를 저장할 파일명입니다. game 디렉터리를 기준으로 하는 상대 경로이며, 파일명을 ".json"으로 끝내는 것을 권장합니다.
- default
None이 아니라면 딕셔너리여야 합니다. 새로운 두 번째 단계 딕셔너리를 만들 때 이 객체를 얕게 복사하여 새 딕셔너리를 초기화합니다. 새 딕셔너리는 그 안의 키가 하나 이상 저장되어야 데이터베이스의 일부로 저장됩니다.
게임 자체 및 외부 Python 모듈과 패키지
Ren'Py는 순수 Python 모듈과 패키지를 가져올 수 있습니다. 게임용으로 직접 작성한 모듈과 패키지는 game 디렉터리에 바로 넣을 수 있습니다. 외부 패키지는 game/python-packages 디렉터리에 넣을 수 있습니다.
예를 들어 python-dateutil 패키지를 설치하려면 게임의 기본 디렉터리로 이동한 뒤 다음 명령을 실행하면 됩니다.
pip install --target game/python-packages python-dateutil
어느 경우든 init python 블록에서 모듈이나 패키지를 가져올 수 있습니다.
init python:
import dateutil.parser
모든 Python 패키지가 Ren'Py와 호환되는 것은 아닙니다. 설치한 패키지를 검토하고 제대로 작동하는지 확인할 책임은 사용자에게 있습니다.
롤백과 isinstance
.rpy 파일에 정의된 Python 코드는 롤백이 작동하도록 변환됩니다. .py 파일에서 가져온 Python 코드는 변환되지 않습니다. 따라서 가져온 Python 코드에서 만든 객체는 롤백이 작동하지 않으며, 생성 후에는 변경하지 않는 것이 좋습니다.
이 변환 과정에서 Ren'Py는 dict, list, set 클래스를 롤백을 지원하는 자체 버전으로 바꿉니다. 이 클래스들은 내장 클래스의 하위 클래스이므로 모든 메서드가 작동합니다. 그러나 Python 내장 dict, list, set 클래스의 인스턴스는 이 클래스들의 인스턴스로 판정되지 않습니다.
이 문제를 해결할 수 있도록 Ren'Py는 Python 클래스를 제공합니다.
- class python_dict
python_dict 클래스는 Python 내장 dict 클래스입니다.
- class python_list
python_list 클래스는 Python 내장 list 클래스입니다.
- class python_set
python_set 클래스는 Python 내장 set 클래스입니다.