0%

Renpy坐牢记录

这里是捣鼓renpy时的自用代码笔记.
虽说是笔记,图省力有些内容直接copy&paste各路教程,如果没标注原文链接就是忘了(对不起).




笔记

代码structure建议.

更改对话框

参数在gui.rpy的Dialogue section下.
没什么特别的,更换图片,按着参数慢慢改.



文字描边

我在同样是gui.rpy的dialogue section下自加了一下代码.实现的是全文描边.

1
2
3
define gui.dialogue_text_outlines = [ (0, "#4a0303", 1, 1) ]
define gui.charaters_text_outlines = [ (0, "#4a0303", 1, 1) ]
define gui.name_text_outlines = [ (0, "#4a0303", 1, 1) ]

gui.dialogue_text_outlines应该是系统自带参数.

关于single word描边,使用部分outline.

1
"You {outlinecolor=#FFC}did{/outlinecolor} it!"


开屏页面(splashscreen)

Renpy doc.
在script.rpy加就好.
但是不知道为什么我第一次加的时候就是不work可能某个设置冲突了.只要删了重来.
所以推荐一开始把这些基础设定都设定好再ry.

Ren’Py中自带了几个默认的文本标签,是在主菜单(main menu)界面之前执行的。
详情可参考如下链接:doc.
此处可用的特殊文本标签有3个:splashscreen、before_main_menu和main_menu。
这3个文本标签在新建工程生成的脚本中并不存在(screen.rpy中的main_menu是screen名,不是label名,请勿混淆)。
我们可以在工程自动生成的任意一个rpy文件中添加这些文本标签,或者可以自己新建一个rpy文件并添加这些文本标签。

按照上面的办法显示的logo,鼠标一点就直接跳过了,有没有办法让logo不能跳过?
办法的确是有的……使用 renpy.pause 函数。
在每一个不想被跳过的scene、show和hide语句后面,都加上一句:

1
$ renpy.pause(1, hard=True)

开头的刀乐符号($)表示后面是一个python函数,入参的数字1表示持续(event)的时间为1秒,hard为True表示鼠标点击不能打断暂停。
官方文档对hard赋值为True用法的建议是——Use this sparingly, as it’s hard to distinguish a hard pause from a crashing game(谨慎使用,因为很难区分硬性暂停和程序卡死)。

知乎章鱼大佬:自定义开场画面.



封面(menu)

menu分多种,quick menu是进入游戏后,对话框下面的save load and skip.
menu指的是刚进入页面的封面.

screen main_menu是在load这个main menu游戏时的function.
main_menu和game_menu还是两个东西.

The difference is simple: Game Menu is shown when the game is in progress, i.e. right-click or Escape would take you back to playing.
Main Menu is shown when the gameplay has not started, or the player has exited to Main Menu. So screen main_menu and screen game_menu both include (“ use “) screen navigation .

知乎章鱼大佬:GUI定制化之主菜单.



Image buttom

Image scale.Imagebutton.
图案按钮,图案的缩小方式立绘,背景可以用zoom(控制摄像机和镜头的距离),链接里讲的im.Scale function也许是直接改scale.

1
image quick button option size = im.Scale("images/UI/quick_option_%s.png", 50,50)

楼主还说快速的话可以直接zoom buttom:

1
2
3
4
5
6
7
8
9
10
init:
transform customzoom:
zoom 0.8

screen blah:
imagebutton:
idle 'blah'
hover 'blah
action blah
at customzoom # this line


图像(image)

第一个问题:如何定义图像(image)?

答:定义图像(image)的方法有两种:1. 把图片文件(png和jpg格式)放入images目录。2. 在脚本中使用图像定义语句。

第一种方法的关键在于:图片文件的命名。去掉文件扩展名之后,图片名字符串强制转换为小写,字符串第一个空格前的子字符串就是图像的名称(也就是说不支持中文),如果没有文件名没有空格分隔则整个字符串就是图像名称。
例如,我们请来了尼禄(?),图片名称为“nero normal.png”,直接放入image目录中(再次建议最好单独建一个character子目录专门放角色图片,甚至每个角色建立单独目录),在脚本中不需要任何定义就可以直接使用show语句显示:

1
2
# show语句的用法在后面
show nero normal

Ren’Py会自动搜索所有在images目录及其子目录下的图片资源,并根据图片文件名定义好图像(image)便于直接在脚本中使用。
如果存在同名图片,则按照拉丁字母排序遍历整个目录结构后得到的顺序,使用最先匹配到的那张图。
如果不能理解上一句话,就改为记住:“避免图片重名!”图片同名,但扩展名不同的也属于图片重名。
图片名如果使用空格分隔成几个字段的话,第一个字段就表示图像标签(tag),后面的字段都表示图像属性(attribute)。图像标签和图像属性在后面的几个语句中会解释,主要作用是用来提(tou)效(lan)。

image语句还可以把一些图片合并起来使用,比如:

1
2
3
4
image nero double = HBox(
"images/charater/nero normal.png",
"images/charater/nero normal.png",
)

附带说明,纯色(solid)也可以使用image语句直接定义。特别是常用的全黑和全白,在转场之类的时候有用:

