A HUD or Heads-Up Display is a set of UI elements that are always visible or accessible during gameplay, such as score displays, health meters, and menu buttons. Including a HUD is essential for most experiences because it displays information that helps players to be successful in their gameplay objectives.
A common HUD element is a health meter with an icon on the left which can be adapted into a timer bar, progress bar, or similar.
Using Hazardous Space Station as a starting place and UI Fundamentals - HUD Meter as a finished reference place, this tutorial demonstrates:
- Setup and use of the Device Emulator to test your design on multiple emulated screens.
- Usage of StarterGui as both a design and storage container.
- How to position/size UI elements around built‑in Roblox controls and device notches/islands, such as the camera notch on modern phones.
- How to replace the default Roblox health meter with your own meter and hook it up to the character's health level.
- How to animate the center portion of the health meter and set its color between five color gradient keypoints (red, orange, yellow, lime, green).
Using the Device Emulator
Roblox is inherently cross-platform, as players can discover and join experiences on a PC or console, then later pick up their phone and continue where they left off. Mobile devices (phones and tablets) have the least amount of screen space, so it's important that your UI elements fit on smaller screens and that they're clearly visible to players.
The best way to test UI designs across platforms is Studio's Device Emulator. This tool provides a preset selection of devices and allows you to add your own custom presets.
Open the Hazardous Space Station template in Studio.
From the Test tab, toggle on the Device tool.
From the bar directly above the main viewport, select a phone emulation such as iPhone X or Samsung Galaxy A51. Then, set the view size to Fit to Window to utilize the maximum space in Studio.
Create a Screen Container
A ScreenGui container holds UI objects (GuiObjects) to display on a player's screen (in this tutorial, the entirety of the health meter). To display a ScreenGui and its child objects to every player who joins the experience, place it inside the StarterGui container. When a player joins and their character first spawns, the ScreenGui and its contents clone into the PlayerGui container for that player, located within the Players container.
To insert an empty ScreenGui:
In the Explorer window, locate the StarterGui container.
Hover over the container, click the ⊕ button, and insert a ScreenGui.
Rename the new container HUDContainer to reflect its purpose.
Utilize Safe Areas
Modern phones take advantage of the entire screen but typically include notches, cutouts, and other elements that occupy screen space. Every Roblox experience also includes the top bar controls for quick access to the main menu, chat, leaderboard, and more.
To ensure players can see and access all UI without obstruction, Roblox provides the ScreenInsets property which controls the safe area insets for the contents of a ScreenGui. Every UI object that you position inside a ScreenGui is relative to the inset bounds.
While the default of CoreUISafeInsets ensures all UI objects remain clear of Roblox UI and device cutouts, DeviceSafeInsets can be a better option to utilize limited screen space, as illustrated below.
In the Explorer window, select HUDContainer.
In the Properties window, set the ScreenInsets property to DeviceSafeInsets.
Set Edge Padding
With ScreenInsets set to DeviceSafeInsets, content can now extend directly up to the physical top edge of the screen. However, a small amount of padding can help push the health meter (and other objects inside the container) slightly away from the screen edges for a cleaner appearance and to prevent them from being clipped.
One way to apply padding to a UI container is through the insertion of a UIPadding modifier:
Insert a UIPadding modifier into HUDContainer.
With the new UIPadding object selected, enter a value of 0, 16 for all edges of the container (PaddingBottom, PaddingLeft, PaddingRight, PaddingTop). This applies padding of 16 pixels all around the container, regardless of the screen's resolution.
Construct the Health Meter
With the screen container configured, you can begin constructing the health meter using Roblox UI objects such as frames and an image label.
Create the Parent Frame
Similar to design applications like Figma and Photoshop, a Frame in Roblox serves as a container for other UI objects. For this tutorial, the entire health meter will be contained in a single parent frame, making it simple to reposition across various HUD layouts.
Insert a Frame into HUDContainer. The new frame appears in the upper-left corner as an empty white square.
Rename the new frame instance to MeterBar.
Position the Frame
In Roblox, the position of a UI object is represented by a UDim2 coordinate set containing Scale and Offset values for both the X and Y axes:
To position a UI object in the upper-right corner of the screen container, Scale is the best approach because an X value of 1 (100%) represents the right edge of the container, regardless of the screen's physical pixel size. Similarly, a Y scale value of 0 (0%) represents the top edge of the container.
Additionally, you'll need to set an upper-right anchor point for the parent frame to define its origin point. Acceptable values are between 0 and 1, relative to the size of the object, so an anchor value of 1, 0 puts the frame's anchor point in its upper‑right corner.
In the Explorer window, select the MeterBar frame that you inserted previously.
Enter 1, 0 for the AnchorPoint property. The frame should now be positioned in the upper‑right corner of the device safe area, slightly indented from the edge as a result of the padding.
Resize the Frame
Like position, the Size of a UI object is represented by a UDim2 coordinate set containing Scale and Offset values for both the X and Y axes.
By default, the new frame's size is {0, 100},{0, 100}, meaning 100 pixels in both width (X) and height (Y). While a strict pixel value is useful in certain cases, many UI elements scale more responsively across multiple screens when set to a percentage of the overall screen container size.
With the MeterBar frame selected, access the Properties window and navigate to the Size property.
Enter a value of 0.35, 0, 0.05, 0 to set a percentage size of 35% wide and 5% tall with no added pixel offsets.
Style the Frame
By default, Frames are filled in solid white. The final health meter should have a darker and slightly opaque fill, as well as a dark outline, so that it stands out better on both light and dark backgrounds.
With the MeterBar frame selected, enter 0 for the BackgroundColor3 property. Studio will automatically convert it to the RGB value of [0, 0, 0].
Enter 0.75 for the BackgroundTransparency property. In Roblox, transparency ranges from 0 for fully opaque to 1 for fully transparent, so 0.75 equals 25% opacity in other applications such as Figma or Photoshop.
Insert a UIStroke object, a powerful UI modifier that adds a customizable stroke to the frame.
With the new UIStroke selected, set the following properties:
- Thickness = 3
- Transparency = 0.25 (75% opaque)
To finalize the meter frame's styling, you can round the corners to form a "pill" shape instead of a sharp rectangle.
Insert a UICorner instance into the MeterBar frame.
With the new UICorner selected, set the CornerRadius to 0.5, 0. Using a scale value of 0.5 (50%) instead of a pixel value is especially convenient for the meter bar because it ensures a fully rounded curve no matter how tall or wide the container becomes.
Create the Inner Fill
Now that the health meter's containing frame is complete, you can add an inner fill portion to represent the character's variable health. Since it only needs to be a solid‑filled region, a sub‑child Frame inside the parent frame is suitable.
Insert a child Frame into the MeterBar frame.
Rename the new frame instance to InnerFill.
With InnerFill selected, set the following properties:
- AnchorPoint = 0, 0.5 (left edge and vertical center)
- Position = 0, 0, 0.5, 0
- Size = 1, 0, 1, 0
Since children of frames are positioned and sized relative to their parent, use of scale makes the inner frame fill the parent's full width and height, starting from the parent's left edge.
To match the "pill" shape of the parent frame, insert an additional UICorner into InnerFill.
With the new UICorner modifier selected, set its CornerRadius property to 0.5, 0 to match the "pill" shape of the parent MeterBar frame.
To better represent that a full meter indicates good health, select InnerFill and set its BackgroundColor3 property to 0, 225, 50 (in a later task, you'll script this color to change based on actual health).
Add the Icon
To more clearly indicate the purpose of the meter, you can add an image label to the left side, in this case a red heart which commonly symbolizes health or life.
Insert an ImageLabel into the MeterBar frame. This object lets you apply a 2D image asset that has been uploaded as a decal to Roblox.
Rename the new label instance to Icon.
With Icon selected, set its ZIndex property to 2. While newly inserted UI objects always layer in front of objects inserted previously, this change ensures the icon always displays in front of the meter's frame elements.
To ensure the icon ImageLabel always stays at a 1:1 aspect ratio, insert a UIAspectRatioConstraint. Although this constraint has customizable properties to control the aspect ratio, you can leave its default values intact.
With Icon selected, finalize the appearance and position by changing the following properties:
- AnchorPoint = 0.5, 0.5 (center anchor)
- BackgroundTransparency = 1 (100% transparent)
- Position = 0, 0, 0.5, 0 (left side of meter and vertical center)
- Size = 2, 0, 2, 0 (200% the overall size of the MeterBar frame, constrained to 1:1 by the UIAspectRatioConstraint)
Constrain the Size
While a scale height of 0.05 (5%) looks good on modern phone screens and gaming monitors which are 16:9 aspect ratio or wider, the meter may look slightly too tall on tablet screens and older phones. You can check this by emulating a tablet like iPad 7th Generation from the Device Emulator.
To keep the meter bar's height more consistent with wider screens, you can apply a UISizeConstraint to limit the maximum pixel height.
Insert a UISizeConstraint into the MeterBar frame.
With the new constraint selected, set its MaxSize property to inf, 20 to restrict its height to 20 pixels while enforcing no width restriction.
Now, the meter bar maintains a more consistent height between wider and taller screens.
Replace the Default Health Meter
Roblox experiences include a default health meter which becomes visible when characters take damage. If you keep the default meter visible, it will duplicate and potentially overlap the custom meter.
Disable the Default Meter
To disable the default health meter, you'll use a client script (LocalScript) within StarterPlayerScripts that calls StarterGui:SetCoreGuiEnabled().
In the Explorer window, expand the StarterPlayer container and locate the StarterPlayerScripts container within it.
Insert a new LocalScript into the container and rename it to HideDefaultHealthMeter to describe its purpose. Scripts within StarterPlayerScripts automatically run when the local player joins an experience, making it an ideal container to run a script that permanently hides the default meter.
When you insert a new script, it automatically opens in a new script editor tab (if it doesn't, double‑click the script in the Explorer window).
Paste the following code inside the HideDefaultHealthMeter script:
HideDefaultHealthMeterlocal StarterGui = game:GetService("StarterGui")-- Hide default health meterStarterGui:SetCoreGuiEnabled(Enum.CoreGuiType.Health, false)Code ExplanationLine Purpose 1 Gets a reference to a core service, StarterGui, which represents the same container where you created the custom health meter and whose contents clone into the PlayerGui container for each player that joins the experience. 4 Calls the service's SetCoreGuiEnabled() method and instructs the default health meter to be disabled (false).
If you playtest the experience now and take damage, you'll notice that the default meter is disabled and hidden (you'll script the custom meter to reflect health changes in the next section).
Listen for Health Changes
All default Roblox character models contain a Humanoid class which provides special behaviors and functionality to the character, such as setting its walk/run speed and managing its health. Health changes on the server replicate to each player's client and you can detect these changes to update both the size and color of the custom health meter.
In the Explorer window, locate the StarterCharacterScripts container within StarterPlayer.
Insert a new LocalScript into the container and rename it to UpdateCustomMeter to describe its purpose. Scripts within StarterCharacterScripts automatically run each time the player's character spawns, making it an ideal container to run a script that fully resets the health meter on each respawn.
In the editor window for the UpdateCustomMeter script, paste the following code:
UpdateCustomMeterlocal Players = game:GetService("Players")-- Reference to local player, character, and humanoidlocal player = Players.LocalPlayerlocal character = player.Characterlocal humanoid = character:WaitForChild("Humanoid")-- Reference to meter bar inner framelocal playerGui = player:WaitForChild("PlayerGui")local meterBarInner = playerGui.HUDContainer.MeterBar.InnerFill-- Gradient sequence colors (red, orange, yellow, lime, green)local gradient = {Color3.fromRGB(225, 50, 0),Color3.fromRGB(255, 100, 0),Color3.fromRGB(255, 200, 0),Color3.fromRGB(150, 225, 0),Color3.fromRGB(0, 225, 50)}-- Function to get color in gradient sequence from fractional pointlocal function getColorFromSequence(fraction: number): Color3-- Each color in gradient defines the beginning and/or end of a sectionlocal numSections = #gradient - 1-- Each section represents a portion of 1local sectionSize = 1 / numSections-- Determine which section the requested fraction falls intolocal sectionStartIndex = 1 + math.clamp(fraction, 0, 1) // sectionSize-- Get the colors at the start and end of the sectionlocal sectionColorStart = gradient[sectionStartIndex]local sectionColorEnd = gradient[sectionStartIndex + 1] or sectionColorStart-- Normalize fraction to be a number from 0 to 1 within the sectionlocal fractionOfSection = math.clamp(fraction, 0, 1) % sectionSize / sectionSize-- Lerp between beginning and end based on the normalized fractionreturn sectionColorStart:Lerp(sectionColorEnd, fractionOfSection)endlocal function onHealthChanged()-- Calculate new health as percentage of maxlocal healthFraction = math.max(0, humanoid.Health / humanoid.MaxHealth)-- Set the bar to new size/color targetsmeterBarInner.Size = UDim2.new(healthFraction, 0, 1, 0)meterBarInner.BackgroundColor3 = getColorFromSequence(healthFraction)end-- Listen for changes to humanoid healthhumanoid.HealthChanged:Connect(onHealthChanged)-- Initially set (or reset) bar size/color to current healthonHealthChanged()Code ExplanationLines Purpose 4‑6 Gets references to the local Player, their Character model, and the Humanoid class within it. 9‑10 Gets a reference to the meter's InnerFill object which must be resized and recolored as the character's health changes. 13‑19 Declares an array of five colors (red, orange, yellow, lime, green) to recolor the meter at various points; for example, green for 100% health, yellow for 50%, red for 0%, or a blend at any fraction between the keypoints. 22‑41 Helper function which returns the color blend between any of the gradient color keypoints. 43‑50 Function which handles any changes to health. Here, it calculates the new health as a percentage of the character's MaxHealth, resizes InnerFill to that scale percentage, and recolors it to the color returned from the getColorFromSequence() function. 53 The main event connection which detects Health changes replicated from the server and calls the onHealthChanged() function. 56 Initially (upon character spawn or respawn) calls the onHealthChanged() function to size and color InnerFill to the correct percentage. Typically this will be the full width and green.
If you playtest the experience now, you'll notice that the custom meter correctly updates both size and color as the character takes damage:
Animate the Meter Bar
To add an extra level of polish to the custom meter, you can animate health changes through tweening, gradually changing the meter bar's size and color over ½ second.
Access the script editor tab for the UpdateCustomMeter script that you edited previously.
Select all lines (CtrlA or ⌘A) and then paste over them (CtrlV or ⌘V) with the following code:
UpdateCustomMeterlocal Players = game:GetService("Players")local TweenService = game:GetService("TweenService")-- Reference to local player, character, and humanoidlocal player = Players.LocalPlayerlocal character = player.Characterlocal humanoid = character:WaitForChild("Humanoid")-- Tween propertieslocal tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)-- Reference to meter bar inner framelocal playerGui = player:WaitForChild("PlayerGui")local meterBarInner = playerGui.HUDContainer.MeterBar.InnerFill-- Gradient sequence colors (red, orange, yellow, lime, green)local gradient = {Color3.fromRGB(225, 50, 0),Color3.fromRGB(255, 100, 0),Color3.fromRGB(255, 200, 0),Color3.fromRGB(150, 225, 0),Color3.fromRGB(0, 225, 50)}-- Function to get color in gradient sequence from fractional pointlocal function getColorFromSequence(fraction: number): Color3-- Each color in gradient defines the beginning and/or end of a sectionlocal numSections = #gradient - 1-- Each section represents a portion of 1local sectionSize = 1 / numSections-- Determine which section the requested fraction falls intolocal sectionStartIndex = 1 + math.clamp(fraction, 0, 1) // sectionSize-- Get the colors at the start and end of the sectionlocal sectionColorStart = gradient[sectionStartIndex]local sectionColorEnd = gradient[sectionStartIndex + 1] or sectionColorStart-- Normalize fraction to be a number from 0 to 1 within the sectionlocal fractionOfSection = math.clamp(fraction, 0, 1) % sectionSize / sectionSize-- Lerp between beginning and end based on the normalized fractionreturn sectionColorStart:Lerp(sectionColorEnd, fractionOfSection)endlocal function onHealthChanged()-- Calculate new health as percentage of maxlocal healthFraction = math.max(0, humanoid.Health / humanoid.MaxHealth)-- Tween the bar to new size/color targetslocal tweenGoal = {Size = UDim2.new(healthFraction, 0, 1, 0),BackgroundColor3 = getColorFromSequence(healthFraction)}local meterBarTween = TweenService:Create(meterBarInner, tweenInfo, tweenGoal)meterBarTween:Play()end-- Listen for changes to humanoid healthhumanoid.HealthChanged:Connect(onHealthChanged)-- Initially set (or reset) bar size/color to current healthonHealthChanged()Key Additions/ChangesLines Purpose 2 Gets a reference to TweenService to implement tweening functionality within the script. 10 Creates a TweenInfo constructor which defines the intended tween's duration, easing style, and easing direction. 52‑57 Instead of simply setting the bar's size and color as in the previous version, declares a tweenGoal table with the target size/color, creates a new tween using the tweenInfo and tweenGoal parameters, and plays the new tween.
If you playtest the experience now, you'll notice that the custom meter tweens between each change in health:
Add a Damage Effect
The default health meter system includes a brief, subtle red tint on the screen edges when the character is damaged. By disabling the default meter, this effect is removed, but you can replace it with your own implementation.
Access the script editor tab for the UpdateCustomMeter script that you edited previously.
Select all lines and paste over them with the following code:
UpdateCustomMeterlocal Workspace = game:GetService("Workspace")local Players = game:GetService("Players")local TweenService = game:GetService("TweenService")-- Reference to local player, character, and humanoidlocal player = Players.LocalPlayerlocal character = player.Characterlocal humanoid = character:WaitForChild("Humanoid")-- Tween propertieslocal tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Exponential, Enum.EasingDirection.Out)-- Variable to store/cache character healthlocal cachedHealth = humanoid.Health / humanoid.MaxHealth-- Get (or create new) color correction effect inside player cameralocal colorCorrection = Workspace.CurrentCamera:FindFirstChildWhichIsA("ColorCorrectionEffect") or Instance.new("ColorCorrectionEffect", Workspace.CurrentCamera)colorCorrection.Name = "DamageColorEffect"-- Reference to meter bar inner framelocal playerGui = player:WaitForChild("PlayerGui")local meterBarInner = playerGui.HUDContainer.MeterBar.InnerFill-- Gradient sequence colors (red, orange, yellow, lime, green)local gradient = {Color3.fromRGB(225, 50, 0),Color3.fromRGB(255, 100, 0),Color3.fromRGB(255, 200, 0),Color3.fromRGB(150, 225, 0),Color3.fromRGB(0, 225, 50)}-- Function to get color in gradient sequence from fractional pointlocal function getColorFromSequence(fraction: number): Color3-- Each color in gradient defines the beginning and/or end of a sectionlocal numSections = #gradient - 1-- Each section represents a portion of 1local sectionSize = 1 / numSections-- Determine which section the requested fraction falls intolocal sectionStartIndex = 1 + math.clamp(fraction, 0, 1) // sectionSize-- Get the colors at the start and end of the sectionlocal sectionColorStart = gradient[sectionStartIndex]local sectionColorEnd = gradient[sectionStartIndex + 1] or sectionColorStart-- Normalize fraction to be a number from 0 to 1 within the sectionlocal fractionOfSection = math.clamp(fraction, 0, 1) % sectionSize / sectionSize-- Lerp between beginning and end based on the normalized fractionreturn sectionColorStart:Lerp(sectionColorEnd, fractionOfSection)endlocal function onHealthChanged()-- Calculate new health as percentage of maxlocal healthFraction = math.max(0, humanoid.Health / humanoid.MaxHealth)-- Tween the bar to new size/color targetslocal tweenGoal = {Size = UDim2.new(healthFraction, 0, 1, 0),BackgroundColor3 = getColorFromSequence(healthFraction)}local meterBarTween = TweenService:Create(meterBarInner, tweenInfo, tweenGoal)meterBarTween:Play()-- Show damage effect if new health is lower than cached healthif healthFraction < cachedHealth then-- Cache new health valuecachedHealth = healthFraction-- Set color correction to red as the initial tint before tweeningcolorCorrection.TintColor = Color3.fromRGB(255, 25, 25)colorCorrection.Saturation = 2.5-- Tween the tint back to white (neutral and no tint change from normal)local colorCorrectionTweenGoal = {TintColor = Color3.fromRGB(255, 255, 255),Saturation = 0}local colorCorrectionTween = TweenService:Create(colorCorrection, tweenInfo, colorCorrectionTweenGoal)colorCorrectionTween:Play()endend-- Listen for changes to humanoid healthhumanoid.HealthChanged:Connect(onHealthChanged)-- Initially set (or reset) bar size/color to current healthonHealthChanged()Key Additions/ChangesLines Purpose 14 Sets a placeholder reference (cachedHealth) to track the character's health amount between changes, so you can compare if a change is lower (damage). 17‑18 On the initial character spawn, creates a new ColorCorrectionEffect inside the player's current Camera, or gets a reference to the same instance on later respawns. By parenting this post‑processing effect to the player's camera, it only applies to their local screen, not to every player's screen on the server. 68‑83 First performs a conditional check to confirm that the health change is lower than the cachedHealth value, indicating damage; if so, it sets cachedHealth to the new value. Next, it sets the ColorCorrectionEffect tint to [255, 25, 25] (red) with higher saturation, then tweens the tint back to the default of neutral white ([255, 255, 255]) with no saturation.
If you playtest the experience now, you'll notice that the screen briefly flashes red whenever the character takes damage: