openlang

One health bar went red and took the rest with it

Got bitten by shared resources in Godot UI this time. Every unit in my little prototype has a health bar scene, a ProgressBar with a StyleBoxFlat override for the fill. When health drops under 25 I wanted that one bar to go red, so I wrote this.

func _on_health_changed(value: float) -> void:
	$Bar.value = value
	var fill := $Bar.get_theme_stylebox("fill") as StyleBoxFlat
	fill.bg_color = Color.RED if value < 25.0 else Color.GREEN

First unit takes a hit and every bar on screen turns red. The override was a sub-resource saved inside the bar scene, and every instance of the scene got the very same StyleBoxFlat object. Set a property on it and it's set for all of them. Obvious once you see it, it's one object.

What I'm unsure about is the fix. Call duplicate() in _ready, tick resource_local_to_scene, or stop writing to the stylebox at all?

The conversation continues 10

Follow the reasoning
  1. Permalink

    Two of those options keep the mutation and give each bar its own object to mutate. The third changes the operation. If the fill only ever has two looks, two stylebox resources that never change, assigned with add_theme_stylebox_override, mean no bar owns anything mutable and the sharing stops mattering.

    Duplicating per bar is the better fit when the colour is continuous, a blend from green to red, where presets would multiply without end. So which is it showing, a threshold or a gradient?

  2. Permalink

    The least code is a checkbox. Select the StyleBoxFlat in the inspector, open its Resource section and tick Local To Scene. Every instance of the bar scene then gets its own copy when it's instantiated, and your handler can stay as it is. I'd start there and only write something if it turns out not to be enough.

  3. Permalink

    Watch get_theme_stylebox("fill"). Remove the override one day and it returns the theme's stylebox, and the handler recolours every ProgressBar on that theme. A duplicate() before writing covers both cases, and a shallow copy is enough for a StyleBoxFlat.

  4. Permalink

    Before ticking it, it's worth checking where the stylebox actually lives, because Local To Scene acts on a resource the scene uses. Is it a built-in sub-resource of the bar scene, or a .tres file that the override points to? Either should get duplicated per instance, as far as I know. A stylebox that comes from a Theme on some parent node is a different object, though, and the checkbox on the bar's own override won't reach it. Once that's known, the checkbox is a safe first step.

  5. Permalink

    Built-in sub-resource, saved right inside health_bar.tscn, no Theme anywhere above it. Ticked Local To Scene and it works, the damaged unit's bar goes red on its own and the others stay green. Slightly annoyed that it's a checkbox and not the default, but I get it, most styleboxes are meant to be shared and copying all of them would be silly.

  6. Permalink

    Now I'm curious how many styleboxes exist. Print $Bar.get_theme_stylebox("fill").get_instance_id() from two bars in _ready. One number before the checkbox and two different ones after, if the copy happens where we think it does. Then try a bar added at run time with instantiate() instead of one placed in the editor, just to see whether it gets its own copy as well.

  7. Permalink

    Why touch the stylebox at all? self_modulate is per node. Set it to red on the one bar and nothing is shared. Two hundred bars means two hundred colour values on two hundred nodes and zero extra resources.

  8. Permalink

    Tried that first, it's how I ended up in the stylebox. self_modulate multiplies everything the bar draws, so the grey background turns dark red along with the fill and you can't read how much health is left anymore. Works for a quick flash on hit, not for a state that sticks around.

  9. Permalink

    Presets get my vote too, though for a reason other than the object count. The rule worth keeping is that each bar's colour depends on its own unit's health and on nothing else. A shared mutable stylebox breaks it by letting one unit's health decide another bar's colour. Duplicates restore it, provided every copy is actually made. Two presets restore it without depending on any copy, because the handler picks one from the current value and the presets themselves never change. I'd still test the value 25 itself, since < and <= disagree there.

  10. Permalink

    Printed the ids. Before the checkbox all six bars showed the same one, after it six different ones, and a bar added with instantiate() got its own too. So that worked as described.

    Then I switched to two preset styleboxes anyway, mostly because of tally6's point. The handler is down to this.

    const FILL_OK := preload("res://ui/fill_ok.tres")
    const FILL_LOW := preload("res://ui/fill_low.tres")
    
    func _on_health_changed(value: float) -> void:
    	$Bar.value = value
    	var fill: StyleBox = FILL_LOW if value < 25.0 else FILL_OK
    	$Bar.add_theme_stylebox_override("fill", fill)

    Local To Scene is unticked again, since nothing writes to a stylebox anymore. Still haven't decided whether exactly 25 counts as low.

Other sides of the same question

  1. Should a rejected tile move the palette forward?

    Creating a coroutine in Lua runs nothing yet. The host calls coroutine.resume, and only then does the generator run, up to th…

  2. Who owns a Vulkan buffer after submission?

    A C++ RAII wrapper around a Vulkan buffer records work that uses the buffer, submits it, and goes out of scope. Its destructo…

    Replies 5cppc
  3. A status region that reads Saved twice

    The screen reader said "Saved" twice after one save. The state behind it has a single transition into saved, and the reducer…