1
2
image black = "#000"
image white = "#ffffff"

知乎章鱼大佬:图像、图层以及4个常用语句.



rollback

Renpy自带返回上一句台词,叫rollback.
在gui.py里加一句:

1
define config.rollback_enable = False

就可以取消rollback.



increase space between lines of text

在script.py里增加:

1
2
3
4
5
init -2:
style say_thought:
line_spacing 20
style say_dialogue:
line_spacing 20


打字机效果(每个字符出现)

需要调cp(characters per second)值.
如果是全局的话可以在option.py的preferences.text_cps里调.



(cps以外的)

速度與等待

cps:
本標籤可以控制文字每秒顯示的字數。
可以是一個絕對的數字,也可以是基於當前顯示速度的變化值。比方說「{cps=*2}文字文字文字{/cps}」這樣。
fast:
這個標籤前的文字會瞬間顯示出來,無視於 cps 或其他設定。
nw:
表示 no wait(不等待)。本行文字顯示完後,「不用按滑鼠」就能自動前進到下一行的腳本指令。
p:
等待指定的秒數或經過滑鼠點擊,然後才換行繼續。
w:
同 p 不過不換行。



整体设置的聲音調控

您可以指定讓遊戲在某些特殊時刻,播放特殊的音樂或音效。

比較常用的有下面幾個:

1
2
3
4
# config.enter_sound = "click.wav"          # 進入 game menu 時的音效
# config.exit_sound = "click.wav" # 離開 game menu 時的音效
# style.button.activate_sound = "click.wav" # 在遊戲中按按鈕時,會發出的音效
# config.main_menu_music = "main_menu_theme.ogg" # 這行定義在主選單 (main menu) 中,會播放什麼音

注,繁体字的一些是从12年的【雪凡与朋友们系列】ctrl c+v过来的.没有校对现今的版本冲突问题.



加密與略去

Ren’Py 遊戲引擎支援替遊戲資源「加密」的功能。

簡單地說吧。這個功能可確保玩家無法手動挖掘您的遊戲資料夾,偷看您的遊戲圖檔與故事腳本。人性本惡是個好假設,否則網路上也就不會有那麼多CG包在四處流傳了。
我們可在建立分發版時,透過 build.classify() 函式來加密(或略去)某些檔案。

它是這樣用的:

build.classify(檔名匹配模式, 分類名)
build.classify() 會將所有滿足「檔名匹配模式」的檔案,歸入指定的「分類」中。依檔案所屬分類不同,遊戲在打包這些檔案時也會採用不同的處理方式。

舉例如下:

1
2
3
build.classify('game/char/**', 'archive')    # 將 game/char 資料夾下的一切檔案放入 archive 分類中
build.classify('game/*.rpy', 'archive') # 將 game 資料夾下,附檔名為 rpy 的檔案放入 archive 分類中
build.classofy('README_win.html', 'windows') # 將 base 資料夾下的 README_win.html 檔案放入 windows 分類中

我记得这个在config文件?中有默认的一些代码,可以跟着代码续写需要隐藏的文件.



自動圖片定義

这个功能倒是真的没听说过,得研究一下.

透過設定以下兩個變數:

1
2
config.automatic_images = ['_', '/'] # 定義分隔符號
config.automatic_images_strip = ['char', 'bg'] # 刪掉不需要的前綴

Ren’Py 會掃描 game 資料夾下所有檔案,並將 png 與 jpg 圖檔找出來,然後把副檔名去掉。接著用 config.automatic_image 所設定的分隔符號,將(包含路徑的)檔名切開。

比方說以下幾個檔案:

1
2
3
4
item/ufo_black.png
char/info_happy.png
bg/library.png
bg/library_night.jpg

依照前面 config.automatic_images 設定的值,圖片名稱將會被切割成:

1
2
3
4
item ufo black    # / 與 _ 成為分隔符號
char info happy # / 與 _ 成為分隔符號
bg library # 只有 / 成為分隔符號
bg library night # / 與 _ 成為分隔符號

然後再用 config.automatic_images_strip 中設定的內容,將不必要的前綴裁掉。

依照先前的設定為例,圖片名字將變成為:

1
2
3
4
item ufo black  # 沒有任何東西被裁掉
info happy # char 被裁掉
library # bg 被裁掉
library night # bg 被裁掉

而其中,「只有圖片名字在兩截以上」的圖片,才會列入自動圖片定義的範圍。

所以最後您會得到三個被定義好的圖片,分別為:

1
2
3
item ufo black
info happy
library night

以上三張圖片的自動定義,等效於手打以下三行:

1
2
3
image item ufo black = "item/ufo_black.png"
image info happy = "char/info_happy.png"
image library night = "bg/library_night.jpg"


滑鼠游標自定義

黑白箭頭的游標實在太過無趣?想換滑鼠游標,怎麼辦?

1
2
3
4
5
6
cursor_default_frame1 = ("檔名1", hotspot_x, hotspot_y) # 設定一個單獨的「禎」
cursor_default_frame2 = ("檔名2", hotspot_x, hotspot_y) # 設定另一禎,多禎交替播放能造成動畫效果。

cursor_default = [cursor_default_frame1, cursor_default_frame2] # 將多個禎放在列表中,會依序播放。

