Gamedevs often implement some solution, replicating a pattern already seen, that works for the intended goal but leads to repetition and a codebase that is more bug-prone, highly coupled to ad-hoc behavior and difficult to reason about and maintain.
It is reasonable that this happens often, as Recoil strays far from regular vanilla Lua, especially with regard to the different ways the lua environment can be affected - and more so in upget1 code - which might make gamedevs hesitant to employ patterns they would otherwise in any other language.
The basecontent-derived upget handlers offer many tools to deal with this situation, and they have been around for decades! But they are often treated as an arcane source, not understood and very rarely adapted to a games needs.
Often gamedevs use globals to talk back and forth between upgets, let's imagine a scenario.
- A settings manager widget2. This widget has knowledge of, can write and read each widgets setting state.
- The settings manager offers an api and requires an api for each widget and each setting.
In the wild, it might look something like this (WG is the shared global in this instance):
-----
-- gui_menu.lua
local showPrices
local function setShowPrices(value)
showPrices = value
-- Code that refreshes internal state
end
function upget:Initialize()
WG['menu'] = {}
WG['menu'].setShowPrices = setShowPrices
-- we omit the getter pattern for brevity
end
-----
-- gui_options.lua
-- we omit the gui code for brevity
local function clickedOnShowPriceDropDown(value)
if WG['menu'] and WG['menu'].setShowPrices then
WG['menu'].setShowPrices(value)
WG['someotherwidget'].showPricesChanged(value) -- another widget that cares about this setting
end
end
That's a lot of boilerplate! Even considering the case that we can 'metaprogram' a bit to reduce repetition, the setters and getters still need to be defined. Further attempts to improve this situation while not rethinking the architecture only lead to forever more clever, ad-hoc, bug-prone solutions.
Use handler:RegisterGlobal to define shared APIs and sculpt the global scope of your widgets.
-----
-- gui_options.lua
-- some API for dealing with options, containing cache and event dispatch
-- e.g. set, get, register and so on
local optionsStore
local setShowPrices
function widget:Initialize()
widgetHandler:RegisterGlobal('Option', optionsStore.register)
setShowPrices = Option('showPrices')
end
local function clickedOnShowPriceDropDown(value)
setShowPrices(value) -- calls listening widgets
end
-----
-- gui_menu.lua
local setShowPrices, showPrices
local function showPricesChanged(value)
showPrices = value
-- Code that refreshes internal state
end
function upget:Initialize()
-- Option returns current value and setter
setShowPrices, showPrices = Option('showPrices', showPricesChanged)
end
local function widgetDecidesToSetOption(value)
setShowPrices(value) -- calls showPricesChanged
end
The proposal omits many important considerations such as: ownership, sandboxing, setting without callback (for hot loops or special internal logic), etc...
What's important is that such considerations don't exist if all widgets use the shared global anyway, and now we don't depend on whether some widget changed names or changing both options and menu widget, having to gather all the context embedded in both codebases and increasing the chance of introducing regressions or spaghettifying more.
Use handler = true for raw access to the handler itself, enabling further customization. An upget with handler = true can manage other widgets and define custom callins for example, encapsulating this logic and insulating the manager code from containing game specific logic.