barsKit
Health and resource bars for any world: **one kit, lots of looks.** You pick a look and say what the bar shows. To change the look later you change one word, live, and nothing else gets rewritten.
barsKit 1.3.0
Health and resource bars for any world: one kit, lots of looks. You pick a look and say what the bar shows. To change the look later you change one word, live, and nothing else gets rewritten.
- 20 looks of its own, plus Lifeline's 13 as
lifeline:<style>, in 8 theme families: sci-fi, fantasy, cartoon, pixel, minimal, realistic, horror and cozy. Each look says its shape, whether it is a screen or world bar, and its size, so you can pick one without trying them all. - Works with the SecondWind health kit (formerly healthKit) out of the box: your health, shield, armour, hearts and lives, and the health of any enemy, NPC or prop.
- Not tied to health: it can show stamina, mana, energy, XP, a boss, or any number your game keeps. The
Resourcecomponent gives you a pool that drains and refills if you don't have one yet. - Feels good: the value eases to its new level, the lost chunk lingers as a ghost before draining, the bar pulses when low, numbers are optional, and each kind of resource has sensible colours.
- Fits any screen: the HUD scales with the screen and respects safe areas, so it stays readable on a phone as well as a desktop. Bars never leave the screen, and the gallery or a panel of yours and the HUD never cover each other.
- Leaves nothing behind:
BK.removetakes a bar down along with the entity it was made on, and closing or removing the gallery takes its screens with it. - Multiplayer: your HUD bars always show your body. Bars over other players, enemies and props read synced SecondWind health, so everyone sees the same value.
It depends on SecondWind 1.2.0 (the health kit formerly called healthKit) and, through it, Lifeline 1.0.1. Both install with it.
Install and use
Install the package into your world with zeromind_install (search ZeroMind for "barsKit"). SecondWind and Lifeline come with it.
1. Give your player health first
A health bar shows the health the health kit keeps, so the player's body needs health before any health, shield, armour, hearts or lives bar can show anything. Without it, the bar finds no health to read and is hidden entirely. You get no error, just no bar. The same goes for an enemy or prop you put a bar on.
The health kit installs with barsKit. It is SecondWind (it used to be called healthKit: same package, same guid), and it lands in deps/7a497fbf-1b4e-4655-8abf-e588fa7caa5c/SecondWind.package. A short name such as SecondWind.api or healthKit.api does not reach into deps/, so load it by its full name, as below.
Add these lines to your scene's build.luau, inside content(), after the default player setup (body is the player body it makes):
-- the health kit (SecondWind, formerly healthKit), by the name its install gives it
local VITALS = asset.resolve("deps.7a497fbf-1b4e-4655-8abf-e588fa7caa5c.SecondWind.Vitals", "component")
body.component.add(VITALS, { maxHealth = 100 }) -- every joining player gets health
-- or with a regenerating shield, the way this world's own scene does it:
-- body.component.add(VITALS, { model = "shield", maxHealth = 100, maxShield = 40 })
-- anything else that should have a health bar: here, an enemy to hit
local enemy = entity.spawnSynced("Grunt")
enemy.component.add("Asset", { source = asset.resolve("@builtin::avatars.humanoid", "avatar") })
enemy.localPosition = { 0, 0, 6 }
enemy.component.add(VITALS, { maxHealth = 250, displayName = "Grunt" })
Adding the Vitals component gives the entity SecondWind's health and Lifeline's Health with it. In build.luau, add the component as above rather than calling HK.attach: the scene rebuilds every time you edit the file, and a second HK.attach on the same body stops the rebuild with "already has a Vitals component". From game code that runs while you play, use the API by the same full name: local HK = require("deps.7a497fbf-1b4e-4655-8abf-e588fa7caa5c.SecondWind.api"), then HK.attach(entity, { ... }), HK.damage(...) and HK.heal(...). See the SecondWind README for health models (hitPoints, shield, armor, hearts, lives), death styles and teams.
To check it worked: BK.read("health", body).ok is true once the body has health, and false before.
If your game keeps its own health instead of using SecondWind, skip this step and point the bar at your number (show = "value" with bar:set, or BK.bind; see Pointing a bar at something). Stamina and mana bars need a Resource instead (see No stamina or mana yet?).
2. Put bars on screen
local BK = require("barsKit.api")
-- your health in the top-left corner
local hp = BK.bar({ look = "ghost", show = "health" })
-- a little bar over an enemy's head (the enemy needs health too, step 1)
BK.bar(enemy, { look = "overhead" })
-- a boss plate across the top
BK.bar({ look = "boss", target = "The Iron Warden", anchor = "topCenter", phases = "0.66 0.33" })
-- switch the look: one word, live
BK.setLook(hp, "pixel")
BK.setLookAll("hologram") -- every bar at once
BK.setLookAll("ornate", { placement = "screen" }) -- just the HUD
In a scene, you can add the component directly: e.component.add(asset.resolve("barsKit.Bar", "component"), { look = "orb", show = "mana", anchor = "bottomRight" }). Every field below is also settable in the editor's inspector.
Pointing a bar at something
show says what the bar shows, and target says whose it is. With no target, a screen bar reads the local player and a world bar reads the entity it sits on.
show | reads |
|---|---|
health (default) | SecondWind health (works on players, enemies and props) |
shield, armor, hearts, lives | the matching SecondWind value |
stamina, mana, xp, anything else | the Resource of that name on the target |
value | a number you set with bar:set(value, max) |
You can also bind any function, or point at any component field:
BK.bind(bar, function() return myGame.energy, myGame.maxEnergy end)
BK.bind(bar, { component = "Turret", field = "heat", max = 100 }) -- or maxField = "maxHeat"
BK.bar({ look = "segmented", show = "value", kind = "ammo" }):set(12, 30)
No stamina or mana yet? Make a resource
BK.resource(player, "stamina", { max = 100, regenRate = 25, regenDelay = 0.8, drainRate = 35, drainKey = "ShiftLeft" })
BK.resource(player, "mana", { max = 120, regenRate = 8 })
if BK.spend(player, "mana", 30) then castFireball() end -- false (and nothing spent) when there isn't enough
BK.gain(player, "mana", 10)
BK.bar({ look = "orb", show = "mana", anchor = "bottomRight" })
A Resource regenerates after regenDelay seconds without spending, and drains while drainKey is held or draining is true. demo = true makes it drain and refill on its own, for showcases. The owner of the entity is the authority; other peers' spend/gain calls are sent to it, and the value syncs to everyone.
Note: a second Resource on one entity is a named component instance. On player avatars, add it at runtime with BK.resource(...) (for example in localPlayerReady) rather than on the PlayerPrototype, because named second instances don't carry over when the prototype is cloned.
The looks
Where is screen (HUD only), world (over an entity only) or both. Size is the HUD size in points at scale 1, and the size class is in brackets: small, medium, wide or tall.
| id | name | shape | where | size | families | suits |
|---|---|---|---|---|---|---|
line | Thin line | line | both | 240×22 (medium) | minimal | health, stamina, xp, energy |
segmented | Segmented | segmented | both | 264×36 (medium) | realistic, sci-fi | health, armor, energy, ammo |
ring | Radial ring | ring | both | 88×88 (small) | sci-fi, minimal | health, shield, energy, cooldown |
hearts | Heart containers (full, half and empty; blue shield hearts) | hearts | both | 180×52 (medium) | fantasy, cozy, pixel | health, lives, hearts |
boss | Boss bar (name, phase marks, phase count) | bar | screen | 560×62 (wide) | fantasy, horror, realistic | boss |
overhead | Over-head bar | bar | world | 180×34 (medium) | minimal, realistic | overhead, enemy, npc |
ghost | Ghost bar (the lost chunk lingers, then drains) | bar | both | 280×30 (medium) | realistic, minimal | health, boss, shield |
stacked | Shield on health | stacked | both | 272×44 (medium) | sci-fi, realistic | health, shield, armor |
arc | Crosshair arc | arc | screen | 76×76 (small) | minimal, sci-fi | stamina, energy, ammo, crosshair |
orb | Glass orb | orb | screen | 108×126 (small) | fantasy, realistic | mana, health, energy |
gauge | Vertical gauge | vertical | screen | 58×176 (tall) | realistic, sci-fi | fuel, heat, oxygen, energy |
pips | Pips / dots | pips | both | 194×32 (medium) | minimal, cozy, cartoon | lives, charges, ammo, combo |
pixel | Pixel art | bar | both | 248×34 (medium) | pixel, cartoon | health, mana, stamina, xp |
hologram | Sci-fi hologram | bar | both | 300×52 (medium) | sci-fi | shield, energy, oxygen |
ornate | Ornate fantasy frame | bar | both | 312×50 (medium) | fantasy | health, mana, boss |
bubbly | Bubbly cartoon | bar | both | 272×46 (medium) | cartoon, cozy | health, xp, stamina |
blood | Blood vial (drips, flickers near death) | bar | both | 270×48 (medium) | horror | health, sanity, boss |
monitor | Heart monitor (ECG that flatlines at zero) | trace | screen | 214×62 (medium) | realistic, horror, sci-fi | health, stress |
stitched | Stitched felt | bar | both | 262×44 (medium) | cozy, cartoon | health, hunger, warmth |
megabar | Tick column (retro platformer meter) | vertical | screen | 30×150 (tall) | pixel, cartoon | health, energy, weapon, boss |
The eleven shapes are line, bar, segmented, stacked, ring, arc, hearts, pips, orb, vertical and trace.
Lifeline's own styles are there too, each with the same labels: lifeline:classic, segmented, ring, hearts, boss, overhead, minimal, stacked, scifi, fantasy, cartoon, pixel, orb.
Asking the kit
BK.looks() -- every look: { id, name, families, suits, description,
-- shape, placement, size = { w, h }, worldSize = { w, h }, sizeClass }
BK.looks({ family = "horror" }) -- blood, monitor, boss
BK.looks({ suits = "mana" }) -- orb, pixel, ornate
BK.looks({ family = "cozy", suits = "health" })
BK.looks({ placement = "world" }) -- looks that can float over an entity ("both" counts)
BK.looks({ size = "small" }) -- ring, arc, orb (or "medium", "wide", "tall")
BK.looks({ shape = { "ring", "arc" } }) -- one shape or several
BK.looks({ family = "pixel", shape = "bar" }) -- filters combine
BK.looks({ includeLifeline = true })
BK.families() -- { { id, name, looks } } for the 8 families
BK.shapes() -- { { id, name, looks } } for the 11 shapes
BK.suits() -- everything looks say they suit
BK.render("orb", { frac = 0.4, kind = "mana" }) -- draw a look inside your own UI tree
Bar fields
| field | default | what it does |
|---|---|---|
look | ghost | Which look. Change it any time. |
show | health | What it shows (see above). |
target | — | Whose value: an entity name or id. Empty means the local player (screen) or this entity (world). |
placement | screen | screen = HUD; world = floats over the target. |
anchor | topLeft | Screen corner or edge: topLeft, topCenter, topRight, left, center, right, bottomLeft, bottomCenter, bottomRight, crosshair. Bars on one anchor stack by order. |
label | — | Caption. Defaults to the resource's name (or the boss's name). |
kind / color | auto | Colour by resource kind (health, shield, armor, stamina, mana, energy, xp, boss, rage, oxygen, heat, ammo, lives, hunger, generic), or a hex override. |
numbers | auto | value (75 / 100), percent, none or auto. |
lowWarning | 0.25 | Below this fraction the bar pulses. 0 turns it off. |
size | 1 | Scale on top of the screen-size scale. |
phases | — | Boss phase marks as fractions, e.g. "0.66 0.33". |
segments | auto | Cell, pip or heart count, where the look uses one. |
worldHeight, range | 2.2, 30 | World bars: height above the target, and how far from the camera they still show. |
hideWhenFull, hideOwn | false, true | Hide while full; don't draw a world bar over your own body. |
offsetX, offsetY, visible | Fine placement and on/off. |
Methods on a Bar (call them with a colon): bar:setLook(id), bar:cycleLook(dir), bar:bind(fn | spec), bar:set(value, max), bar:sample(). On a Resource: res:spend(n), res:gain(n), res:set(v), res:fraction(). The API mirrors them: BK.setLook, BK.bind, BK.sample, BK.read(show, target), BK.bars(), BK.barOf(entity), BK.localPlayer().
Multiplayer
- Bar settings are local (not synced). Each peer runs its own HUD, so your screen bars always read your avatar.
- World bars and boss bars read SecondWind's synced state, so every player sees the same value over the same enemy.
Resourcevalues are owned by the entity's owner and synced.BK.spend/BK.gainfrom another peer are sent to the owner.- Damage and healing go through SecondWind (
HK.damage,HK.heal), which already routes them to the authority.
The gallery (barsKit.Showcase)
Add the Showcase component to any entity to get the gallery:
- Every look side by side, all following one value that drains and refills. Each card is labelled with the look's name, its id and families, and a gold line giving its shape, where it goes and its size.
- Filter chips for families, recolour chips for resource kinds, a numbers toggle, and Lifeline's looks.
- A switcher that flips your own health bar through every look. Use
[/]or the ◀ ▶ buttons;Ghides or shows the gallery.
The gallery and the HUD never cover each other, at any screen size:
- The panel takes the largest part of the screen the HUD leaves free (
BK.freeRect), clear of every screen bar and the touch buttons. - While the gallery is open the switcher sits inside it. When it is closed, the switcher stands alone and screen bars step clear of it.
- Bars that would land under the panel (the crosshair arc, world bars) are hidden until it closes.
- Cards are dealt into even rows. Rows differ by at most one card, and every row is stretched to fill the width, so there are no empty spots.
On a desktop-sized window, every look and its label is on screen at once, with no scrolling. The gallery tries every column count from 3 to 8, splits the panel's height evenly between the rows, and picks the grid that draws the looks largest without shrinking any one of them too far (BK.layout.fitGrid). A 1080p window comes out as 6 columns by 4 rows, and a shorter 1280×682 window as 7 by 3. The cards get compact there: the family line shortens or drops families ("+2") instead of being cut off. A panel too small to fit every card at a readable size scrolls instead, with the larger cards. That covers a phone in either orientation, where the touch buttons and the boss plate leave the gallery a small panel. On a phone the cards reflow to two columns, and a card's labels shorten rather than get cut off.
local g = entity.find("BarsGallery").component.get(asset.resolve("barsKit.Showcase", "component"))
g:gridInfo() -- the grid it drew last: { fits, scrolls, cols, rows, cellH, gridH, minScale, cards, panel }
g:layoutFor(1920, 1080) -- lay it out for another screen size without drawing: info, widget tree
G toggles the gallery, as does setting the component's open field. Removing the component or despawning its entity takes down both of its screens.
Phones and touch screens
- Bars scale with the screen but never drop below 0.85 on a phone, so text stays legible.
- Corner bars are laid out first. A centred bar (a boss bar) that meets one first slides sideways, and only then steps a row down (or up, for a bottom bar), so nothing overlaps on a narrow screen.
- A bar never leaves the screen. On a tiny screen with no clear row left, it shrinks into the widest gap of a row it tried.
- While the player is on touch, screen bars step clear of the on-screen buttons (Jump, Sprint, …). Top stacks move down past them and bottom stacks move up.
- World bars (over heads) draw under screen bars, with the nearest on top.
Your own panels and the HUD
All of these use UI points, the same space as ui.screenSize(). Pass nil as the rect to clear a key.
-- keep screen bars out of a rect (a minimap, a chat box): the bars move
BK.keepOut("minimap", { x = 1100, y = 10, w = 170, h = 170 })
-- a panel of yours covers a rect (an inventory): bars that would land under it are hidden until you clear it
BK.cover("inventory", rect)
-- or let the HUD tell you where there is room: the largest rect clear of every bar, touch button and keepOut
local sz = ui.screenSize()
local rect = BK.freeRect(sz.width, sz.height, { pad = 8, margin = 6, minW = 300, minH = 200 })
BK.screenRects() -- where the HUD drew its screen bars last frame: { { x, y, w, h, anchor, look, hidden } }
BK.layout -- the pure helpers the gallery uses: largestFree, evenRows, rowWidths, packLines, fitGrid, overlaps
Removing bars
local hp = BK.bar({ look = "ghost", show = "health" }) -- makes an entity for the bar
BK.remove(hp) -- removes the bar and despawns that entity; true when it took a bar down
BK.remove(hp) -- false: the bar is already gone (calling it again is harmless)
local tag = BK.bar(enemy, { look = "overhead" })
BK.remove(tag) -- removes only the Bar component; the enemy stays
BK.unbind(bar) -- undo BK.bind: the bar goes back to its `show` field
BK.isBound(bar) -- is a function or field spec bound right now?
Removing a Bar component by hand, despawning its entity or removing a Resource cleans up the same way. No HUD entries, bindings, covers, keepOuts or screens stay behind; the tests check this.
Known limits
Checked on 2026-10-18 in the desktop app: a fresh install into a brand-new, empty world, and two people in Gaugeworks at once. barsKit loaded in full and every bar in this README worked there. Each limit below happened.
- #19201: a player's name tag reads "{PlayerName}". Some avatars show the literal text
{PlayerName}over their heads instead of a name. This is the engine's own name tag, not a barsKit bar. barsKit's gallery panel is opaque, so the tag never shows through a gallery label. - #19260: older copies of dependencies sit in
versions/folders. A fresh install leaves single-module copies of two of the kit's dependencies beside the full packages. The health kit's own folder (deps/7a497fbf-1b4e-4655-8abf-e588fa7caa5c/SecondWind.package) holds no old copies.deps/45f92606-2e79-4b2b-8e03-b1fa2649e5c2/versions/<hash>/Lifeline.package/holds older copies of the Lifeline health package'sapimodule and itsHealthcomponent.deps/40bc9b7f-d32c-427f-b0ac-22d25071e2fa/versions/<hash>/MatchFlow.package/holds an older copy of MatchFlow'srulesmodule.- The warning in the log comes from the copied Lifeline
Healthcomponent, which can't find itsapimodule: "component source failed to load …deps.45f92606-…versions.<hash>.Lifeline.Health… module~.apiin the calling script has no root to resolve against". - barsKit uses the full
Lifeline.packageandMatchFlow.packageand never these copies. Bars, the gallery andBarandShowcasecomponents saved in a scene all load and survive a world reload. Nothing needs deleting.
- #19449: a second
Resourceon one entity reaches other players mixed into the first. The engine syncs one instance of a component per entity. If a body has both a stamina and a manaResource, the other players' copy of that body has oneResource: it keeps the first pool's name and max, but its value follows the second pool. In the Gaugeworks test, another player read your "stamina" as 120 out of 100 while your mana was full. Your own bars are right, because they read your own copy. Bars that show another player's second pool are not. Until #19449 is fixed, keep pools that other players need to see on separate entities (for example a child entity per pool), or show only one synced pool per body. - #19169: a second session that joins a game someone is already playing doesn't fully get its player. The first player in a game is not affected. Seen twice in Gaugeworks on 2026-10-18:
- Web browser version: the joining player got no player view (the camera stayed on the spawn and they couldn't move) and none of the world's screens, so no barsKit bars, gallery or switcher. barsKit's code did run in that session: its health took the demo's hits and its bars reached the other player. The engine just never finished handing that player their body. The browser console showed only the engine refusing to spawn the world's shared entities again (the dummy, the pot, the boss and the crate), nothing from barsKit.
- Desktop app: the joining player got their HUD and saw the right health, boss and dummy values. But bars over the bodies (barsKit's and the health kit's own) were drawn at ankle height instead of over the heads.
- Web browser version, one player: a fresh install of 1.3.0 works. All of these work: the gallery with every card, your health bar with numbers, a boss bar, an over-head bar,
Gand[/]. #19201 shows there too. Two people in the web version can't be checked until #19169 is fixed.
Version
1.3.0. require("barsKit.api").VERSION. Tests: barsKit.testSuite, 68 tests (run with the tests toolbox).
-
1.3.0: the release where barsKit behaves differently: it runs on SecondWind 1.2.0 (formerly healthKit) and draws a backing plate behind numbers over bars. Both came in 1.2.1; this version number says so. No call or setting changed. Known limits now name the platform they were checked on (the desktop app; the 1.2.1 notes wrongly said the web browser version) and add #19449: a second
Resourceon one entity reaches other players mixed into the first. A new test checks that the version agrees inpackage.yaml,BK.VERSION, the README's title and its Version line. Heart containers now draw real heart shapes at 180×52, with all five hearts inside the card, even in a session where Lifeline's heart renderer has not loaded yet. It used to fall back to 100×46 solid red squares with the fifth one cut off, seen in the phone gallery. barsKit now keeps trying to load Lifeline's renderer and switches to it once it loads.BK.removenow returns false for a bar that is already gone, as documented; it used to return true again. Known limits describe what #19260 really leaves on a fresh install: old LifelineapiandHealthcopies and an old MatchFlowrulescopy. -
1.2.1: tidy-up release; no call or setting changed. barsKit now uses the health kit by the name a fresh install gives it: SecondWind 1.2.0 (formerly healthKit, same package and guid), loaded by its full name. Before this, barsKit and its README's first example failed to load in a new world. The first example now works pasted as-is into a new world with only barsKit installed, and keeps working when you edit the scene again. The gallery's sample value and screens run only while the gallery is placed on an entity and the game is playing, not when its script loads. The gallery panel and switcher are opaque, so nothing in the world shows through their labels. A number drawn on top of a bar (over-head, ghost, pixel art, ornate, bubbly) sits on a backing plate, so the pale ghost trail can't hide it. The heart monitor's readout lines no longer touch. Gallery components get
sample()andisRunning(). Known limits now list only what still happens on a fresh install. -
1.2.0: every look is on screen at once on desktop-sized windows, labelled and without scrolling: the gallery picks its column count and card size to fit the panel (
BK.layout.fitGrid,BK.layout.packLines), and addsgridInfo()andlayoutFor(). Phones scroll as before. The shared value, boss bar, overhead dummy, ghost bar and look switcher are unchanged. The README now says to give your player health first, and how, and lists the known limits. -
1.1.0: every look says its shape, where it goes (screen, world or both) and its size, and
BK.looksfilters on all three (BK.shapes()lists the shapes). The gallery places itself in the room the HUD leaves free and hides bars under it, keeps the switcher inside it while open, and deals cards into even rows. A centred bar slides sideways before stepping down and never leaves the screen, and world bars draw under screen bars. New:BK.remove,BK.unbind,BK.isBound,BK.cover,BK.screenRects,BK.freeRectandBK.layout. Removing a bar, unbinding or closing the gallery leaves nothing behind. -
1.0.2: lighter gallery: virtual scrolling (only the rows in view are laid out and painted), the look switcher is redrawn only when it changes, and the gallery rebuilds 20 times a second on phones (30 on desktop). On a phone the open gallery now costs about 3ms a frame instead of about 9.5ms.
-
1.0.1: phone layout: a legible minimum scale, no overlap between corner and centre bars, bars and the gallery keep clear of touch buttons, and
BK.keepOut. -
1.0.0: first release.
Scoped to this part · feeds back into the world's score.