config.mouse = {'default': cursor_default} # 將設定好的游標(含動畫)定名為 'default'

每個單獨的游標--如上面範例中的 cursor_default--都是由一個 list(列表)構成的。而列表中的每一個元素,都代表了一「禎 (frame)」,游標會以每秒 20 禎的播放速度(也就是 20 Hz)在禎與禎之間進行切換,由此組成游標動畫。

每一個單獨的「禎」,都是一個有三項元素的元組 (tuple)。

元組中第一個元素代表著該禎所用的圖檔檔名,而後兩個元素則代表著這張圖片的 hotspot 的座標。

config.mouse 是一個存放著遊戲中所有可用滑鼠游標的字典 (dict),其「鍵」就是游標的名稱。預設名稱包括:”default”, “say”, “with”, “menu”, “prompt”, “imagemap”, “pause”, “mainmenu” 與 “gamemenu”……這些名字應該很容易顧名思義,故不特別介紹。

如果您實在搞不懂,如同上面的示範,只設定 “default” 一項就準沒錯了。



區段觀念

初始區段 (init block):

1
2
3
init:
# 這個內部就是初始區段
image library = "bg/library.png"

此處複習一下:您在初始區段中進行的設定,會在遊戲被載入的瞬間--也就是在連主選單都還沒出現之前--就被立刻執行完畢。不管這個區塊是被放在遊戲的哪個檔案中,又或是被放在檔案的最尾端,那都不例外。

1
2
3
4
init -1:
# 後執行的內容
init -2:
# 先執行的內容

您可以在 init 後面加上數字,數字愈小的區段就會愈先被執行完畢;至於數字相同的初始區段,其執行順序是不分先後的,只能放入平行性質的工作。

初始區段的執行順序如果沒有被顯式指定的話,那預設就是 0。

init python: 這種區段,就僅僅只是把 Python 區段與 init 區段合併起來而已。



特殊字體調整

以下標籤同樣與字體呈現方式有關,也需要透過前後兩個標籤標明影響範圍才能使用;但除此之外,這些還有些別的參數需要指定。

color:輸入 #f00 之類的色碼,控制文字顏色
size:指定字體大小。可以用 =+10 來增大減小,也可以設為絕對值
font:字型,輸入為檔名。
k:字元間距。可以加大每個字之間的距離。

下兩個標籤可以增加空白量,而且使用時都不需要加上尾部標籤。

space:在當前行中,加入指定數量的空白量,單位是像素。
vspace:會立刻分行,並在下一行的垂直方向中,加入指定像素的額外行高。
使用範例見此:

1
2
"用一般分行方式\n分出來的行高{vspace=0}與用 vspace = 0 分出來的行,高度一樣;{vspace=20}而透過 vspace = ? 可以增加額外的行高。{vspace=-10}vspace 設為負數並沒有作用。"
"用在增加一段{space=50}額外空白上"


ruby text

嘩哈!終於出現了!上次預告過的,充滿著愛與夢想,還害咱被貼上醫療廢棄物標誌塞進垃圾桶的東西。

先貼張圖來:

要在文字中產生 ruby text 效果,您必須同時使用兩個標籤:rb 與 rt。頂部的小字是由 rt (ruby on top) 定義,rb 則用來註記底部的字。比方說,剛剛那個範例事實上是這樣寫的:

1
"在{rb}DMZ{/rb}{rt}非武裝區{/rt}的便利商店中,\n{rb}ATM{/rb}{rt}自動櫃員機{/rt}與{rb}ATM{/rb}{rt}反戰車飛彈{/rt}的差別可是很重要的。"

rb、rt 標籤的使用規則如下:

rb 標籤須寫在前面,rt 標籤則緊跟在後。
有時您也可以不寫 rb 標籤,僅僅只去寫 rt 標籤。不過這樣一來,rt 文字就只會出現在「前一個字符」的上面,而不是「前面整個詞」的上面。

您現在可能很想馬上試試看 ruby text 效果,不過……
直接使用 ruby text,你會收到一個 “Ruby glyph out of bounds” 的錯誤。

以上錯誤的意思是說,您要求顯示一個 ruby 文字,但您沒有給他足夠的顯示空間……是的,Ren’Py 並不會自動替 ruby text 勻空間出來。想要使用 ruby text,您得自己動手,做一些事前配置。

自己動手豐衣足食,請看下面的設定方式:

1
2
3
4
5
# 先設定您所打算使用的 ruby 文字的特徵,這包括……
init -1 python:
style.ruby_style = Style(style.default) # 這行表示用預設的 style 為模版,建立一個全新的 style,名字為 style.ruby_style
style.ruby_style.size = 12 # 設定 ruby 的字體大小是多少
style.ruby_style.yoffset = -20 # 「文字的高度」要「往下」偏移多少。因為期望中 ruby text 要往上偏,所以應該設成負值的。

以上區塊可以貼在任何地方,不過作為設定的一部份,建議貼在 options.rpy 中比較適當。

前面的手續完成,新的 ruby style 就算是建立好了,然而建立好歸建立好,這個全新的 style 目前卻沒有被套用到任何實際顯示項目中。您必須再去設定對話者,讓對話者去使用這個 ruby style:

