GUI introduction
Contents
Background history
"OldUI" UI Started as a 1:1 clone of Frontier, in 2008, implemented in C++.
"NewUI" was meant to move UI from C++ to Lua, so we don't need to re-compile the game for every change, and allo players to easily modify the game.
"PiGUI" was the second attempt at moving UI from OldUI and NewUI, uses the third party Imgui library, so we get a lot for free, but downside is it's not custom made for us.
Pioneer has at times 3 different UI systems, in the transitioning phase to PiGUI. Now (2021-05) NewUI is completely replaced by PiGUI, but some parts of OldUI still linger (F6,F7,F8 screens).
Getting started with UI
Some pointers on how to get started with PiGUI. The perhaps easiest is to look at the example code script provided below, and modify it. Then search the code for other UI elements you're interested in duplicating. However, let us look at an in-depth way to go from imgui to pioneer's UI.
Imgui
Start with downloading and compiling the stand alone Imgui C++ program according to instructions on their website, and launch, here, we've compiled the sdl opnegl 3 example and launching it from linux command line:
./examples/example_sdl_opengl3/example_sdl_opengl3
Which results in a demo program that shows all features of Imgui.
Many of the features of Imgui are ported to lua in pioneer, but not all. Let's look at how this code makes its meandering path to our in-game pioneer screen.
First, look at the file above, we see the tab-bar is looking very sexy. By what magic is it made? Let's see if we can find it in imgui's C++ source (don't worry, you don't need to know C/C++, other than maybe recognizing if/for-loops and arrays). So we search the C++ source code for some string/sentence visible the demo ap, e.g. "Beetroot".
On line 2026 we find "Beetroot", it's part of a data structure, with the other names we saw in the tab-bar in the demo app. This data structure (an array) is then used on line 2031 (for the check boxes, look at the image of the demo again) and then line 2038 for what seems to be the tab-bar. The function ImGui::BeginTabBar seems to be a good candidate for what's making the magic. Let's see if we can find it in pioneer's source code, e.g. by searching from pioneer's source root directory:
git grep -i BeginTabBar data/pigui/libs/forwarded.lua:ui.beginTabBar = pigui.BeginTabBar data/pigui/libs/wrappers.lua: local open = pigui.BeginTabBar(id) src/lua/LuaPiGui.cpp: bool state = ImGui::BeginTabBar(str.c_str(), tab_bar_flags); src/lua/LuaPiGui.cpp: { "BeginTabBar", l_pigui_begin_tab_bar }, src/pigui/PerfInfo.cpp: if (ImGui::BeginTabBar("PerfInfoTabs")) { src/pigui/PerfInfo.cpp: if (ImGui::BeginTabBar("Texture List")) {
(here, we have ignored the contrib/ folder, that's where we put third party libraries, like imgui, we don't ever want to work or read those). We find matches for BeginTabBar in src/lua/LuaPiGui.cpp. This is where the imgui function is imported (from contrib/imgui) to pioneer's source, then in LuaPiGui we expose it to Lua, by the function l_pigui_begin_tab_bar, which is then mapped to the key "BeginTabBar". Note: this could be arbitrary, e.g. "Begin_Tab_Bar", or "begin_tab_bar", etc. either way, it's the name by which we can call it from the Lua code.
Searching BeginTabBar in data/ we find a match in data/pigui/libs/forwarded.lua:
ui.beginTabBar = pigui.BeginTabBar
This is where we re-name it to something more akin to our lua defined "pigui". So when we use it in our code, we should preferably use "ui.beginTabBar". See example of this further down.
PiGUI
Example
Below is a snippet that if saved to a lua file placed anywhere in data/modules/ will draw a window on the WorldView screen.
Result
Code
-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details -- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt --[[ data/pigui/modules/hello_world.lua Written for @impaktor as a tutorial on how to get started with pigui by @sturnclaw, extended by @impaktor --]] local Game = require 'Game' -- pigui is traditionally imported as 'ui' for simplicity. local ui = require 'pigui' -- gameView is needed to dispatch to our Hello World window local gameView = require 'pigui.views.game' -- we want all displayed text to be localized local lui = require 'Lang'.GetResource('ui-core') -- ui.WindowFlags is an acceleration structure to allow composing window flags -- once and reusing them with each call. local window_flags = ui.WindowFlags { -- We don't want to be able to collapse the window. -- "NoCollapse" } local amount = 1000 local Character = require "Character" -- Register this window as a Game-view module. It will be displayed when the -- player is in the World View. -- registerModule takes two parameters, the unique key of the module, and the -- module descriptor table. gameView.registerModule("Small example", { -- Pretty self explanatory, if this is true, draw() is called when the -- ship is in hyperspace as well as in normal space. ShowInHyperspace = true, -- Called once per frame while the module is active. The function is passed -- the module object (this table) and the frame delta time draw = function(self, deltaTime) -- ui.window takes three parameters: the window title, the window flags, -- and a function containing the body of the window. -- the window title is an IMGUI string ID - two windows cannot share -- the same ID at the same stack position. To work around this, -- everything after ## is not part of the title, but instead used to -- make the window ID unique. ui.window("Debug!##id43", window_flags, function() ui.text("State: " .. Game.player:GetFlightState()) if (ui.beginTabBar("mytabbar")) then if ui.beginTabItem("squares") then ui.text("test1") ui.endTabItem() end if ui.beginTabItem("BALLS!") then ui.text("test2") ui.endTabItem() end if ui.beginTabItem("Triangle") then ui.text("test3") ui.text("some more text") ui.endTabItem() end ui.endTabBar() end -- Give player mone if pressing the button if ui.button("Give money", Vector2(100, 0)) then Game.player:AddMoney(amount) end -- draws a horizontal line ui.separator() -- Reputation & kills if ui.collapsingHeader("Reputation & kills", {}) then Character.persistent.player.reputation = ui.sliderInt("Reputation", Character.persistent.player.reputation, 0, 512) ui.sameLine() ui.text(Character.persistent.player:GetReputationRating()) Character.persistent.player.killcount = ui.sliderInt("Kills", Character.persistent.player.killcount, 0, 6000) ui.sameLine() ui.text(Character.persistent.player:GetCombatRating()) end end) end })