样式
样式是一种命名的外观:一组用于文本和容器的属性,例如字体、颜色、背景和内边距,并带有一个父级,凡是它自身未设置的属性都从父级继承。Novella 游戏的每个部分都用某个样式绘制:对话窗口及其文本、名字标签、选项、快捷菜单、存档位,以及游戏自有界面上的按钮和框架。故事只需定义一个同名样式,就能为其中任何一个重新设定样式;也可以为自己的界面、角色和台词中的个别词语创建专属外观。
样式的工作方式与 Ren'Py 中相同,因此 Ren'Py 作者会觉得很熟悉;两者的差异列在本页末尾。主题设置仍然是设置游戏外观最快捷的方式,因为每个主题设置都是某个内置样式中某个属性的简写;样式则用于主题键无法表达的一切。
style gold_button is button:
background "gui/gold_idle.png"
hover_background "gui/gold_hover.png"
borders (24, 24, 24, 24)
padding (32, 12)
style gold_button_text is button_text:
font "Ma Shan Zheng"
size 40
color "#e8dcc6"
hover_color "#ffd27a"
insensitive_color "#7d7568"
style say_dialogue:
outline_width 2
outline_color "#1a0f0a"
style ghost_window is say_window:
background "#10202cd0"
style shout:
color "#ff6050"
bold true
character ghost "Ghost" window_style "ghost_window" what_size 40
定义样式
style name [is parent]: 位于脚本的顶层,其下的块中每行一个属性。多个属性可以写在同一行(padding (32, 12) text_align center)。
- 父级是当本样式未设置某个属性时,该属性的来源样式。
style gold_button is button:从button已有的一切出发,只修改它列出的内容。没有父级的样式继承自default,即所有样式的根,它保存界面的字体、字号和颜色。 - 第二次定义是对第一次的补充。之后在同一文件或另一个文件中再次写
style title:,会为同一个样式设置更多属性;两次都设置了同一属性时,以后者为准。带is的后续定义会更改父级;不带的则保留原父级。 - 与内置样式同名的样式会重新设定游戏中对应部分的样式。
style say_dialogue:并不是一个新样式,而是叠加在 Novella 自身say_dialogue之上的更多属性,而每一行对话都是用它绘制的(参见内置样式)。
值的写法与脚本中一致:颜色写作 "#rrggbb" 或 "#rrggbbaa"(也可写作 "#rgb"),还有数字、true 和 false、用于字体和文件的带引号文本、center、bold 和 none 等裸词,以及写在括号中的边距。尺寸以 1080 像素高的游戏为基准,以像素为单位,并像主题中的尺寸一样随游戏缩放。边距写作 (left, top, right, bottom);两个数字分别表示左右和上下,一个数字表示四边相同。
样式是定义而不是游戏状态:它们不会被存档,因此从旧存档加载的游戏会使用脚本当前定义的样式绘制。开启开发者模式时,按 Shift+R 会重新加载脚本,并立即应用已更改的样式。
状态
元素处于一个或多个状态,可以通过给属性名加前缀来为某个状态指定该属性:
| 前缀 | 元素处于 |
|---|---|
idle_ |
既不在指针下也不是不可用状态:静止。 |
hover_ |
在指针下,或拥有键盘或手柄焦点。 |
selected_ |
被选中:菜单的当前标签页、处于开启状态的选项、已获得的成就。 |
insensitive_ |
不可按下,例如 sensitive 条件为 false 的按钮。 |
visited_ |
之前选过:在先前的游玩中选择过的菜单选项。 |
selected_idle_、selected_hover_、selected_insensitive_ |
被选中,同时处于静止、在指针下或不可用状态:selected_hover_color 是指针下的当前标签页的颜色,且仅用于这种情况。 |
hover_color "#ffd27a" 为指针下的按钮文本着色,hover_background "gui/gold_hover.png" 则替换其图片。任何属性都可以加任何前缀。hover_sound 并不是 hover 状态下的 sound,而是一个独立的属性:指针移到按钮上时播放的声音。
如何查找属性
查找属性时,先在样式本身中查找,然后依次在其父级中查找。在每个样式中:
- 与元素状态相符的带状态前缀的属性优先,最具体的在前,排序方式与 Ren'Py 相同:被选中元素的复合前缀(
selected_hover_),然后是selected_,然后是hover_或insensitive_,然后是visited_,最后是idle_; - 否则使用不带前缀的属性;
- 只有当样式两者都没有时,才会询问其父级。
因此,设置了某个不带前缀属性的样式会覆盖该属性的所有状态,经由它无法到达父级的状态变体:
style base_text is text:
color "#111111"
hover_color "#222222"
selected_color "#333333"
style plain_text is base_text:
color "#444444"
在指针下时,base_text 为 #222222;被选中且在指针下时为 #333333,因为 selected_ 先于 hover_ 尝试;selected_hover_color 会在这一种情况下胜过两者。plain_text 在所有状态下都是 #444444:它自己设置了 color,因此查找在到达其父级的 hover_color 之前就停止了。它没有在任何地方设置 text_align,因此该属性来自更上层的 text。这就是 Ren'Py 的规则,也正是设置颜色的样式所预期的行为。
属性
| 分组 | 属性 | 值 |
|---|---|---|
| 文本 | font |
字体族,在一个字符串中以逗号分隔;由第一个包含该字符的字体绘制它,与主题的字体列表相同。 |
size |
文本的字号。 | |
color |
文本的颜色。 | |
bold |
true 或 false。 |
|
weight |
normal、medium、semibold 或 bold。 |
|
line_spacing |
行高,以文本字号的倍数表示。 | |
text_align |
left、center 或 right。 |
|
outline_width、outline_color |
文本周围的描边,绘制在字母后面。 | |
shadow、shadow_color、shadow_offset |
柔和阴影、阴影颜色及其偏移距离(未设置时随文本字号而定)。 | |
ruby_scale |
注音文本(ruby)相对于其所注释文本的大小。 | |
| 容器 | background |
容器的绘制底面:一种颜色或 solid("#203040c0");一个图像名称、一个文件或一个图像函数,例如 tint("gui/panel.png", "#a0ffa0"),按 borders 以九宫格方式拉伸;一个 frame("gui/panel.png", 24, 24),按其自身的边框拉伸;或 none。 |
borders |
背景图像的九宫格边角,(left, top, right, bottom),以图像自身的像素计:边角保持不变,边和中间部分拉伸(参见九宫格图像)。 |
|
border_color、border_width |
以颜色绘制的容器的轮廓线及其宽度。 | |
corner_radius |
以颜色绘制的容器的圆角。 | |
accent_color |
小标记:选中标签页下方的横条、滑块的填充、文本框的光标、点击继续箭头。 | |
image |
元素绘制的图片,例如点击继续标记(ctc)。 |
|
| 布局 | padding |
容器的边缘与其所含内容之间的空间。 |
margin |
容器周围的空间,即容器与屏幕边缘之间的空间。 | |
xsize、ysize |
容器的宽度和高度。 | |
spacing |
容器中依次排列的内容之间的空间。 | |
xoffset、yoffset |
相对于容器原本位置向右和向下的偏移。 | |
| 声音 | hover_sound |
指针移到按钮上时播放。 |
activate_sound |
按下按钮时播放。 |
背景或图像可以是任何以函数形式书写、不带引号的图像源,就像 Ren'Py 书写 displayable 那样:
style tab_button is button:
background frame("gui/tab.png", 24, 24)
selected_hover_background tint("gui/tab.png", "#ffe0a0")
style tooltip:
background Solid("#101820e0")
corner_radius 6
frame(picture, left, top[, right, bottom]) 即 Ren'Py 的 Frame:图片按其自身的边框拉伸,给出两个数字时分别表示横向和纵向的边框。其他任何函数(tint、zoom、crop、composite 等)都作为图片绘制,并按样式的 borders 拉伸。solid(colour) 是一种颜色,因此 corner_radius 和 border_color 仍然适用于它。Frame 和 Solid 可以按 Ren'Py 的方式拼写。背景中的图片按其自身大小绘制,不会先适配舞台尺寸,因此边框始终以图片自身的像素计。
并非每个属性对每个元素都有意义:text 没有背景,vbox 没有字体。元素不使用的属性只是不会被读取;而根本不是属性的名称会由验证器报告。
描边绘制在字母后面,其全部宽度都在字母之外,形成光晕:带深色描边的细浅色文字仍保持自身的颜色和形状。主题的 text_outline_width 和 {outlinecolor} 文本标签也是如此,后者绘制至少两个像素的描边。
内置样式
Novella 自己绘制的一切都使用其自身的某个样式绘制,而这些样式是由主题设置构建的。因此,define theme.text_color = "#fff4d0" 与
style say_dialogue:
color "#fff4d0"
表达的是同一件事。主题键是某个内置样式中某个属性的简写;故事中同名的样式会叠加在主题构建的样式之上,因此它是细化而非替换。带有新 background 的 style say_window: 会保留主题中窗口的高度、外边距和内边距,带有描边的 style say_dialogue: 会保留对话的字体、字号和颜色。未定义任何样式的游戏看起来与样式出现之前完全一样:在这一改动前后,Novella 对 25 个内置界面的逐帧巡检以及 The Red Thread 的完整游玩绘制出的像素完全相同。
位置由界面决定;外观由样式决定。用于放置某物的主题键,例如 choice_center_y 或 main_menu_xalign,背后没有对应的样式。
| 位置 | 样式 |
|---|---|
| 对话窗口 | say_window(窗口:背景、margin、ysize、padding)、say_dialogue(台词)、say_label(说话者的名字)、namebox(名字标签:背景、padding、xoffset、yoffset)、side_image(xsize)、ctc(点击继续标记:image、accent_color)、centered_text、nvl_window、nvl_dialogue、nvl_label。 |
| 游戏中 | choice_button 和 choice_button_text(xsize、ysize、选项之间的 spacing)、quick_button 和 quick_button_text、tooltip 和 tooltip_text、skip_indicator 和 skip_indicator_text、input_frame 和 input_prompt、notify_frame、notify_title 和 notify_text。 |
| 菜单 | menu_button 和 menu_button_text、option_button 和 option_button_text、page_button 和 page_button_text、main_menu、main_menu_title、main_menu_version、game_menu、game_menu_navigation、game_menu_title、save_slot、save_slot_text、save_slot_note、save_slot_empty、save_slot_label、pref_label、slider、gallery_frame、gallery_locked、gallery_title、history_dialogue、history_choice、history_label、history_empty、achievement_frame、achievement_title、achievement_text(已获得的成就处于选中状态)、about_title、about_text、help_key、help_action、confirm_frame、confirm_text、message_frame、message_title、message_text。 |
| 游戏自有界面 | default(所有样式的根)、text、button 和 button_text、frame、vbox、hbox、grid、fixed、viewport、bar、input 和 input_text。 |
一些内置样式继承自其他样式:centered_text 和 nvl_dialogue 继承自 say_dialogue,nvl_label 继承自 say_label,history_dialogue 继承自 say_dialogue,quick_button_text、menu_button_text、option_button_text 和 page_button_text 继承自 button_text。因此,为 say_dialogue 设置的 outline_width 也会为 NVL 页面和历史记录加上描边,为 button_text 设置的 hover_color 会作用于每个菜单以及游戏自有界面上的按钮。在 Divooka 中,Built-in Style Names 节点会列出所有内置样式。
style say_window:
background "#10141ce8"
border_color "#7fb3c8"
corner_radius 4
style say_label:
weight bold
style choice_button:
hover_background "#2a4a5ef0"
hover_border_color "#ffd98a"
style choice_button_text:
visited_color "#8a8478"
在界面上
界面中的每个元素都用一个样式绘制,按以下顺序选择:
- 元素用
style "name"指定的样式;对于按钮的标签,若故事中存在name_text,则使用它; - 否则,若故事中存在该样式,则使用其容器的前缀下该元素种类的样式:在带有
style_prefix "gold"的界面或容器中,按钮用gold_button绘制,其标签用gold_button_text绘制,text用gold_text绘制,frame用gold_frame绘制; - 否则使用该元素种类自身的样式:
text、button(其标签用button_text)、frame、vbox、hbox、grid、fixed、viewport、bar、input(配合input_text)。
style_prefix 写在界面顶部,与 dim 和 modal 并列;也可以写在容器上,作用于该容器所含的内容。任何样式属性也可以直接写在元素上,放在其他子句之后,并仅针对该元素叠加在其样式之上。在按钮上,带 text_ 前缀的属性用于设置其标签的样式:text_color "#ffffff"、text_size 28。容器在自身未设置内边距、间距和尺寸时,会从其样式中获取这些值。
style gold_button is button:
background "#3a2a10"
hover_background "#6a4a18"
border_color "#c8a05080"
hover_border_color "#ffd27a"
corner_radius 12
style gold_button_text is button_text:
color "#e8dcc6"
hover_color "#ffd27a"
style card is frame:
background "#14202ae8"
padding (40, 30)
screen stall:
style_prefix "gold"
vbox style "card" align (0.5, 0.5) spacing 16:
text "The night market" size 44 outline_width 2
button "Buy a lantern" size (420, 64) return "lantern"
button "Buy a fan" size (420, 64) text_color "#ffffff" return "fan"
button "Leave" size (420, 64) close
vbox 用 card 绘制,按钮因界面的前缀而用 gold_button 绘制;第二个按钮的标签无论处于何种状态都是白色,因为直接写在元素上的属性会叠加在其样式的所有状态之上。