1
2
3
4
5
# 隨便找一個 init block,來設定敘事者(您也可以設定在 script.rpy 檔案最上方)。
init:
define ruby = Character(what_ruby_style = style.ruby_style, what_line_leading = 10) # 一個沒有名稱的敘事者(旁白)
define info = Character("音符", color = "#fff3c4") # 一個沒有 ruby 能力的敘事者,寫在這裡只是和下方做對比
define info_ruby = Character("音符", color = "#fff3c4", what_ruby_style = style.ruby_style, what_line_leading = 10) # 一個帶有名字的敘事者

和平常的敘事者設定相比,追加了兩個參數,包括:

what_ruby_style
將您剛剛設定好的 ruby style 指定給它,讓它使用您指定的字體大小、高度渲染。
what_line_leading
這個參數將定義「文字行上方」會被留下多少空間。數字愈大,每行文字的行高也會愈高。

對於擁有 ruby 文字支援能力的敘事者來說,就算您沒有真的在文字中插入 ruby 字,line_leading 也會空在那裡,不會消失。而又因為平常沒在使用 ruby text 時,上面空著一大截空間很難看,故建議各位平常寫腳本時別用 ruby 敘事者說話,只要在 ruby 字出現時小用一下就好,這樣應該是比較妥當的用法。

以上全數完成後,您就可以愉快地使用 ruby text 了,不過即使如此,如果不小心照顧她,您依然難免會在使用過程中碰上 “Ruby glyph out of bounds” 的問題,這是因為您雖然運用 line_leading 騰出了縱向行高,但是對於 ruby text 來說,其實橫向的寬度也是有限制的--您不能讓 rt 字超出底部字 (rb) 允許的橫向空間太多。

換句話說,小字的字數會受到大字的寬度所限制。要是您打太多字、字太長,那就會愉快地爆格……

……嗯?您居然不覺得愉快?真是的,所以說這就是您嫩的地方了啊。具體地說,對於蹂躪不能坦率地感到歡愉,是無法從事這種好比幫地獄挖地窖的,充滿著燦爛希望的好工作啊……您看,地窖的天花板正在您頭頂上閃閃發亮呢。這是多麼美麗的現實啊!

作者您十年前的精神状态还好吗.

之前沒有特別提及,除了小字字數太多以外,還有另外一種狀況也會導致 “Ruby glyph out of bounds”,就是敘事者的 line_leading 與 ruby style 中的 size 尺寸不匹配--簡單地說,ruby 文字的字體太大而文字上方留空空間不夠大,也會讓 Ren’Py 抗議沒有空間可以顯示。還請注意。

請注意ruby官網範例中,將新設定的 ruby_style 指定給 style.default--這種作法會讓遊戲中的所有文字(包括按鈕上的文字,所有的!),全都能使用 ruby style 效果,但副作用就是讓所有文字的上方都多出不自然的空白空間……故並不推荐直接套用。



標準式命名空間建立法

請看下方的例子,您可以像這樣建立並使用一個命名空間:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 在開始區段建立一個名叫 Lancelot 的命名空間
init python in Lancelot:
pass # 可以在此做一些初始化。但也可以不做任何初始化,簡單跳過。


# 遊戲開始
label start:

# 您可以直接使用這個命名空間內的變數,為各種變數設值
$ Lancelot.pilot = "Suzaku"
$ Lancelot.HP = 100
$ Lancelot.SP = 50
$ Lancelot.weapon = ["VARIS", "MVS"]

# 像用普通變數那樣,直接去使用命名空間中的變數
"我的名字是[Lancelot.pilot]!"

「標準式命名空間」的優勢,是可以在某些特定的狀況下,手動「進入」命名空間!

如下範例:

1
2
3
4
5
6
7
8
label start:

python in Lancelot: # 進入名叫 Lancelot 的命名空間……

pilot = "Suzaku" # 在命名空間中,不用多事替變數指定命名空間的名稱。
HP = HP + SP # 省了好多打字數!看起來也更清晰了。

"我的名字是[Lancelot.pilot]!"

在特定命名空間中,您不用囉唆地輸入全名--這也是理所當然的!就好像您在班上大喊「座號 39 號是誰」時,別人絕不會認為您在問隔壁班或隔壁學校 39 號一樣。這在某些計算密集的場合中有其方便之處。

如果對 in 的用法還是不明白,官方在 changelog 中有一些說明,還請參考看看 。

用了命名空間之後,遊戲中就不會一堆沒頭沒腦不知道是誰的 HP 滿地滾了。當您要製作一些規模更大、資料量也更多的遊戲時,這會讓遊戲變得更強健更好管理。還能最大限度避免打錯字。命名空間大致都能自動幫您找出問題。

物件式命名空間建立法
除了前述的「標準式命名空間」外,您還可以用另一種手法建立許多獨立的命名空間:

1
2
$ Lancelot = store.object()    # 用 store.object() 建立物件式命名空間。
# 如果沒有這行就直接設值給 Lancelot.xxx,則會出現錯誤訊息。

