界面

本页面讲述的内容长期有效
Lhy19981018讨论 | 贡献2020年11月22日 (日) 17:10的版本 →‎Basics

CK3的用户界面(UI)是高度可修改的,但由于这个原因,UI修改器会禁用成就,因为玩家可能会用它们作弊。

游戏还包含一个GUI编辑器,可以在游戏中检查UI元素并编辑它们。

mod作者可以:

  • 改变界面的视觉风格
  • 使窗口可以移动和调整大小。
  • 更改和删除要素
  • 增加新的按钮
  • 从代码中显示更多信息
  • 增加新的窗口(有一个变通办法)

mod作者不能:

  • 增加新的热键(只能重复使用现有的热键)。
  • 在另一个窗口中显示一个窗口的信息,除非开发者包含这种可能性。


基础

CK3的界面是通过游戏/gui文件夹中的.gui文件创建的,它有点类似于html文件。

因此,你可以用任何文本编辑器来编辑它们,比如 VS Code, Sublime or Atom。语法高亮选择Python或Perl 6,它们很适合。

CK3使用.dds文件来制作纹理,它保存在game/gfx/文件夹内。

要编辑或保存.dds文件,可以使用带有Intel插件的Photoshop或带有this插件的GIMP。


要在游戏中重新加载gui文件并使用GUI编辑器,添加-debug_mode和-develop启动选项。

  • 在Steam上右击游戏,选择属性,设置启动选项,添加-debug_mode -develop

你可以使用以下控制台命令。

  • reload gui - 重载所有的gui文件,以显示游戏中的任何变化。
  • reload texture - 重新加载所有纹理文件。
  • gui editor - 打开GUI编辑器。
  • tweak gui.debug - 允许启用UI元素的高亮显示。工具提示将显示其名称、大小和位置。
  • DumpDataTypes - 将在你的日志文件夹中创建一个data_types.log(Documents/Paradox Interactive/Crusader Kings III/logs),列出每个窗口/游戏对象的所有可用的GUI函数。

其他提示。

  • 始终打开错误日志,查看代码中是否有错误(在同一个日志文件夹中)。
  • 点击控制台下方的 "Toggle Release Mode "启用游戏中的错误跟踪器,查看你的更改是否导致任何新的错误。
  • 当使用reload gui命令时,游戏静音,因为每次都会触发前奏的声音。
  • 使用Reload GUI mod,它增加了一个重新加载gui的按钮(用热键),并在你重新加载时关闭设置窗口。
  • 将gui文件夹添加到你的文本编辑器中,这样它就会使用它来自动完成。
  • 你可以在gui/debug中使用test_gui.gui进行测试。要显示这个窗口,打开控制台并点击测试窗口。
  • 折叠代码,这样更容易看到它的结构。通常的热键是Ctrl+K、Ctrl+1(其中1是折叠代码的级别)。

创建GUImod

1. 启动游戏启动器,进入Mods,Mod Tools,填写所有字段,包括标签。

点击创建会在Documents/Paradox Interactive/Crusader Kings III/mod中新建一个文件夹和一个.mod文件。

2. 接下来,在你的mod里创建一个 "gui "文件夹,然后从game/gui里复制你要修改的文件到那里。

  • 如果你不知道需要哪个文件,请使用GUI编辑器,在游戏中检查它。

GUI Editor

GUI Editor是一款用于编辑游戏中UI的开发者工具。

要使用它,你需要在-debug_mode-developer选项下启动游戏。