角色
角色可以为其台词的每个部分设置专属外观。这些部分是 window(对话窗口)、namebox(名字标签)、who(名字)和 what(台词)。在 character 行上:
window_style、namebox_style、who_style和what_style为该部分指定一个样式,在该角色的台词中分别代替say_window、namebox、say_label和say_dialogue。为此创建的样式通常继承自对应的内置样式,因此只修改它列出的内容。- 部分名称与任何样式属性连写,即可仅为该角色在该部分上设置这一属性,叠加在该部分的样式之上:
what_size 40、what_outline_width 2、window_background "#200000"、who_color "#ffff00"、namebox_corner_radius 0。
style ghost_window is say_window:
background "#10202cd0"
border_color "#a8d8ff60"
style ghost_name is say_label:
color "#a8d8ff"
character ghost "Ghost" window_style "ghost_window" who_style "ghost_name" what_size 40 what_outline_width 2
character living "Living"
幽灵在自己的窗口中说话,名字使用自己的颜色,台词更大并带有描边;living 则保留 Novella 的默认窗口。
较早的角色选项仍然有效,并且在其有所设置时仍然优先:color 为名字着色,除非 who 样式设置了自己的颜色;size、font 和 text_color(也可拼写为 what_color)会覆盖样式,设置台词的字号、字体和颜色。
台词片段
{style=name}...{/style} 为台词的一部分设置样式:
style shout:
color "#ff6050"
bold true
label start:
ghost "Boo. {style=shout}BOO.{/style}"
片段只采用其样式所设置的内容:当样式或除 default 以外的某个父级设置了 font、size、color、bold 或 weight、描边和阴影时,采用这些设置。只设置颜色的样式会保留周围台词的字号和字体,因此 shout 在幽灵 40 像素的台词和旁白 34 像素的台词中同样适用。片段内部或外围的其他文本标签会叠加在它之上,因此 {style=shout}{size=+10}BOO{/size}{/style} 会更大。
在故事构建器中
故事构建器写出的样式与脚本中的相同。StoryBuilder.Style(name, s => ...) 使用 StyleBuilder 定义一个样式:
story.Style("gold_button", s => s
.Is("button")
.Background("#3a2a10")
.When(StyleState.Hover, hover => hover.Background("#6a4a18").BorderColor("#ffd27a"))
.BorderColor("#c8a05080")
.CornerRadius(12));
story.Style("gold_button_text", s => s
.Is("button_text")
.Color("#e8dcc6")
.When(StyleState.Hover, hover => hover.Color("#ffd27a")));
story.Style("say_dialogue", s => s.Outline(2, "#1a0f0a"));
story.Style("shout", s => s.Color("#ff6050").Bold());
StyleBuilder |
设置 |
|---|---|
Is(parent) |
父级。 |
Set(property, value) |
按脚本名称设置任意属性:Set("line_spacing", 1.4)、Set("padding", Value.FromList([32, 12]))。带状态前缀的名称(hover_color)按原样采用。 |
When(state, s => ...) |
某一状态的属性:When(StyleState.Hover, h => h.Color("#ffd27a")) 设置 hover_color。状态有 StyleState.Idle、Hover、Selected、Insensitive 和 Visited。 |
WhenSelected(state, s => ...) |
被选中元素在另一状态下的属性:WhenSelected(StyleState.Hover, h => h.Color("#ffffff")) 设置 selected_hover_color。 |
Font(families)、Size(size)、Color(color)、Bold(bold)、TextAlign(alignment)、Outline(width, color)、Shadow(shadow, color) |
文本属性。 |
Background(colorOrImage) 或 Background(imageSource)(例如 ImageSources.Frame("gui/tab.png", 24, 24))、Borders(left, top, right, bottom)、BorderColor(color)、CornerRadius(radius)、Padding(left, top, right, bottom)、BoxSize(width, height)、Spacing(spacing) |
容器属性。 |
属性名称在设置时即会检查,因此拼错的名称(Set("colour", ...))会立即抛出异常,而不是什么都不绘制。
角色的各部分在角色定义之后通过 StoryBuilder.CharacterStyle(id, part, style, properties) 设置:
story.Character("ghost", "Ghost")
.CharacterStyle("ghost", "window", "ghost_window")
.CharacterStyle("ghost", "who", properties: who => who.Color("#a8d8ff"))
.CharacterStyle("ghost", "what", properties: what => what.Size(40).Outline(2, "#000000"));
在 ScreenBuilder 上,StylePrefix(prefix) 即界面的 style_prefix;Style(name, prefix) 为最后添加的元素指定样式,并为容器指定作用于其所含内容的前缀;StyleProperty(property, value) 直接在最后添加的元素上设置属性,text_ 开头的名称用于设置按钮标签的样式:
story.Screen("stall", s => s
.StylePrefix("gold")
.VBox(v => v
.Text("The night market", size: 44)
.StyleProperty("outline_width", 2)
.Button("Buy a lantern", ScreenActions.Return("lantern"))
.Button("Buy a fan", ScreenActions.Return("fan"))
.StyleProperty("text_color", "#ffffff")
.Button("Leave", ScreenActions.Hide()), spacing: 16)
.Align(0.5, 0.5)
.Style("card"));
ScriptWriter.WriteStyle 将样式输出为其脚本块;用构建器编写的故事输出后再读回,得到的是相同的脚本。
在 Divooka 中
Novella 工具箱的 Styles 类别每次用一个节点逐步构建样式,就像其他函数式节点构建故事一样:
Start Style(
gold_button,父级button)→ Set Style Property(background,"#3a2a10")→ Set Style Property(background,"#6a4a18",状态hover)→ Set Style Property(corner_radius,12)→ Add Style 添加到故事中
值的输入方式与脚本中的写法相同:颜色写作带引号的 "#ffd27a",尺寸写作 40,边距写作 (24, 12),还有 true、center。状态可以是复合状态,例如 selected_hover。Set Style Background 则从 Images 节点获取背景:Image File、Filtered Image、Composite Image,或 Frame Image(按图片自身的边框拉伸图片)。Style To Script 以脚本形式显示样式,Built-in Style Names 列出故事可以重新设定样式的内置样式。在界面上,Set Screen Style Prefix、Style Last Element(指定样式,对于容器还可指定前缀)和 Set Last Element Style Property 的作用分别等同于脚本中的 style_prefix、style 以及直接写在元素上的属性。在角色上,Character Part Style 为某个部分指定样式,Set Character Style Property 为某个部分设置属性,两者都位于 Characters 类别中。每个节点都会在设置时检查属性名称。
验证器报告的内容
lint(NovellaPlayer lint、控制台的 lint、Check Story 和 Check Game)会读取每个样式以及每个引用样式名称的位置:
- 不是样式(既非内置样式也非故事自有样式)的父级;
- 父级形成循环(
loop_a is loop_b、loop_b is loop_a),作为错误报告; - 样式、角色的部分或界面的元素上 Novella 不认识的属性;
- 类型错误的值:未写作
"#rrggbb"或"#rrggbbaa"的颜色、本应是数字却写了单词、不是一个、两个或四个数字的边距、left、center或right以外的text_align、四种取值以外的weight、不是常量的值; - 既不是颜色、
none、故事定义的图像,也不是游戏文件的背景或图像;引用了游戏中不存在的文件的图像函数;以及不在游戏文件中的声音(根本无法读取的图像函数,例如拼错的fram(...),会在其书写位置报告为错误); - 被引用但未定义的样式:由界面的
style、角色的window_style等,或由{style=name}片段引用。
最后一项最为重要,因为拼错的样式名称在其他情况下不会有任何提示:元素只是以默认外观绘制。
与 Ren'Py 的差异
Novella 的样式遵循 Ren'Py 的模型,包括父级、状态前缀和 style_prefix,并新增了 visited_ 状态,用于在先前游玩中选过的选项。以下内容不受支持:
Frame的tile选项,以及带动画的背景;- 在游戏运行时更改样式(Python 中的
style.button.background = ...、style.rebuild());样式是定义,在游戏启动或脚本重新加载时读取; - 片段的
{=style}写法;请写作{style=name}。
属性集是 Novella 自有的,已在上文列出:Ren'Py 的 outlines 列表对应 outline_width 和 outline_color,文本的 xalign 式数值对应 text_align left|center|right,xpadding 或 left_padding 对应 padding 的各边。没有 Novella 对应项的 Ren'Py 属性会被验证器报告为未知属性。Ren'Py 的 gui 变量在主题中有对应项,主题的键同样是内置样式的简写。