此種命名空間的建立法,咱稱之為「物件式」,因為它是用 python 物件模擬出一個命名空間。在 Ren’Py 6.13 版之前(含),這也是唯一一種命名空間的啟用方式。早期版本使用者請注意這點。

同標準式命名空間那般,您可以透過「命名空間名稱」與點號 (.) 來精確指出變數:

1
2
3
4
5
6
7
8
9
10
11
label start:
# 以下這些變數都能被正常使用,也能正確地被儲存系統收錄

# 用法沒啥不一樣,只是非得要輸入全名而已
python:
Lancelot.pilot = "Suzaku"
Lancelot.HP = 100
Lancelot.SP = 50
Lancelot.weapon = ["VARIS", "MVS"]

"我的名字是[Lancelot.pilot]!"

但請特別注意!物件式命名空間不支援 python in XXX 語法!

請看以下範例:

1
2
3
4
5
6
7
8
9
label start:
"開始實驗囉,請保持好心情~"

$ somebody = object() # 指定 somebody 為一個物件型命名空間
$ somebody.emotion = 'x_x'
python in somebody: # 嘗試運用標準型命名空間專用的方式來修改它……
emotion = '^_^' # Ren'Py 不會提示您做錯任何事情

"您的心情是「[somebody.emotion]」" # 警告--此處的結果絕非您想要的!!

結果為:

您的心情是「x_x」
過程中您不會得到藍白錯誤畫面,只會在最後得到一個不仔細看很難發現的錯誤結果,因此絕對不要混用兩種方法!絕對!

【store.object 的命名?】
如果您對「物件式命名空間」進行深入測試的話,可能會發現除了 store.object() 以外,object() 與 renpy.store.object() 也都代表了相同的意思。

1
2
3
4
label start:
$ t1 = object()
$ t2 = store.object()
$ t3 = renpy.store.object()

以上三個命名空間 t1、t2、t3 都可以正確工作無誤,而事實上他們的意思也的確都是相等的:變數 t1、t2、t3 都會成為一個 renpy.python.RevertableObject 類型的物件。

也就是說 renpy.store.object() 是全名,而其他的名稱則都是別名。

更有甚者您還能發現,被變數也可以依此種命名方式設定或取回使用。

1
2
3
4
5
6
7
8
9
10
label start:

$ count = 5

baka "這是……蘿妲之果?居然一次拿到[store.count]顆!" # 5 顆。

$ fruit = object()
$ fruit.name = "蘋果"

friend "只是普通的[renpy.store.fruit.name]而已吧!" # 蘋果

有經驗的電腦玩家應該已經看出來了--Ren’Py 的主程式其實本身就是跑在名叫 renpy.store 的命名空間中,而所有我們自訂的遊戲變數,預設也都是被存放在這個空間裡。



透過自定義類別 (class) 來控制物件式命名空間
從上面的介紹看起來,自從 6.14 版以後,標準式命名空間顯然更適合用來收藏變數。

不過之所以刻意介紹物件式命名空間,是因為這種命名空間的本質是物件--換言之,您可以用自訂類別 (class) 的方式來引導命名空間做一些特別的事情。像是進行一些限制或自動化什麼的。

以下是個限制變數名稱的範例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
init python:
# 自行定義一個叫作 Someone 的類別,用來存放角色的內部資料。

# 注意 class 一定要繼承自「store.object」,否則內部資料不會被儲存。
class Someone(store.object):

# 這個類別的特徵是「您只能設定某些變數名稱」,此處指示本類別只能設定 name、race、description 三種東西。
# 如此一來可以有效避免打錯字。
# 比方說失手把 name 打成 mame 時,一旦腳本執行到此處就會立刻報錯,而不會就默不作聲地執行過去。

acceptable_attribute = {'name', 'description', 'race'}

def __setattr__(self, name, value):
if name not in type(self).acceptable_attribute:
raise Exception('Not allow this variable')
super(store.object, self).__setattr__(name, value)
1
2
3
4
5
6
7
8
9
10
11
# 實際使用看看
label start:

$ baka = Someone()

# 試著設定各種數值
$ baka.name = '⑨'
$ baka.race = '妖精'
$ baka.description = '⑨應該不需要解釋'

$ baka.excuse = '那個⑨是怎樣!我才不……' # 這行會提示錯誤,因為 "excuse" 並不包括在可設定的變數之中……Resistance is futile.

因為這種可以被用來自訂的特質,所以物件式命名空間在某種意義上其實更具彈性。當然,如果您用不到自訂 class 的功能,用標準型命名空間也是完全足夠的。

【自訂 class 的注意事項】
如果您用一些自訂類別 (class) 的物件來存放變數,則這個變數「一定要繼承自 store.object」,否則內部變數的變化,並不會被 Ren’Py 的 state 系統處理,當然也就無法收錄進存檔中,因此完全不能用。

繼承 store.object 是最重要的事情。絕對不可以忘!



Map Navigation

Map Navigation in RenPy | Vlog #4 Creating a Map Screen.



行内字体描边

转载自Ren’Py 无上限!

在同一行内设置不同的字体描边。
因为文本样式标签不继承 outlines 属性,所以需要特殊的方法实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
style border_ is text:
color "#fff"
outlines [(5, "#000", 0, 0)]
ypos -0.2