要打开编辑器,可以选择

  • 按Ctrl+F8键
  • 用`键(在Esc下面)打开控制台,点击GUI编辑器。
  • 打开控制台,运行gui_editor命令。

特点

默认情况下,编辑器启动时启用了编辑模式。你可以在顶部窗口中禁用它,称为Outliner。热键 "E"。

  • 编辑模式类似于浏览器中的检查模式。当它被启用时,你不能与游戏互动,但它允许你选择UI的部分,并在下面的属性窗口中更改它们。
  • 用鼠标滚轮滚动来改变编辑器应该关注的元素,因为hud.gui往往会被放在其他窗口的顶部。
  • 黄色边框表示选中的元素。要隐藏其他边框,请取消勾选Outliner中的 "Show Hierarchy"(热键 "L")。
  • 按住鼠标右键可以移动所选元素。
  • 要撤销任何操作,请按Ctrl+Z或大纲中的撤销按钮。重做是旁边的Ctrl+Y。
  • Outliner中的红星*表示未保存的更改。按Ctrl+S或顶部的保存按钮来保存它们。确保你正在编辑的gui文件在你的mod中,否则它会将更改写入游戏文件夹。(要重置它们,请从Steam的属性窗口验证完整性)
  • 你可以通过拖动任何开发窗口来移动它们,并通过拖动边缘来调整它们的大小。
  • 您可以在Outliner的层次结构中拖动UI元素来重新排序。右键单击以显示上下文菜单。
  • 您可以在 "属性 "窗口中更改或添加新属性(通过单击加号)。


通过单击Outliner中的 "窗口",您可以打开另外两个窗口。UI组件和注册数据类型。

  • gui/shared/standard.gui和gui/defaults.gui包含最常见的东西,如按钮、图标和文本。
  • 注册数据类型可以用来查询游戏中显示数据的功能。
  • 点击数据类型中的右上角按钮会将这些数据转储到你的日志文件夹(Documents/Paradox Interactive/CK3/logs)。你也可以使用 "DumpDataTypes "控制台命令。


注意:通常很容易误选一个模板,改变模板会影响UI中的所有实例。注意你在大纲中选择了什么文件。如果你在属性窗口中看到一个以 "type: "开头的蓝色标题,那就是一个模板,所以要注意不要编辑这部分内容(除非你打算编辑)。

UI code

CK3’s UI is composed of containers and objects inside them.

Most windows, for example, are created using a window container, while map icons use a widget or an hbox.

The order in the file determines the order on the screen: what's lower in the code will appear on a higher layer.

Most elements can also contain others, for example, there can be a textbox inside an icon inside a button. Nested elemenets, aka children, will will be moved with their parent.

Position is set relative to the top left corner (either of the screen or the parent). This can be changed with parentanchor property. The available options are: left, right, top, bottom, hcenter (horizontal center) and vcenter (vertical center). They can be combined with a | like this: parentanchor = right|vcenter.

Every element is opened and closed with curly brackets, like this: container = { }.

The common code style is to open and close the block on the same level, while indenting the contents with one tab:

widget = {
  size = { 50 50 }
  alpha = 0.5
}

This helps you see the structure of the code better, notice any missing or extra brackets, and is needed for some editors to correctly fold the blocks of code.

UI Components

Window

  • The only movable container. To enable movement, add movable = yes property.
  • Can be fixed size or resized by its children.
  • In the game the background is set using templates, like using = Window_Background and using = Window_Decoration.
  • If a child is outside of a window, it won't be clickable and won't show a tooltip. Use allow_outside = yes to change this.

widget

  • A static container. Similar to a window in other regards.

margin_widget

  • Similar to a widget, but can be resized with margins. (This allows us to make windows that resize to screens of different size, by setting height to 100% and margins to ~50 to show the hud)

container

  • Does not have a fixed size (but you can set maximumsize).
  • Resizes automatically to fit all its children, including invisible ones. Use ignoreinvisible = yes to ignore them.
  • Often used to group multiple elements to move them together.

flowcontainer

  • Arranges all its children in a horizontal row. Use direction = vertical to make it vertical.
  • Doesn't ignore invisible children by default. Use ignoreinvisible = yes to change it.
  • Does not have a fixed size.
  • Its children cannot have positions, as they are set automatically.
  • If you need to adjust position of its child, you can put it inside a container or a widget and then change position relative to this parent.

hbox vbox

  • Arranges all its children in a horizontal row and spreads them along its width. Vbox is the same but vertical.
  • Cannot have a fixed size, but instead takes the width of its parent as its own (ignoring the parent's margins). Vbox takes the height.
    • If its parent can't have a fixed size (like flowcontainer), this may crash the game.
  • Can be limited by minimumsize/maximumsize and margins.
  • Ignores invisible children by default. Use ignoreinvisible = no to change this.
  • An hbox inside another hbox will have 0 size and won't spread its children. Use layoutpolicy_horizontal = expanding to resize it (or layoutpolicy_vertical = expanding in a vbox)
  • Accepts datamodels (to create lists from game data).

dynamicgridbox

  • Is only used with datamodels.
  • Arranges all the items vertically. Use flipdirection = yes to make it horizontal.
  • Doesn't ignore invisible items by default. Use ignoreinvisible = yes to change it.
  • Can be fixed size, resized by the content and limited by minimumsize and maximumsize.
  • Items can be of different size.
  • Can become laggy with very long lists.

fixedgridbox

  • Similar to a dynamic box but all its itmes are of fixed size (it's essentially a table).
  • Is only used with datamodels.
  • Arranges all its items vertically. Use flipdirection = yes to make it horizontal.
  • Cannot ignore invisible items.
  • Can be fixed size, resized by the content and limited by minimumsize and maximumsize.
  • Much better for performance with long lists.

overlappingitembox

  • Is only used with datamodels.
  • Arranges all its items horizontally and overlaps them if the list is longer that the size of the box. Use flipdirection = yes to make it horizontal.
  • Can be fixed size or autoresized.

scrollarea

  • A widget with scrollbars that appear if the content is bigger than its size.
  • Scrollbars can be disabled with scrollbarpolicy_horizontal = always_off and scrollbarpolicy_vertical = always_off.
    • A scrollarea with no scrollbars can be used to crop lists or images.
  • Can be fixed size, resized by the content and limited by minimumsize and maximumsize.

button

  • A clickable object. Accepts onclick and onrightclick.
    • When adding a right click function, include button_ignore = none.
  • Doesn't have a texture by default.
  • Can be fixed size or resized by its children.
    • A 0x0 button can be used to add invisible hotkeys.

icon

  • Displays a texture.
  • Can be used as a widget to store children.
  • Can be flipped with mirror = horizontal or mirror = vertical.

textbox

  • Shows text.
  • Can be fixed size or autoresized.
  • Use elide = right or elide = left to cut off text that is too long
  • Can be a single line or multiple, with multiline = yes.
  • Game files often use templates set in gui/shared/text.gui, like text_single. Use them to keep visual consistency and to type less code every time.

Promotes and Functions

Each window has a predefined set of commands - promotes and functions - available for it. These can be found in the data_types.log file in Documents/Paradox Interactive/Crusader Kings III/logs/ after you use the DumpDataTypes console command.

They are used to display all data from the game, like your name, gold, children, and to set button actions.

A promote returns a scope, i.e a game object, like Character or Province, while a function returns a number, a string or a boolean (true/false) value, etc.

Global commands can be used anywhere, like GetPlayer (returns the player character) or GetCurrentDate.

Other commands can only be used in their window/object, for example, GetParents can only be used in the character window and must be started with CharacterWindow.GetParents.

Commands can be chained like this:

CharacterWindow.GetCharacter.GetPrimaryTitle.GetHeir.GetPrimarySpouse.GetFather

Children can inherit the scope from their parent, meaning we wouldn't need to retype the line above to show information about this character. Instead we can set datacontext of a widget to this line and then every textbox in it will use "[Character.GetNameNoTooltip]", "[Character.GetGold]", etc.

The same applies to items in gridboxes.

Templates

Templates are named blocks of code which can be used multiple times throughout the code, which helps maintain the same style and reduce the amount of code we write. Editing the template will edit all instances of it!

Templates are global and can be defined in any file. Most of the game templates are stored in gui/shared. A local version, local_template, has to be defined within the same file.

Templates can store the contents of an entire window or just one line, like this one:

template Window_Size_Sidebar
{
		size = { 610 100% }
}

This template can be used inside another element with "using = Window_Size_Sidebar", which will, essentially, replace the "using" line with the contents of the template.

Types

If templates can contain just a few properties, types are always whole elements, like a button or a widget.

text_single and text_multi are types of a textbox with many properties already defined for them, so we don't need to retype them every time and instead simply write:

text_single = {
	text = "my text"
}

Types are defined in a slightly different way, by creating a named group of types first:

types Standard_Types
{
	type text_single = textbox {
	...
	}
}

Blockoverride

Templates and types can have named override blocks, which allow us to edit a part of an instance without changing the whole template. For example, a template may have a default block of text:

block "text"
{
	text = "default_text"
}

To replace it, we add a blockoverride with the same name in our instance:

blockoverride "text"
{
	text = "actual text"
}

We can also remove it from our instance like this: blockoverride "text" {}

Scripted GUIs

Scripted guis are, essentially, hidden events triggered from the UI.

The are stored as .txt files in game/common/scripted_guis and cannot be reloaded from the game, unlike the .gui files.

The basic structure of a scripted gui is:

gui_name = { 
	scope = character 		# the root scope, i.e. the target of the effects
	saved_scopes = {} 		# any additional targets

	is_shown = {} 		# is it visible on the UI?

	ai_is_valid = {} 		# is the AI allowed to use it? Disabled by default.

	is_valid = {} 		# can the player use it?

	effect = {			# what it does
		custom_tooltip = ""	# adds a tooltip
	}	 			
}

Not all of the blocks are necessary. Sometimes a scripted gui may only contain the scope and is_shown or effect.

In the .gui file we use the following:

datacontext = "[GetScriptedGui('gui_name')]"

onclick = "[ScriptedGui.Execute( GuiScope.SetRoot( GetPlayer.MakeScope ).End)]"

visible = "[ScriptedGui.IsShown( GuiScope.SetRoot( GetPlayer.MakeScope ).End)]"

enabled = "[ScriptedGui.IsValid( GuiScope.SetRoot( GetPlayer.MakeScope ).End)]"

tooltip = "[ScriptedGui.BuildTooltip( GuiScope.SetRoot( GetPlayer.MakeScope ).End)]"

datacontext is necessary to link the element to the scripted gui. Other commands won't work without it.

In this example the scripted gui is scoped to the player with a global function GetPlayer.MakeScope. It can be changed to someone in the character window, e.g. CharacterWindow.GetCharacter.MakeScope, or if the scope was a province, HoldingView.GetProvince.MakeScope.

ScriptedGui.Execute is used with buttons and will execute everything listed in the effect block in the scripted gui.

IsShown and IsValid check for conditions in is_shown and is_valid blocks.

BuildTooltip can also be used with a textbox to display custom text in the UI.

To save another scope to our scripted gui, we use AddScope like this:

"[ScriptedGui.Execute( GuiScope.SetRoot( GetPlayer.MakeScope ).AddScope('target', CharacterWindow.GetCharacter.MakeScope ).End )]"

And then we use the same name in the scripted gui:

saved_scope = {
  target
}

Multiple scopes can be saved this way.

It is important to not put any spaces before the dots or opening parentheses! Execute( is correct, Execute ( is not. Other spaces can be omitted, but they help with readability.

Displaying a variable or script value

Variables and script values can be shown on the UI like this:

variable: text = "[GetPlayer.MakeScope.Var('test_var').GetValue|1]"

svalue: text = "[GuiScope.SetRoot( GetPlayer.MakeScope ).ScriptValue('test_value')|0]"

In this example the variable is stored in the player character and is called test_var. Any other scope can be used, just remember to add MakeScope.

|1 at the end is optional and will cut off all decimals save for one, so instead of 1.573 you will see 1.5. Note that it does not round the value. You can set this at any number, add % to convert to a percentage, add = or + to color the value if it's positive or negative.

When using localization to display values in events, we use this:

event_var: "[ROOT.GetCharacter.MakeScope.Var('test_var').GetValue|0]"

event_value: "[SCOPE.ScriptValue('test_value')|0]"

If you have a saved scope (named "target" here), it changes to this:

event_var: "[target.MakeScope.Var('test_var').GetValue|0]"

event_value: "[GuiScope.SetRoot( target.MakeScope ).ScriptValue('test_value')|0]"

Displaying data lists

We can create custom lists of characters, for example, to make societies.

To do this, first, we need to add them to a variable list. This can be done though an event or a scripted gui, like this:

effect = {
	every_living_character = {
		limit = {
			has_trait = paranoid
		}

		root = {
			add_to_variable_list = {
				name = secret_society
				target = prev
			}
		}
	}
}

If we fire this effect for the player, they will be the root, so the list will be stored in them.

Then we use any list box (vbox, dynamicgridbox, fixedgridbox) with the datamodel set to our list:

dynamicgridbox = {
	datamodel = "[GetPlayer.MakeScope.GetList('secret_society')]"

	item = {
		flowcontainer = {
			datacontext = "[Scope.GetCharacter]"

			portrait_head_small = {}

			text_single = {
				text = "[Character.GetNameNoTooltip]"
			}
		}
	}
}

New windows and toggles

There isn't a simple way to create a new window and make your mod compatible with others. We have to either overwrite the hud or other windows.

The usual way is to add the new window to hud.gui inside of ingame_topbar widget and then add a button to show/hide it.

The alternative is to rewrite one of the unused windows, like test_gui and add a button with the console command that shows it.

Toggles with PdxGuiWidget

PdxGuiWidget is a simple function used to hide or reveal named elements.

In this example we have a container with a hidden submenu, one button that shows it and another that hides it.

  1. when clicked, the first button goes back to its parent, searches in it for the submenu and the other button, reveals them and hides itself
  2. the second button searches for the element and the button, reveals them and hides itself
container = {
	button = {
		name = "show submenu"

		onclick = "[PdxGuiWidget.AccessParent.FindChild('submnenu').Show]"
		onclick = "[PdxGuiWidget.AccessParent.FindChild('hide submenu').Show]"
		onclick = "[PdxGuiWidget.Hide]"
	}
		
	button = {
		name = "hide submenu"
		visible = no

		onclick = "[PdxGuiWidget.AccessParent.FindChild('submnenu').Hide]"
		onclick = "[PdxGuiWidget.AccessParent.FindChild('show submenu').Show]"
		onclick = "[PdxGuiWidget.Hide]"
	}

	widget = {
		name = "submenu"
		visible = no
	}
}

If the elements are separated by more parents/children, we can repeat AccessParent like this:

onclick = "[PdxGuiWidget.AccessParent.AccessParent.AccessParent.AccessParent.FindChild('submnenu').Show]"

Each button can hide or reveal multiple elements of any type. You only need to provide the name.

Pros:

  • easy to edit on the fly as any changes can be reloaded with the "reload gui" command

Cons:

  • the toggles will reset any time the game is restarted
  • If you want to hide multiple things or toggles, the code will get very bloated and hard to manage
  • trying to hide entries in a data list (like a dynamicgridbox) will only hide the first instance

Toggles with animation

We can set up animations that are triggered by buttons (or conditions) to hide/show elements or to even move them. This way we don't need to count how many parents separate the button and the window and we can trigger many things at once with just one onclick.

The previous example would look like this and work the same way. The first button triggers "show_submenu", which hides the button and shows the rest, while the second button triggers "hide_submenu", which hides this button and the widget and shows the first button.

container = {
	button = {

		state = { # this is an animation
			name = show_submenu
			on_start = "[PdxGuiWidget.Hide]"
		}
		state = {
			name = hide_submenu
			on_start = "[PdxGuiWidget.Show]"
		}

		onclick = "[PdxGuiTriggerAllAnimations('show_submenu')]"
	}
		
	button = {
		visible = no

		state = {
			name = show_submenu
			on_start = "[PdxGuiWidget.Show]"
		}
		state = {
			name = hide_submenu
			on_start = "[PdxGuiWidget.Hide]"
		}

		onclick = "[PdxGuiTriggerAllAnimations('hide_submenu')]"
	}

	widget = {
		visible = no

		state = {
			name = show_submenu
			on_start = "[PdxGuiWidget.Show]"
		}
		state = {
			name = hide_submenu
			on_start = "[PdxGuiWidget.Hide]"
		}
	}
}

it is longer, but animations can be saved as templates and reused with one line, like using = hide_animation. Fullscreen Barbershop uses animations extensively, if you want a better example.

Pros:

  • can also be edited on the fly, with "reload gui" command
  • easier to link many things together, and even open a different window and trigger an animation in it

Cons:

  • animation blocks can be quite lengthy
  • all toggles will reset when the game is restarted

Toggles with Scripted GUIs

Scripted guis allows us to use script to toggle visibility. They make it easier to manage multiple things but can impact performance when used in big numbers.

For a scripted toggle we need to create a .txt file in common/scripted_guis/. The name of the file can be anything.

A basic toggle in this file would looks like this:

gui_toggle = {
	scope = character
	
	is_shown = {
		has_variable = gui_toggle
	}
	
	effect = {
		if = {
			limit = {
				has_variable = gui_toggle
			}
			remove_variable = gui_toggle
		}
		else = {
			set_variable = gui_toggle
		}
	}
}

When clicked, it adds a variable to the scoped character and removes it when clicked again.

Then, if the variable is present, our window will be visible. If it's not, it's hidden.

This is how the gui file would look like. We can use just one button as its function will change with each click.

container = {
	button = {
		datacontext = "[GetScriptedGui('gui_toggle')]"
		onclick = "[ScriptedGui.Execute( GuiScope.SetRoot( GetPlayer.MakeScope ).End )]"
	}

widget = {
	datacontext = "[GetScriptedGui('gui_toggle')]"
	visible = "[ScriptedGui.IsShown( GuiScope.SetRoot( GetPlayer.MakeScope ).End )]"
	}
}

"GetPlayer" is a global promote and returns the player character.

"Player.MakeScope" makes our player the scope of the scripted gui, meaning this is where we store the variable in.

Two buttons can be used if you want different tooltips for them. In this case, copy the 'visible' property to both buttons, but add 'Not' to one of them, so it's hidden by default:

visible = "[Not(ScriptedGui.IsShown( GuiScope.SetRoot( GetPlayer.MakeScope ).End ))]"

Pros:

  • the toggles are saved in the character and won't reset unless they die or you start a new game
  • easier to link many things, even in different windows

The downsides:

  • you need to restart the game to update scripted guis
  • it's a little harder to remember the syntax (copy the code from here to reduce the chance of mistakes)