fix(hud): keep a single HUD, preferring the authored one over the fallback

Both the authored template and the built-in fallback name their ScreenGui
"SurvivalHud", and the fallback fires on a 1s timer if no bars have bound
yet. The dedupe ran only once at startup, so a fallback that raced the
template (more likely now startClient does more before the template copies
in) left two HUDs on screen — a restart usually shuffled the timing enough
to hide it, until it didn't.

Tag the fallback, make the dedupe continuous (re-runs whenever another HUD
appears) and always prefer the authored HUD, and don't build the fallback
when a HUD ScreenGui already exists. Also link the inventory/hotbar demo
video in docs/inventory.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Samuel Lison
2026-06-23 17:23:17 +10:00
co-authored by Claude Opus 4.8
parent 5ab855fa1b
commit 1b5ef088b3
3 changed files with 41 additions and 8 deletions
+2
View File
@@ -7,6 +7,8 @@ Character Sheet, with Codex / Achievements / Quests scaffolded). Like the surviv
(icons, counts, fills) — never layout or colors. The engine ships **zero items**; your game registers
them, and their display data replicates to clients automatically.
> 📹 **See it in action:** [Inventory & hotbar system](https://makertube.net/w/wXRkuJo323AHMpZKVwWxt3)
- **Data layer (server):** [src/systems/Inventory.luau](../src/systems/Inventory.luau)
- **UI (client):** `PanelManager`, `InventoryUi`, `Hotbar`, `CharacterSheet`, `DragDrop`, `SlotGrid`,
`UiFallback` under [src/client/](../src/client)
+34 -8
View File
@@ -105,15 +105,31 @@ end
local Hud = {}
local started = false
-- installHud() + the StarterGui->PlayerGui copy can briefly produce two HUDs; keep one.
-- Several paths can land more than one HUD in PlayerGui — installHud()'s clone + the
-- StarterGui->PlayerGui copy, or a late fallback racing the authored template. Keep exactly one,
-- always preferring an AUTHORED HUD over the built-in fallback (and dropping extra copies).
local function dedupeHuds(playerGui: Instance)
local kept: Instance? = nil
local authored: Instance? = nil
local fallbacks: { Instance } = {}
for _, gui in playerGui:GetChildren() do
if gui:IsA("ScreenGui") and gui.Name == HUD_NAME then
if kept then
gui:Destroy()
if gui:GetAttribute(HudFallback.FALLBACK_ATTRIBUTE) then
table.insert(fallbacks, gui)
elseif authored then
gui:Destroy() -- a second authored copy
else
kept = gui
authored = gui
end
end
end
if authored then
for _, fb in fallbacks do
fb:Destroy() -- authored wins; the fallback is only a no-template safety net
end
else
for index, fb in fallbacks do
if index > 1 then
fb:Destroy()
end
end
end
@@ -296,6 +312,13 @@ function Hud.start(_options: { [string]: any }?)
end
dedupeHuds(playerGui)
-- Run it again whenever another HUD shows up (a fallback racing the template, a late
-- StarterGui copy, installHud's clone) so we never end up with two on screen.
playerGui.ChildAdded:Connect(function(child)
if child:IsA("ScreenGui") and child.Name == HUD_NAME then
dedupeHuds(playerGui)
end
end)
-- A stat bar is any GuiObject carrying a `Stat` attribute (how the authored
-- template marks them) OR tagged `SurvivorStatBar` (for the Builder UI / fallback).
@@ -442,11 +465,14 @@ function Hud.start(_options: { [string]: any }?)
end
end)
-- Zero-setup safety net: if nothing showed up, build the minimal fallback.
-- Zero-setup safety net: if no HUD showed up at all, build the minimal fallback. Guard on the
-- ScreenGui's presence (not just whether bars bound yet) so a slow authored HUD never gets a
-- fallback built alongside it; if it still arrives late, the ChildAdded dedupe drops the extra.
task.delay(FALLBACK_WAIT, function()
if next(bound) == nil then
HudFallback.build(playerGui, StatConfig.resolve().stats)
if next(bound) ~= nil or playerGui:FindFirstChild(HUD_NAME) then
return
end
HudFallback.build(playerGui, StatConfig.resolve().stats)
end)
end
+5
View File
@@ -28,9 +28,14 @@ local FALLBACK_COLORS = {
local HudFallback = {}
-- Marks a HUD as the built-in fallback (vs an authored template), so the binder's dedupe can
-- always drop the fallback in favour of an authored HUD when both end up present.
HudFallback.FALLBACK_ATTRIBUTE = "SurvivorCoreHudFallback"
function HudFallback.build(playerGui: Instance, stats: { any })
local screen = Instance.new("ScreenGui")
screen.Name = HUD_NAME
screen:SetAttribute(HudFallback.FALLBACK_ATTRIBUTE, true)
screen.ResetOnSpawn = false
-- Respect the ~36px Roblox topbar inset so bars never render under the CoreGui menu.
screen.IgnoreGuiInset = false