style borderbox_:
xmaximum 24 # 此处适用于字号为22的情况

init python:
def border_tag(tag, argument, contents):
return sum([
[(
renpy.TEXT_DISPLAYABLE,
Fixed(Text(c, style="border_"), style="borderbox_")
) for c in text]
if kind == renpy.TEXT_TEXT
else [(kind, text)]
for kind, text in contents
], [])

config.custom_text_tags["border"] = border_tag

label start:
"Yiri" "有时候需要一点{border}{cps=20}恐怖的文字{/cps}{/border}。"

原理是把需要改变描边放到 Text 中,这样就能设置单独的 style。为了实现慢速显示,这里为每一个字都包装了单独的 Text,并且外包 Fixed 来控制字间距。



Screens

Ren’py Screens Tutorial

Screen是「一群 UI 元件的在螢幕上的排列組合」。

依據官方手冊的說明,Screen 可能因為以下四種原因被呼叫出來:

隱式呼叫:比方說讓 Ren’Py 顯示對話時,會自然叫出一個名叫 “say” 的 Screen,專門負責顯示對話框與對話內容。
自動呼叫:比方說啟動遊戲時,會自動秀出名叫 “main_menu” 的 Screen,這就是遊戲的封面主選單。
被現存的 UI 元件呼叫:「呼叫 Screen」這件事,有可能被當作一個行動 (action) 繫結到某些使用者操作上:如按螢幕上某個按鈕、按鍵盤的某個熱鍵等。此時可能呼叫出 Screen 來。
舉例而言,在遊戲中按右鍵,預設會叫出遊戲選單 (game_menu) 。
顯式呼叫:使用 “show screen” 指令,就會像顯示圖片那樣將 screen 直接顯示在螢幕上。

此處稍加提醒:Screen 總是會出現在其他圖片上。這是因為 Screen 使用的圖層和 show 圖用的圖層並不相同的緣故,設計 Screen 時還請注意這個特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# ================= 定義圖片與轉場特效 ===================

define blight = Fade(0.25, 0.5, 1.5, color = "#FFF") # 爆炸轉場特效,與 screen 無關
image bg = "bg.jpg" # 背景素材,與 screen 無關

# ================= screens.rpy ======================
# 以下是關於 screen 的定義

screen timer: # screen 定義在此,僅僅就只有四行而已。看起來有點複雜?等會兒再細講!
default time = 30
timer time action [Hide("timer"), Jump("gameover")]
add DynamicDisplayable(countdown, sec = time) align (0.5, 0.3) # DynamicDisplayable 在此


init python: # countdown 是 DynamicDisplayable 所使用的函式,第六章有提到這是怎麼定義的,以及幹什麼用。不重複。
def countdown(show_time, same_tag_show_time, sec):
remain_sec = sec - show_time
remain_text = Text('剩下 {:.1f} 秒'.format(remain_sec), size = 40)
return (remain_text, 0.1)

# ================= script.rpy =======================
# 以下是使用示範

label start:
scene bg
"我迅速打開箱子的最底層,扳過計時器"

show screen timer(time = 10) # 顯示出計時器 screen: "timer"
"炸彈爆炸只剩十秒!該死……要剪紅線還是黃線?"

menu:
"「看清楚就下手!但是時間……」"

"紅線!":
hide screen timer # 收掉計時器 screen: "timer"
jump gameover
"黃線!":
hide screen timer
jump gameover
"藍……疑!?":
"你猶豫了短短一息,將鐵剪放到藍線之間,深呼吸……"

hide screen timer
with dissolve
"喀擦"
"……冷汗流滿了全身,計時器終於停住了。"
return

label gameover:
with blight
"轟隆的巨響迴盪在城市中間。少女感覺到了什麼,偶然將視線望向遠方。"
"(全篇完)"
return

看過上面的範例,相信各位已經注意到了,召喚 Screen 的方法就是:

1
2
show screen 名稱
hide screen 名稱

show screen 不能像 show 那樣,在後方加上 at、with 語句。
但 with 語句可以作為獨立的語句而單獨存在,效果依然能套用上去。正如例子中的展示。

阻塞與非阻塞

用 show screen 呼喚出來的 Screen,是不會「阻塞」遊戲正常進行的。

這個意思是說,screen 只是顯示在螢幕上面,您繼續點滑鼠,故事還是能往下推進,screen 的存在並不會攔截正常遊戲腳本流程。

如果您就是需要一個阻塞效果,讓 screen 攔在玩家面前說:「等等!你得先把我搞定才能繼續去做別的,否則此路不通。」則您必須用 call screen 語句來取代 show screen 語句。如下:

1
call screen 名稱

您大可修改前述的爆炸範例來試試效果。

注意,您只能透過在 screen 中加入兩種 action:”Jump” 和 “Return” 來讓玩家能脫離阻塞狀態。比方說:

1
2
3
4
5
screen some_screen:
textbuttom "脫離阻塞" action Return() at truecenter

label start:
call screen some_screen
"您必須脫離阻塞才能看到這一行。換句話說,您剛剛按過那個按鈕,執行過 action Return 了。"

當這兩種 action 的其中之一被運行時,故事才能繼續進行下去。



