Spiral Knight's health system

I was curious on how I would make a health system similar to Spiral Knights

image

when the player takes damage, it transitions to a Half State or an Empty State depending on how big the damage the player has taken. So let’s say the player takes 1 damage point, then an entire pip (starting from the right) will be in a complete empty State. If the player takes a damage in form of a decimal,such as .5 then it’ll become a half State, if the player takes the same damage point of .5, then the pip in the half state will become empty, but if the pip is in a half state and the player takes 1 damage, then i’ll make the half state pip empty and make the pip before it half state. The Pips are the health bars you see in the image above

I’ve tried making this myself, but it didn’t really work out, sometimes the pips won’t even be cloned to the player health, and most of the time, when the player takes damage, the pips just vanish

The ClipsDescendants property can be used to create a sort of mask to hide parts of UI. Adjust a UI element with this property with another layer underneath to simulate a health bar this way.

I’d start by implementing with a pip class for each individual pip and a health bar class which handles the state of the pips when given the current health data.

Something like this:

local Pip = {}
Pip.__index == Pip

function Pip.new()
    # Create a new pip object with frames, images etc...
end

function Pip:SetState(newState: "Full" | "Half" | "Empty")
    # Implement the visual changes to the individual pip based on the state
end


local HealthBar = {}
HealthBar.__index = HealthBar

function HealthBar.new(maxHealth: number, currentHealth: number?)
    
    local self = {}
    self.MaxHealth = maxHealth
    # Implement / reference visual gui stuff
    
    # Assuming maxHealth (number of pips) is a whole number
    assert(math.ceil(maxHealth) == math.floor(maxHealth))
    #  - 0.5 damage takes a pip from full to half state
    #  - 1 damage takes a pip from full to empty
    
    self.Pips = {}
    for i = 1, maxHealth do
        local newPip = Pip.new()
        self.Pips[i] = newPip
    end
    
    setmetatable(self, HealthBar)
    
    self:SetCurrentHealth(currentHealth or maxHealth)

    return self
end

function HealthBar:SetCurrentHealth(health: number)
    local fullEnd = math.floor(health) # The index of the last full pip
    local isHalfFull = (health % 1 == 0.5) # true if there needs to be a half full pip
    local emptyStart = isHalfFull and fullEnd + 2 or fullEnd + 1 # Index of the first empty pip

    for i = 1, fullEnd do
        self.Pips[i]:SetState("Full")
    end
    
    if isHalfFull then
        self.Pips[fullEnd + 1]:SetState("Half")
    end

    for i = emptyStart, self.MaxHealth do
        self.Pips[i]:SetState("Empty")
    end

end

I see… What I did wasn’t that far off from what you did (I think). thanks for the help!

if I may ask, It doesn’t seem to be cloning any “Pip” based on the character/Player’s maxHealth, I edited it but it still doesn’t seem to be doing anything?

local Pip = {}
local PipTemplate = script.Parent.Pip
local maxHealth = game:GetService("Players").LocalPlayer.Character:WaitForChild("Humanoid").MaxHealth
local currentHealth = game:GetService("Players").LocalPlayer.Character:WaitForChild("Humanoid").Health
Pip.__index = Pip

function Pip.new()
	local pip = PipTemplate:Clone()
	pip.Parent = script.Parent
	return setmetatable(pip, Pip)
end

function Pip:SetState(newState: "Full" | "Half" | "Empty")
	if newState == "Full" then
		self.Image = "rbxassetid://12404917036"
	end
	if newState == "Half" then
		self.Image = "rbxassetid://12409136414"
	end
	if newState == "Empty" then
		self.Image = "rbxassetid://12409173552"
	end
end

local HealthBar = {}
HealthBar.__index = HealthBar

function HealthBar.new()
	local self = {}
	self.MaxHealth = maxHealth

	assert(math.ceil(maxHealth) == math.floor(maxHealth))

	self.Pips = {}
	for i = 1, maxHealth do
		self.Pips[i] = Pip.new()
	end

	setmetatable(self, HealthBar)

	self:SetCurrentHealth(currentHealth)

	return self
end

function HealthBar:SetCurrentHealth(health: number)
	local fullEnd = math.floor(health)
	local isHalfFull = (health % 1 == 0.5)
	local emptyStart = isHalfFull and fullEnd + 2 or fullEnd + 1

	for i = 1, fullEnd do
		self.Pips[i]:SetState("Full")
	end

	if isHalfFull then
		self.Pips[fullEnd + 1]:SetState("Half")
	end

	for i = emptyStart, self.MaxHealth do
		self.Pips[i]:SetState("Empty")
	end
end

Okay I think the issues are:

  1. the Pip constructor trying to overwrite the metatable of a roblox class which you can’t do
  2. you need to create an instance of the HealthBar class
  3. connect health changed event to update the health bar instance

For (1):

function Pip.new()
	local self = {}
	local imageLabel = PipTemplate:Clone()
	imageLabel .Parent = script.Parent 
	self.ImageLabel = imageLabel
	return setmetatable(self, Pip)
end

function Pip:SetState(newState: "Full" | "Half" | "Empty")
	if newState == "Full" then
		# Change .Label to .ImageLabel.Label 
		self.ImageLabel.Image = "rbxassetid://12404917036"
	end
	...
end

For (2) at the bottom of your code or in another module / script:


local healthBar = HealthBar.new(humanoid.MaxHealth, humanoid.Health)

humanoid.HealthChanged:Connect(function(health)
    healthBar:SetCurrentHealth(health)
end)

Edit: I think more needs doing when cloning the pips too in order to put them in the correct positions. Depends if you have a UIListLayout or something going on already though.

Sorry for the late reply, thanks again! I’ll let you know if anything.

what about sizing? ClipDescendants still allow the children to be sized along with the parent

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.