GUI introduction
This page aims to give pointers as to how to get started making UI using pioneer's Lua scripts. For deeper discussion on pioneer's "under the hood" rendering, see forum post: A simple (sort of) description of Pioneers rendering
Contents
Background history
Pioneer's history of different UI (User Interface) systems is a long and winding one, thus the following is a lament of what was.
OldUI
What is now called "OldUI" by the dev team, is the original UI, from when Pioneer started as a 1:1 clone of Frontier, in 2008, implemented in pure C++. Any change to the UI elements required C++ knowledge, a full copy of the soucr code, with compiler, and re-compilation.
NewUI
NewUI was meant to move the UI from C++ to Lua, so we don't need to re-compile the game for every change, and allow players to easily modify the game. The station screens were successfully converted in 2013, by #2589 , but never the cockpit, nor starmaps.
Due to lack of time, and coders, transition stalled, with SystemInfo being halfway there (#3564). At the time of writing this the SectorView (F2) and OrbitView (F6), and others are still defined in the old hard coded C++ source code.
NewUI is recognized by the Deep blue screens, with thin cyan borders on buttons. The bottom Frontier "coockpit"/radar dashbord is still OldUI.
PiGUI
The main creator of NewUI (robn) left the project which was now in a limbo between two simultaneous different UI systems. As ecraven joined, in the summer of 2016 he demonstrated how a third party C++ library, Dear Imgui, could be used to fast prototyping, and he quickly moved the OldUI cockpit to Imgui, and this was done from lua scripts, as the Imgui functions were exposed to the Lua scripting side of the project, thus we call this UI: "PiGUI".
Pioneer had at this time 3 different UI systems, in the transitioning phase to pure PiGUI. A collaborative effort of several new developers (sturnclaw, Gliese852, vakhoir) culminated to remove all NewUI screens by converting them to PiGUI #5032 which finished in december of 2020.
In the transition to PiGui, the galaxy (image) map (F8) was removed, since motivation in the dev team to purge the code base of NewUI right away was higher than to port it to PiGui, as it didn't have any real purpose for the game, other than as a fun gimmick. (The galaxy image is still used under the hood to compute star density in sectors)
At the time of writing (2021-05) OldUI still lingers (F6,F7,F8 screens), but PiGui has replaced most of OldUI, and all of NewUI.
Getting started with UI (PiGUI)
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 })