咱們來看看爆炸範例中的 screen 到底是啥意思:

1
2
3
4
screen timer:
default time = 30
timer time action [Hide("timer"), Jump("gameover")]
add DynamicDisplayable(countdown, sec = time) align (0.5, 0.3)

上述爆炸範例中的 Screen,就只是一個計時器,功能只有兩項,非常單純。

首先,它會在被呼喚(show screen 或 call screen)時開始倒數計時,並在倒數歸零時,將敘事線跳轉到 label “gameover” 的地方。其次,它還會在螢幕中央顯示一個大大的倒數計時數字,來提醒玩家還剩多少時間可用。



Screen 的參數與設值

第二行的 default time = 30 表示:time 這個參數預設值為 30。如果在 show screen 時忘了指定要倒數多久,則倒數的時限就會自動被設定為三十秒。建議您觀察一下上面四行中有哪些地方用到了 time 參數,來體會參數的用法。

咱們對照一下顯示 screen 時的指令是什麼:

1
show screen timer(time = 10)

括號中,變數 time 會被指定為 10……由於我並沒有偷懶或忘記給值,因此 time 在本例中事實上是被設為 10 的。

但如果我這樣寫……

1
show screen timer

這樣腳本也不會出錯,不過 time 沒有給定,就會從 30 秒處開始倒數。

您還可以在此設定別的變數,不過如果 screen 內部沒有用到這個變數,那就只是浪費時間而已:

1
show screen timer(emotion = 'XD') # 不會出錯,但也沒意義,因為 screen 內部沒有用到 "emotion" 這個參數


等待一定时间后触发特殊剧情

某些隐藏剧情的进入条件是在某一处对话处等待一定时间,可以通过这样的方式实现。

1
2
3
4
5
6
7
8
9
10
define y = Character("Yiri")
label start:
$ before_time = renpy.get_game_runtime()
y "你需要在这里等待5秒钟。{w=5}{nw}"
$ diff_time = renpy.get_game_runtime() - before_time
if diff_time < 5: # 如果等待时间不足,进入正常剧情
y "时间还不到哦。"
y "你只等了[diff_time]秒,你有点缺少耐心了。"
else: # 如果等待时间充足,进入隐藏剧情
y "不错,你等了5秒钟。"

原理是设置一个超时自动跳过的对话,并记录对话前后的时间,对比时间差。
使用renpy.get_game_runtime,不会计算在菜单界面或存档期间的时间。



存档钩子

有的时候我们需要在玩家存档、读档时进行一些额外的操作,这就需要拦截FileAction这一行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
init -5 python:
class ActionHook(Action):
alt = None
def __init__(self, action, func):
self.action = action
self.func = func
self.alt = action.alt

def get_sensitive(self):
return self.action.get_sensitive()

def get_selected(self):
return self.action.get_selected()

def get_tooltip(self):
return self.action.get_tooltip

def periodic(self, st):
return self.action.periodic(st)

def predict(self):
return self.action.predict()

def __call__(self):
return self.func(self.action)

def __eq__(self, other):
return self.action == other.action and self.func == other.func

FileActionOld = FileAction
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    def FileActionNew(name, page=None, **kwargs):
action = FileActionOld(name, page=page, **kwargs)
def do_action(action):
if forbid_saving and renpy.current_screen().screen_name[0] == "save":
renpy.notify("不,你现在不可以存档。")
elif forbid_loading and renpy.current_screen().screen_name[0] == "load":
renpy.notify("不,你现在不可以读档。")
else:
return action() # 执行原本的 action
return ActionHook(action, do_action)
FileAction = FileActionNew

define y = Character("Yiri")
default forbid_saving = False
default forbid_loading = False
label start:
y "接下来是存档测试。"
$ forbid_saving = True
y "现在,存档被禁止了。"
y "这很有趣,不是吗?"
$ forbid_saving = False
y "好了,你现在可以存档了。"
$ forbid_loading = True
y "不过,不能读档。"
$ forbid_saving = True
y "现在,试试一起被禁用的感觉。"
$ forbid_saving = False
$ forbid_loading = False
y "很好,玩笑开够了。你可以继续了。"
return

简单解释一下上面的代码。
首先我们定义了一个ActionHook类,它的作用是包装一个Action并改变它的行为。可以看到它在创建时需要传入一个action和一个func,当获取属性时,返回action的相关属性,但调用(__call__)时改为调用func。
然后我们替换掉了原本的FileAction行为,改为创建一个ActionHook,其中调用由do_action执行。在do_action中, 进行是否执行原本行为的判断。
理论上用这种方法,可以拦截绝大多数的内置 action。

存档计数

Check if Player saved.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
define help = Character('Help')

# By defult we do not know about ambush.
# It is only set at specific point of the game.
default ambush_knowledge = False

init python:

# A function which takes a dictionary and saves value of ambush_knowledge as extra data
def save_extra_info(d):
d['ambush_knowledge'] = ambush_knowledge

# We need to add this function to list of callbacks to actually invoke it as part of save process
config.save_json_callbacks.append(save_extra_info)

