렌파이에서는 게임 상태를 저장하거나, 저장한 상태를 불러오거나, 이전 게임 상태로 롤백하는 기능을 지원합니다. 구현된 방식은 조금 다르지만, 롤백으로는 사용자의 입력을 기다리는 각 명령문이 시작되었을 때 게임을 저장한 뒤에 사용자가 롤백했을 때 저장한 상태를 불러옵니다.
주석
되도록이면 게임 엔진 버전이 달라도 세이브 파일이 호환될 수 있도록 노력하지만 호환성은 보장되지 않습니다. 세이브 파일 호환성을 없애더라도 더 큰 이득이 있다면 호환성을 포기할 수도 있습니다.
렌파이는 게임 상태를 저장하려 합니다. 이 때 저장하는 상태로는 내부 상태와 파이썬 상태가 있습니다.
내부 상태는 게임이 시작되고 나서 바뀌어야 하는 렌파이의 모습 전부로 이루어져있으며, 아래와 같은 데이터 등이 그 예입니다:
파이썬 상태는 저장 영역에 있는 변수 중에서 게임이 시작되고 나서 변경된 변수 및 해당 변수에서 접근할 수 있는 모든 객체로 이루어져있습니다. 중요한 것은 변수에 대한 변경점만 기록하며 객체의 필드는 바뀌어도 객체는 저장되지 않는다는 점입니다.
예제를 살펴보겠습니다.
define a = 1
define o = object()
label start:
$ b = 1
$ o.value = 42
위 예제 스크립트에서는 b 만 저장합니다. a가 저장되지 않는 까닭은 a는 게임이 시작되고 나서 바뀌지 않았기 때문입니다. 'o` 도 바뀌지 않았기 때문에 저장되지 않습니다. o 객체의 참조가 바뀌었을 뿐이지 변수 자체가 바뀐 것은 아닙니다.
게임이 시작하기 전에 바뀌지 않는 파이썬 변수는 저장되지 않습니다. 만약 어느 변수가 저장되어있는 다른 변수와 같은 객체를 참조하는 경우 문제가 발생할 수 있습니다. 예제를 살펴보겠습니다.
init python:
a = object()
a.f = 1
label start:
$ b = a
$ b.f = 2
"a.f=[a.f] b.f=[b.f]"
위 예제 스크립트에서 a 와 b 는 서로 같은 객체를 참조하고 있습니다. 그러나 저장하거나 불러오는 과정에서 이 상황이 바뀌어, a 와 b 가 서로 다른 객체를 참조하게 됩니다. 이렇게 되면 상당히 헷갈리기 때문에 저장된 변수와 저장되지 않은 변수가 서로 같은 객체를 참조하도록 하지 말아야 합니다. (위와 같은 상황이 발생할 가능성은 낮지만 저장되지 않은 변수와 저장된 필드가 같은 객체를 가리킬 때 문제가 일어날 수 있습니다.)
저장되지 않는 상태의 종류에는 여러가지가 있습니다. :
렌파이는 인터렉션이 일어나는 상황에서 가장 마지막 인터렉션을 일으키는 명령문이 시작될 때 저장을 합니다.
여기서 기억해야할 점은 명령문이 시작 될 때 저장을 한다는 사실입니다. 인터렉션이 여러 번 발생하는 명령문을 실행하는 중에 불러오거나 롤백하면, 명령문이 시작되었을 때 활성화된 상태가 렌파이가 저장하는 상태가 됩니다.
파이썬을 이용해 정의한 명령문에서 이런 문제가 발생할 수 있습니다.:
python:
i = 0
while i < 10:
i += 1
narrator("카운트는 이제 [i].")
위 코드에서 사용자가 중간에 게임을 저장하고 불러오면 루프가 새로 시작됩니다. 파이썬 대신 이 문제는 렌파이에서 비슷한 코드를 작성하는 것으로 피할 수 있습니다.:
$ i = 0
while i < 10:
$ i += 1
"카운트는 이제[i]."
렌파이는 게임 상태를 저장하기 위해 파이썬 피클 시스템을 사용합니다. 피클 모듈로 저장할 수 있는 데이터는 다음과 같습니다.:
다음과 같은 타입은 피클할 수 없습니다.:
기본적으로 렌파이는 게임을 저장할 때 cPickle 모듈을 사용합니다.
config.use_cpickle
변수값을 바꾸면 피클 모듈을 대신 사용합니다.
게임이 느려지지만 저장할 때 발생하는 에러를 더 잘 보고합니다.
고급 세이브 시스템에서 사용할 수 있는 변수는 하나입니다.:
save_name
= ...¶각 세이브에 저장할 문자열. 세이브에 이름을 지정해 세이브들이 서로 다르다는 정보를 사용자에게 알려줄 수 있다.
여러가지 고급 액션이나 기능은 스크린 액션 페이지에서 확인할 수 있습니다. 다음은 하급 세이브 및 로드 액션 목록입니다.
renpy.
can_load
(filename, test=False)¶filename 을 불러올 수 있다면 참을, 아니라면 거짓을 반환한다.
renpy.
list_saved_games
(regexp='.', fast=False)¶저장된 게임을 목록으로 만든다. 각 게임 세이브가 반환하는 튜플에는 다음과 같은 데이터가 들어있다.:
디스플레이어블.
renpy.
list_slots
(regexp=None)¶비어있지 않은 세이브 슬롯의 리스트를 반환한다. regexp 가 존재한다면, regexp 로 시작하는 슬롯만 반환된다. 슬롯은 문자열 순서대로 정렬된다.
renpy.
load
(filename)¶filename 에서 게임 상태를 불러온다. 불러온 후에는 이 함수를 불렀던 이전 상태로 돌아갈 수 없다.
renpy.
newest_slot
(regexp=None)¶가장 최신 세이브 슬롯(수정된 시간이 가장 최근인 세이브 슬롯)의 이름을 반환한다. 없다면 None을 반환한다.
regexp 가 존재한다면, regexp 로 시작하는 슬롯만 반환된다.
renpy.
save
(filename, extra_info='')¶세이브 슬롯에 게임 상태를 저장한다.
save_name
변수의 값이다.이 함수를 호출하기 전에 renpy.take_screenshot()
를 먼저 호출해야 한다.
renpy.
take_screenshot
(scale=None, background=False)¶스크린샷을 찍는다. 이 스크린샷은 게임 세이브의 일부로서 저장된다.
renpy.
unlink_save
(filename)¶filename 세이브를 지운다.
게임이 불려왔을 때 게임 상태는 (아래에서 설명한 롤백 시스템을 사용해) 현재 명령문이 실행을 시작할 때의 게임 상태로 초기화됩니다.
간혹 이런 작동 방식이 발생하지 않아야 할 경우가 있습니다. 예를 들어 값을 수정할 수 있는 어떤 스크린이 있을 때 게임을 불러온 후에도 이 값을 유지해야하는 경우가 있습니다. renpy.retain_after_load 를 호출하면, 데이터가 게임이 저장되었을 때의 상태로 복원되지 않으며 다음 체크포인트 인터렉션이 종료하기 전 상태로 불려올 것입니다.
데이터를 변경하지 않았을 때에도 제어 흐름이 현재 명령문 시작 상태로 초기화됨을 기억해야 합니다. 해당 명령문은 명령문의 시작 상태에 존재하는 데이터로 다시 실행될 것입니다.
다음은 예제입니다:
screen edit_value:
hbox:
text "[value]"
textbutton "+" action SetVariable("value", value + 1)
textbutton "-" action SetVariable("value", value - 1)
textbutton "+" action Return(True)
label start:
$ value = 0
$ renpy.retain_after_load()
call screen edit_value
renpy.
retain_after_load
()¶불러오기가 발생할 때 현재 명령문과 다음 체크포인트를 포함하는 명령문 사이에 수정된 데이터를 유지시킨다.
최신 프로그램 대부분에 있는 되돌리기/다시하기 기능과 비슷하게 사용자는 롤백 기능으로 게임을 이전 상태로 되돌릴 수 있습니다. 롤백 이벤트가 일어나는 동안 게임 화면이나 게임에서 사용하는 변수를 시스템이 알아서 관리하긴 합니다만, 여러분이 게임을 만들 때 알아두어야 할 사항이 몇 가지 있습니다.
렌파이 명령문은 대부분 알아서 롤백과 롤포워드 기능을 지원합니다. ui.interact()
함수를
직접 호출한다면 롤백과 롤포워드 기능을 수동으로
추가해야 합니다. 다음과 같은 구조로 만들 수 있습니다.:
# 롤백 하지 않는다면 None. 롤포워드 하고 있다면
# 마지막 체크포인트를 지나온 후부터 경과된 시간이 값이 된다.
roll_forward = renpy.roll_forward_info()
# 여기에 스크린을 설정한다...
# 사용자에게 인터렉션을 받는다.
rv = ui.interact(roll_forward=roll_forward)
# 인터렉션 결과를 저장한다.
renpy.checkpoint(rv)
renpy.checkpoint를 호출하고 난 뒤에는 사용자가 게임과 상호작용 할 수 없어야 합니다. (만약 상호작용 한다면 사용자가 롤백을 할 수 없을 것입니다.)
renpy.
can_rollback
()¶롤백할 수 있다면 참을 반환한다.
renpy.
checkpoint
(data=None)¶현재 명령문을 사용자가 롤백할 수 있는 체크포인트로 만든다. 이 함수가 한 번 호출되고 나면 현재 명령문에서는 사용자가 더 이상 인터렉션을 할 수 없어야 한다.
renpy.roll_forward_info()
가
이 데이터를 반환한다.renpy.
in_rollback
()¶롤백이 실행되었다면 참을 반환한다.
renpy.
roll_forward_info
()¶롤백 상태일 때 마지막으로 이 명령문이 실행되었을 때 renpy.checkpoint()
에 전달된 데이터를 반환한다. 롤백 상태가 아니라면 None을 반환한다.
renpy.
rollback
(force=False, checkpoints=1, defer=False, greedy=True, label=None)¶이전 체크포인트로 게임 상태를 롤백한다.
경고
롤백을 막으면 사용자는 매우 불편해집니다. 사용자가 실수로 다른 선택지를 클릭했을 경우에 실수를 고칠 방법이 없어집니다. 롤백이란 저장하기나 불러오기와 같은 기능이기 때문에 사용자가 자주 저장하게 되어 게임 흐름이 끊어지게 됩니다.
롤백은 일부 구간에서만 비활성화하거나 게임 플레이 내내 비활성화할 수 있습니다.
롤백할 수 없도록 만들고 싶다면 config.rollback_enabled
변수로
비활성화할 수 있습니다.
롤백을 아예 막기보다는 롤백을 일부 구간에서만 막는 편이 더 흔합니다. 이는
renpy.block_rollback()
함수로 제어할 수 있습니다. 이 함수가 호출되면
렌파이는 해당 지점으로 롤백하지 않습니다. 예를 들어:
label final_answer:
"그것이 마지막 대답인가?"
menu:
"네":
jump no_return
"아뇨":
"우리에겐 네가 입을 열게 할 방법이 많아."
"그러니 잘 생각해봐야 할 거다."
"한번 더 묻겠다..."
jump final_answer
label no_return:
$ renpy.block_rollback()
"그렇다면 좋다. 이제는 무를 수 없어."
위 예제에서 no_return 레이블에 도달하면 이전 선택지로 롤백할 수 없게 됩니다.
롤백 고정 기능은 블록 방지 기능과 롤백 기능의
중간이라고 할 수 있는 기능입니다. 롤백은
가능하지만, 한 번 고른 선택지는 다시 무를 수
없습니다. 롤백 고정은 아래 예제에 나온 것처럼 renpy.fix_rollback()
함수로 설정할 수 있습니다.
label final_answer:
"그것이 마지막 대답인가?"
menu:
"네":
jump no_return
"아뇨":
"우리에겐 네가 입을 열게 할 방법이 많아."
"그러니 잘 생각해봐야 할 거다."
"한번 더 묻겠다..."
jump final_answer
label no_return:
$ renpy.fix_rollback()
"그렇다면 좋다. 이제는 무를 수 없어."
위 예제에서는 fix_rollback 함수가 호출된 뒤에 선택지 화면으로 롤백할 수는 있습니다. 하지만 다른 선택문을 고를 수는 없게 됩니다.
fix_follback 기능을 사용하는 게임을 기획할 때 생각해보아야 할 주의사항이
몇 가지 있습니다. 렌파이는 checkpoint()
함수에 전달된 데이터를
전부 자동으로 암호화합니다. 그러나 렌파이의 특성상
파이썬 코드로 이 기능을 우회하고 예상치 못한 결과가
발생할 수 있는 방법으로 렌파이 엔진를 바꿀 수도 있습니다. 이를 방지하기 위해
추가로 코드를 작성하거나 문제가 발생할 수 있는 부분에
롤백을 막는 일은 게임 기획자에게 달려있습니다.
선택지, renpy.input()
, renpy.imagemap()
등 내부 유저 인터렉션도
fix_rollback 함수에 따라 입력한 내용이 고정됩니다.
fix_rollback함수를 사용하면 선택지와 이미지맵의 작동방식이 바뀌므로
이러한 변경사항을 게임 화면에 나타내야 합니다. 이를 위해서는
선택문 버튼의 상태가 어떻게 바뀌는지를 이해하는 것이
중요합니다. config.fix_rollback_without_choice
옵션에서
선택할 수 있는 모드는 두 가지가 있습니다.
기본적으로는 선택된 선택문을 "selected"로 설정하여 스타일 속성 이름 앞에 "selected_" 를 붙여 활성화하는 것입니다. 다른 버튼은 전부 "insensitive_" 를 스타일 속성 앞에 붙여 비활성 상태로 만듭니다. 이렇게 하면 선택할 수 있는 선택문은 하나만 남게 됩니다.
config.fix_rollback_without_choice
를
False로 설정하면 모든 버튼이 비활성 상태가 됩니다. 선택된 선택지는
"selected_insensitive_" 접두사가 붙은 스타일 속성을
사용하고 다른 버튼은 전부 "insensitive_" 접두사가 붙은
스타일 속성을 사용합니다.
fix_rollback 시스템을 잘 다루어야 하는
파이썬 루틴을 만들 때 알아두어야 할 점이 몇가지 있습니다. 우선
renpy.in_fixed_rollback()
함수는 현재 게임이 롤백 고정 상태이어야 하는지
결정하기 위해 사용될 수 있습니다. 둘째로,
롤백 고정 상태일 때 ui.interact()
는 어떤 액션이
실행되든 항상 roll_forward 데이터를 반환합니다. 즉,
ui.interact()
/ renpy.checkpoint()
함수가 사용될 때는
이미 작업이 대부분 완료된 상태라는 뜻입니다.
맞춤 스크린을 간단하게 만들 수 있도록 흔하게 사용할 수 있는
두 가지 액션을 마련해두었습니다 ui.ChoiceReturn()
액션은
이 액션이 적힌 버튼이 클릭되었을 때의 값을 반환합니다.
ui.ChoiceJump()
액션은 특정 스크립트 레이블로 넘어가도록 할 때 사용할 수 있습니다. 그러나 이
두 액션은 오직 스크린이 call screen
명령문으로 호출되었을 때만
제대로 작동합니다.
예제:
screen demo_imagemap:
imagemap:
ground "imagemap_ground.jpg"
hover "imagemap_hover.jpg"
selected_idle "imagemap_selected_idle.jpg"
selected_hover "imagemap_hover.jpg"
hotspot (8, 200, 78, 78) action ui.ChoiceJump("swimming", "go_swimming", block_all=False)
hotspot (204, 50, 78, 78) action ui.ChoiceJump("science", "go_science_club", block_all=False)
hotspot (452, 79, 78, 78) action ui.ChoiceJump("art", "go_art_lessons", block_all=False)
hotspot (602, 316, 78, 78) action uiChoiceJump("home", "go_home", block_all=False)
예제:
python:
roll_forward = renpy.roll_forward_info()
if roll_forward not in ("Rock", "Paper", "Scissors"):
roll_forward = None
ui.hbox()
ui.imagebutton("rock.png", "rock_hover.png", selected_insensitive="rock_hover.png", clicked=ui.ChoiceReturn("rock", "Rock", block_all=True))
ui.imagebutton("paper.png", "paper_hover.png", selected_insensitive="paper_hover.png", clicked=ui.ChoiceReturn("paper", "Paper", block_all=True))
ui.imagebutton("scissors.png", "scissors_hover.png", selected_insensitive="scissors_hover.png", clicked=ui.ChoiceReturn("scissors", "Scissors", block_all=True))
ui.close()
if renpy.in_fixed_rollback():
ui.saybehavior()
choice = ui.interact(roll_forward=roll_forward)
renpy.checkpoint(choice)
$ renpy.fix_rollback()
m "[choice]!"
renpy.
block_rollback
()¶게임에서 현재 명령문 이전으로 롤백하지 못하도록 막는다.
renpy.
fix_rollback
()¶이 명령문을 사용하기 이전에 내린 결정은 사용자가 바꿀 수 없도록 막는다.
renpy.
in_fixed_rollback
()¶현재 롤백 중이며 현재 상황이 renpy.fix_rollback() 명령문을 실행하기 전이라면 참을 반환한다.
ui.
ChoiceJump
(label, value, location=None, block_all=None)¶`value`를 반환하는 선택문 액션. 롤백 고정 기능에 맞게 버튼 상태를 관리한다. (작동 방식에 대한 설명은 block_all 부분을 참조하라.)
False면 선택된 선택지일 때 버튼은 selected 상태가 되며 선택된 선택지가 아니라면 insensitive 상태가 된다.
True면 버튼은 롤백 고정 상태일 때 항상 insensitive 상태이다.
None이면 config.fix_rollback_without_choice
변수에서
값을 받는다.
화면에 있는 항목에 전부 True값을 지정하면
선택지를 클릭할 수 없게 된다(하지만 롤포워드 기능은 사용할 수 있다).
ui.interact()
를 호출하기 전에
ui.saybehavior()
함수를 호출하면 이런 작동방식을 바꿀 수도 있다.
ui.
ChoiceReturn
(label, value, location=None, block_all=None)¶`value`를 반환하는 선택문 액션. 롤백 고정 기능에 맞게 버튼 상태를 관리한다. (작동 방식에 대한 설명은 block_all 부분을 참조하라.)
False면 선택된 선택지일 때 버튼은 selected 상태가 되며, 선택된 선택지가 아니라면 insensitive 상태가 된다.
True면 버튼은 롤백 고정상태일 때 항상 insensitive 상태이다.
None이면 config.fix_rollback_without_choice
변수에서
값을 받는다.
화면에 있는 항목에 전부 True값을 주면
선택지를 클릭할 수 없게 된다(하지만 롤포워드 기능은 사용할 수 있다).
ui.interact()
를 호출하기 전에
ui.saybehavior()
함수를 호출하면 이런 작동방식을 바꿀 수도 있다.
NoRollback
¶이 클래스를 상속 받는 클래스의 인스턴스는 롤백 과정에 참여하지 않는다. NoRollBack 클래스의 인스턴스를 통해 접근할 수 있는 객체만이 다른 경로를 통해 접근할 수 있을 때에 롤백 과정에 참여한다.
예제:
init python:
class MyClass(NoRollback):
def __init__(self):
self.value = 0
label start:
$ o = MyClass()
"어서와!"
$ o.value += 1
"o.value는 [o.value]. 네가 롤백한 다음에 클릭할 때마다 이 값은 증가할 거야."