Quick answer: Negative scale anywhere in the parent chain mirrors the text. Reset scales to positive; use rotation for facing, not negative scale.
Floating damage numbers use Label3D with billboard enabled. When the enemy faces left (flipped via negative X scale), the numbers render mirrored too.
Cause: Inherited Negative Scale
Label3D billboards to face the camera, but the parent’s negative scale still applies to the text geometry. The character is flipped via scale.x = -1; the label inherits it.
Detach From Flipped Parent
# Don't parent the label under the flipped sprite
CharacterRoot (scale positive)
- Sprite3D (flip via rotation, not scale)
- DamageLabel (Label3D)
Keep the label outside the flipped subtree. Or flip the sprite by rotating 180° on Y instead of negative scale.
Counter-Scale
If you must keep it parented:
func _process(_d):
# cancel out parent's negative scale
label.scale.x = abs(label.scale.x) * sign(get_parent().scale.x)
Hacky but works if architecture demands it.
Billboard Modes
- Disabled: text uses its own transform.
- Enabled: always faces camera fully.
- Y-Billboard: rotates around Y only (good for trees/signs).
None of these fix mirrored geometry from scale — the fix is the scale itself.
Verifying
Enemy faces both directions; damage numbers always readable. No mirrored text.
“Negative scale mirrors everything below it. Flip sprites with rotation; keep labels out of the flipped subtree.”
As a rule: never use negative scale for character flipping in 3D. Rotation is cleaner and doesn’t poison child transforms.