# A function to check if we know about ambush in any of the save files
def check_ambush_knowledge():
# Iterate over list of all save file names
for name in renpy.list_saved_games(fast=True):
# Extract extra info
json = renpy.slot_json(name)
# Get save value of ambush_knowledge. We are protected from both
# missing JSON info and missing 'ambush_knowledge' record
# this can happen if we add this logic later in later patch and someone has save from previous version
knows_about_ambush = json.get('ambush_knowledge', False) if json else False
if knows_about_ambush:
return True
return False


# A help center to hint about need to save in certain moment
label knowledge_corner:
help 'You died.'
help 'Well, you had no way to know, what expect you behind that door, right?'
help 'If only you could {color=#00F}remember{/color}...{p}If only there was a way to {color=#00F}save{/color} those memories...'
help 'Maybe then you could find another way out?'
return


label start:
'You see a door. There is something written on the door:'
'{i}Enter here{/i}'
menu:
'Enter':
'The door slammed shut behind you.'
$ ambush_knowledge = True # All games saved after that line will pass ambush_knowledge check
'Suddenly light flooded the room.'
'You saw dozens of soldiers surrounding you.'
'It is a trap!'
'Commander' 'Fire!'
'You died before you even heard first gunshot.'
$ ambush_knowledge = False # If you didn't save before, you missed the moment.
# Show hint if you did not do the right thing
if not check_ambush_knowledge():
call knowledge_corner
centered 'YOU HAVE LOST'

# This option will only be avaliable if you have a game saved in the ambush scenario
"Don't enter" if check_ambush_knowledge():
'Knowing well, what waits for you behind those doors, you decide not to enter.'
'You eveaded the trap!'
centered 'YOU HAVE WON'
return

主要是save_json_callbacks这个build-in.



(数据删除)效果

利用负字偶距将大量文字压缩到同一位置,达到(数据删除)的效果。
同时,设置 noalt 和 alt,保证历史记录中可以正常显示原本的文字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
style compress_text is text:
kerning -50

init python:

def define_text_tag(name):
def wrapper(func):
config.custom_text_tags[name] = func
return func
return wrapper

@define_text_tag("erase")
def erase_tag(tag, argument, contents):
return sum([
[
(renpy.TEXT_TAG, "noalt"),
(renpy.TEXT_TAG, "=compress_text"),
(renpy.TEXT_TEXT, '酯裹溴苯荸'.join(text)),
(renpy.TEXT_TAG, "/=compress_text"),
(renpy.TEXT_TEXT, ' '),
(renpy.TEXT_TAG, "/noalt"),
(renpy.TEXT_TAG, "alt"),
(renpy.TEXT_TEXT, text),
(renpy.TEXT_TAG, "/alt"),
]
if kind == renpy.TEXT_TEXT
else [(kind, text)]
for kind, text in contents
], [])

label start:
"**" "你和我说的,不会是{erase}数据删除{/erase}吧?"


在立绘上用气泡标识当前说话人

注册全局对话钩子,每当新的对话发出时,获取当前说话人立绘的位置,叠加一个气泡图层上去。
此方法需要在创建 Character 时,指定其 image 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
define 北萱草 = Character("忘忧北萱草", image="萱草")
image speaking = "images/speaking.png"

init -1 python:
def show_speaking(event, interact=True, **kwargs):
if not interact:
return

tag = renpy.get_say_image_tag()
if not tag:
renpy.hide("speaking")
return

bounds = renpy.get_image_bounds(tag)
if not bounds:
return

x, y, w, h = bounds
mstyle = Transform(xpos=x / config.screen_width, ypos=y / config.screen_height)
renpy.show("speaking", at_list=[mstyle], zorder=1000)

config.all_character_callbacks.append(show_speaking)

此方法需要有一张和角色立绘大小相同的 speaking.png 作为气泡图像。



Omake房间之类的

Image Gallery, Music Room, and Replay Actions.



Reference

知乎:被诅咒的章鱼,Ren’Py引擎从入门到放弃系列.

很实用,很实用,推荐全看完.

雪凡與好朋友們的 Ren’Py 遊戲引擎初學心得提示系列.

很实用,很实用,恩师啊.
不过这个是12年的写的!!跟现在的renpy版本差很多,需要注意.
我是写过一点再回去看看人思路,帮助很大.

renpy小心得

博主的演示.

【心得】用Renpy的一點心得

可能還可以寫的功能有

  • 利用archive打包檔案
  • 影片播放
  • 遊戲劇情回顧
  • 持久性資料(Persistent data)的使用,可處理cg解鎖等問題。
  • 製作翻譯檔
  • 不同語言換字型
  • 外掛玩家翻譯
  • 滑鼠滾輪向上改成歷史訊息而不是直接倒退回上一句

Ren’Py Cookbook

一个国外的renpy论坛?
救我狗命.

71 mini game Renpy

互联网佛祖普度众生.

一些有用的Renpy教程链接

黑凤梨大佬普渡众生.



Cookbook

Cookbook之中比较喜欢的:
Ren’py Cookbook - Kinetic Text!

itch
大多的神秘文字效果都可以用这个包来演出了.

Ren’py Cookbook: Snow & Particles

itch

Ren’py RPG Combat Engine!

Ren’py Images and Action Editor Tutorial