Valericoe Posted May 5, 2020 Share Posted May 5, 2020 (edited) Hey Everyone, Today I'll be detailing the creation of a custom user interface. 1. Setting up the debugging environment (Optional) Placing UI elements takes some trial and error. I recommend running a dedicated server if you're making a clientside mod or adding a keybind to reload the latest save. Placing something like this in your modmain.lua allows you to press CTRL+R in-game to reload your latest save. (If this is not working, set your server max player count to 1) local _g = GLOBAL local require = _g.require AddSimPostInit(function() _g.TheInput:AddKeyHandler( function(key, down) if not down then return end -- Only trigger on key press -- Require CTRL for any debug keybinds if _g.TheInput:IsKeyDown(_g.KEY_CTRL) then -- Load latest save and run latest scripts if key == _g.KEY_R then if _g.TheWorld.ismastersim then _g.c_reset() else _g.TheNet:SendRemoteExecute("c_reset()") end end end end) end) 2. Displaying and Closing the UI In order to display a screen for a client, we need to 'push' it to TheFrontEnd. If we want a persistent widget, we can use AddChild Here we've added a keybind to T that determines if we have an active screen and if that screen is the player HUD. We then push the "spawner.lua" screen with the player as an argument. Replace "spawner" with your screen name. It also calls OnClose() if T has been pressed while the screen is active. local _g = GLOBAL local require = _g.require AddSimPostInit(function() _g.TheInput:AddKeyHandler( function(key, down) -- Only trigger on key down if not down then return end -- Push our screen if key == _g.KEY_T then local screen = TheFrontEnd:GetActiveScreen() -- End if we can't find the screen name (e.g. asleep) if not screen or not screen.name then return true end -- If the hud exists, open the UI if screen.name:find("HUD") then -- We want to pass in the (clientside) player entity TheFrontEnd:PushScreen(require("screens/spawner")(_g.ThePlayer)) return true else -- If the screen is already open, close it if screen.name == "spawner" then screen:OnClose() end end end -- Require CTRL for any debug keybinds if _g.TheInput:IsKeyDown(_g.KEY_CTRL) then -- Load latest save and run latest scripts if key == _g.KEY_R then if _g.TheWorld.ismastersim then _g.c_reset() else _g.TheNet:SendRemoteExecute("c_reset()") end end end end) end) 3. Creating the Screen Skeleton We need to import some widgets from the base game. These abstract much of the details away while giving us extensive functionality. local Screen = require "widgets/screen" local Widget = require "widgets/widget" local Templates = require "widgets/templates" local Text = require "widgets/text" local ImageButton = require "widgets/imagebutton" Next we create the screen class and register the screen. Replace "spawner" with your screen name. The screen at this moment is just a translucent black square that covers the entire screen. This creates an effect of dimming the game behind the UI. local Spawner = Class(Screen, function(self, inst) -- Player instance self.inst = inst -- Any 'DoTaskInTime's or 'DoPeriodicTask's should be assigned in here -- These are cancelled upon close to prevent stale components self.tasks = {} -- If you want to maintain state look into GetModConfigData, Replicas, or TheSim:SetPersistentString -- Register screen name Screen._ctor(self, "spawner") -- Darken the game -- We're using the DST global assets self.black = self:AddChild(Image("images/global.xml", "square.tex")) self.black:SetVRegPoint(ANCHOR_MIDDLE) self.black:SetHRegPoint(ANCHOR_MIDDLE) self.black:SetVAnchor(ANCHOR_MIDDLE) self.black:SetHAnchor(ANCHOR_MIDDLE) self.black:SetScaleMode(SCALEMODE_FILLSCREEN) self.black:SetTint(0, 0, 0, .5) end Next we're going to add the OnClose function. function Spawner:OnClose() -- Cancel any started tasks -- This prevents stale components for k,v in pairs(self.tasks) do if v then v:Cancel() end end local screen = TheFrontEnd:GetActiveScreen() -- Don't pop the HUD if screen and screen.name:find("HUD") == nil then -- Remove our screen TheFrontEnd:PopScreen() end TheFrontEnd:GetSound():PlaySound("dontstarve/HUD/click_move") end Next we're adding some logic to temporarily intercept keypresses to detect ESC. function Spawner:OnControl(control, down) -- Sends clicks to the screen if Spawner._base.OnControl(self, control, down) then return true end -- Close UI on ESC if down and (control == CONTROL_PAUSE or control == CONTROL_CANCEL) then self:OnClose() return true end end Finally, make sure to return the class you just created. return Spawner Test your UI by pressing T a few times in-game. Your screen should darken on the first press, then return to normal on a second press. If your screen is getting progressively darker that likely means you're not closing the screen. 4. Adding Elements Now that we have a basic screen we're going to add some elements. The most important one is the ROOT, all future elements should be a child of this widget. Children of the ROOT will be positioned relative to it. -- Set the inital position for all our future elements self.proot = self:AddChild(Widget("ROOT")) self.proot:SetVAnchor(ANCHOR_MIDDLE) self.proot:SetHAnchor(ANCHOR_MIDDLE) -- 20 'pixels' to the right - This scales with the window size self.proot:SetPosition(20, 0, 0) self.proot:SetScaleMode(SCALEMODE_PROPORTIONAL) Some images are split into smaller images which may be reassembled with a template. -- We're using a template for our background -- In this case we're calling a function to assemble the pieces of "images/dialogcurly_9slice.xml" -- The offsets center it above the player's inventory self.bg = self.proot:AddChild(Templates.CurlyWindow(500, 450, 1, 1, 68, -40)) Adding golden header text self.title = self.proot:AddChild(Text(NEWFONT_OUTLINE, 40, "Spawn Disaster", {unpack(GOLD)})) self.title:SetPosition(0, 250) Adding animations Since positioning can be difficult to determine, we've added Text elements that update their position 10x per second. self.animationUp = self.proot:AddChild(Text(NEWFONT_OUTLINE, 30, "Y: ", {unpack(RED)})) self.animationUp:SetPosition(-520, -350) -- Assign the task to the client self.tasks[#self.tasks + 1] = self.inst:DoPeriodicTask(.1, function() local pos = self.animationUp:GetPosition() self.animationUp:SetPosition(pos.x, pos.y > 350 and -350 or pos.y + 5) self.animationUp:SetString("Y: " .. pos.y) end) self.animationRight = self.proot:AddChild(Text(NEWFONT_OUTLINE, 30, "X: ", {unpack(RED)})) self.animationRight:SetPosition(-600, -290) -- Assign the task to the client self.tasks[#self.tasks + 1] = self.inst:DoPeriodicTask(.1, function() local pos = self.animationRight:GetPosition() self.animationRight:SetPosition(pos.x > 600 and -600 or pos.x + 5, pos.y) self.animationRight:SetString("X: " .. pos.x) end) In this section we'll be adding cards that are defined in disasters.lua. (attached) We're using the ImageButton class to add functionality upon click and display an icon with the button text. The default position isn't quite centered, so we center the internal elements accordingly. Images are added by specifying the atlas and tex, note that the tex doesn't include "/images". local function createDisasterCard(index, x, y, scale) local disaster_card = {} disaster_card.fill = self.disasters:AddChild(Image("images/fepanel_fills.xml", "panel_fill_tall.tex")) disaster_card.fill:SetPosition(x, y) disaster_card.fill:SetScale(.25 * scale, .5 * scale) disaster_card.title = self.disasters:AddChild(Text(NEWFONT_OUTLINE, 40, disasters[index].name or " ")) disaster_card.title:SetPosition(x, y + 170) disaster_card.title:SetScale(scale) disaster_card.description = self.disasters:AddChild(Text(NEWFONT_OUTLINE, 25, disasters[index].description or " ")) disaster_card.description:SetPosition(x, y) disaster_card.description:SetScale(scale) disaster_card.description:SetRegionSize(175, 350) disaster_card.description:EnableWordWrap(true) disaster_card.description:EnableWhitespaceWrap(true) disaster_card.button = self.disasters:AddChild(ImageButton("images/frontend.xml", "button_long.tex", "button_long_highlight.tex", "button_long_disabled.tex", nil, nil, {1,1}, {0,0})) disaster_card.button:SetPosition(x + 5, y - 170) disaster_card.button:SetScale(.6 * scale) disaster_card.button:SetText("Do it!") disaster_card.button.text:SetPosition(-5, 4) disaster_card.button:SetFont(BUTTONFONT) disaster_card.button.icon = disaster_card.button:AddChild(Image(disasters[index].iconAtlas or "images/global.xml", disasters[index].iconTex or "square.tex")) disaster_card.button.icon:SetPosition(-100, 10) -- Left of text disaster_card.button.icon:SetScale(scale) -- Pass clicks through to the button disaster_card.button.icon:SetClickable(false) disaster_card.button:SetOnClick(function() -- RPC to server local modName = KnownModIndex:GetModActualName("Custom UI") SendModRPCToServer(GetModRPC(modName, "SpawnDisaster"), index) end) end We then use this function to create UI elements based on disasters.lua (attached). local disasters = require "screens/disasters" self.disasters = self.proot:AddChild(Widget("ROOT")) self.disasters:SetPosition(-100, 0) self.disasters.cards = {} for i=1, #disasters do self.disasters.cards[i] = createDisasterCard(i, (i - 2) * 220, 0, 1) end The SpawnDisaster RPC is defined in modmain.lua as so: local disasters = require "screens/disasters" local modName = _g.KnownModIndex:GetModActualName("Custom UI") AddModRPCHandler(modName, "SpawnDisaster", function(player, index) if index <= #disasters then print("Spawning Disaster " .. (disasters[index].name or "NIL") .. " by " .. (player.name or "NIL")) -- Last index is random disasters[index == #disasters and math.random(#disasters - 1) or index].activate(player) end end) Draw the rest of the owl in disasters.lua (or download it). Done! If you want something specific, look at the game screen for references. Often Klei builds widgets that are convenient to work with. Please let me know if there's anything I can improve. Best of luck with your user interfaces! Thanks, Val disasters.lua customUI.zip Edited December 8, 2021 by Valericoe 8 1 Link to comment Share on other sites More sharing options...
Thomas Die Posted October 3, 2020 Share Posted October 3, 2020 Damn is this still working this looks amazing Link to comment Share on other sites More sharing options...
Wonderlarr Posted December 1, 2020 Share Posted December 1, 2020 How do I go about making custom inventory interfaces? I need to have a custom inventory widget for 5 specific items, that are fixed to nothing but those items. Link to comment Share on other sites More sharing options...
Valericoe Posted May 6, 2021 Author Share Posted May 6, 2021 On 10/3/2020 at 3:14 PM, thomas4846 said: Damn is this still working this looks amazing Yup, still works. On 12/1/2020 at 3:31 PM, TheSkylarr said: How do I go about making custom inventory interfaces? I need to have a custom inventory widget for 5 specific items, that are fixed to nothing but those items. I think the bundling wrap code is a good place to start. Link to comment Share on other sites More sharing options...
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now