How to Build a Godot Fighting Game Camera and HUD

Build a Godot fighting game camera that follows both players, then connect the HUD and screen shake to damage events. You will work through camera positioning and on-screen feedback together, guided by Zenva’s coding and game-development teaching experience across a catalog of over 400 courses.

This tutorial assumes you have the supplied fighting-game project open in Godot, including its fighters, imported UI font, health system, and GlobalEvents autoload.

Download Project Files

The files and full project are available through the course included in the Intermediate Godot Mini-Degree if you want the complete guided path. You can also keep reading this free tutorial to work through camera tracking, the fighter HUD, and damage-triggered shake.

Make a Complete Card Battler in Godot e1788415191322 - How to Build a Godot Fighting Game Camera and HUD
FREE GODOT COURSE
LEARN GODOT, UNITY, UNREAL & MORE
ACCESS FOR FREE
AVAILABLE FOR A LIMITED TIME ONLY

Build the Godot Fighting Game Camera Follow System

Start by keeping the camera centered between the fighters. You will then smooth its horizontal movement and adjust its distance as the players move apart.

Positioning the Camera

Select the scene’s Camera3D and set its position to approximately X: 0, Y: 1.3, Z: 4. The Z value sets its distance from the action, while the Y value leaves space above the fighters for the HUD. The script will control the horizontal position.

image 82 - How to Build a Godot Fighting Game Camera and HUD

Creating the Camera Controller Script

Create a Camera folder inside Scripts, then create camera_controller.gd. Extend Camera3D and export two target references:

extends Camera3D

@export var target_a : Node3D
@export var target_b : Node3D

Attach the script to the Camera3D node. In the Inspector, assign the first fighter to Target A and the second fighter to Target B.

Godot Inspector panel showing Target A assigned to Fighter and Target B assigned to Fighter2 on the Camera3D node.

Following the Players

Call _camera_follow every physics frame. Find the midpoint between the fighters with a lerp weight of 0.5, then use that midpoint for the camera’s X position:

func _physics_process(delta):
	_camera_follow(delta)

func _camera_follow (delta : float):
	var center_pos : Vector3 = target_a.global_position.lerp(target_b.global_position, 0.5)

	position.x = center_pos.x

Linear interpolation takes a start, an end, and a weight. A weight of 0 returns Target A’s position, 1 returns Target B’s position, and 0.5 returns the midpoint. This keeps the camera horizontally between both fighters.

The fighting game running with two fighters facing each other on a stone platform, the camera centered between them.

Adding Smoothing

Directly assigning the midpoint makes the camera mirror every quick movement. To soften that response, add an exported smoothing value:

@export var smoothing : float

Replace the direct X assignment with an interpolation from the current position toward the midpoint:

position.x = lerp(position.x, center_pos.x, smoothing * delta)

Set Smoothing to a value such as 5 in the Inspector. The camera can now ease toward the center instead of snapping to every dash or small movement.

image 83 - How to Build a Godot Fighting Game Camera and HUD

Dynamic Zoom with a Curve

When the fighters are close, the camera should move closer. When they separate, it should pull back to keep them in view. Export a Curve resource to map the distance between the players to the camera’s Z position:

@export var zoom_curve : Curve

The curve’s horizontal axis represents fighter separation, and its vertical axis represents camera distance.

Configuring the Zoom Curve

Create a new resource in the Zoom Curve field, open its editor, and set these ranges:

  • Min Value: 4, the closest camera Z position.
  • Max Value: 6, the furthest camera Z position.
  • Min Domain: 2, the minimum fighter distance used by the curve.
  • Max Domain: 10, the maximum fighter distance used by the curve.

Godot Inspector showing the Zoom Curve property on the Camera3D node with the curve editor open, displaying min and max value and domain settings.

Add a bottom-left control point at distance 2, Z position 4 and a top-right point at distance 10, Z position 6. With this mapping, a separation of about 6 units gives a camera Z position of roughly 5.

Godot Inspector showing the completed zoom curve with two control points mapping player distance (2-10) to camera Z position (4-6).

Sampling the Zoom Curve in Code

Calculate the distance between the targets and pass it to zoom_curve.sample():

func _camera_follow (delta : float):
	var center_pos : Vector3 = target_a.global_position.lerp(target_b.global_position, 0.5)
	var distance : float = target_a.global_position.distance_to(target_b.global_position)

	position.x = lerp(position.x, center_pos.x, smoothing * delta)

	position.z = zoom_curve.sample(distance)

distance_to measures the separation, and sampling the curve returns the corresponding camera Z value. You can tune the value range, domain, and curve shape to suit the arena and change how strongly the camera responds at different distances.

The fighting game running during a debug session, showing the camera dynamically positioned between two fighters with zoom based on their distance.

Build the Health and Stamina HUD Layout

With the action framed, add a HUD panel for each fighter. The health bar will show live damage updates; the stamina bar will be prepared for a later system.

Creating the HUD Panel

Open the Fighter scene and add a CanvasLayer child. This keeps the interface separate from the 3D scene. Add a Panel beneath it, rename the panel Hud, and resize and position it near the top of the screen.

Godot editor showing the fighter scene tree with a CanvasLayer and Hud Panel node, and the 2D viewport displaying the panel element.

Adding the Health Bar

Add a ProgressBar beneath Hud, rename it HealthBar, and position it inside the panel. Disable Show Percentage to remove the default text overlay.

Under Theme Overrides > Styles, give both Background and Fill a new StyleBoxFlat. Use dark gray for the background and red for the fill.

Godot editor showing the HealthBar ProgressBar with a red fill color and dark gray background, with the color picker open in the Inspector.

Set the Corner Radius to 10 on both style resources to round the bar’s corners.

image 84 - How to Build a Godot Fighting Game Camera and HUD

Adjusting the Project Resolution

Open Project > Project Settings > Display > Window. Set Viewport Width to 1280 and Viewport Height to 720.

image 85 - How to Build a Godot Fighting Game Camera and HUD

In the Stretch section, set Mode to Canvas Items and Aspect to Keep so the interface scales with the window while keeping its aspect ratio.

image 86 - How to Build a Godot Fighting Game Camera and HUD
The fighting game running in debug mode with two fighter characters on stage and the red health bar displayed in the top-left corner.

Resizing the HUD and Adding Health Text

Select Hud and set its size under Layout > Transform to 425 by 84 pixels. Increase the HealthBar’s height to fill more of the panel.

image 87 - How to Build a Godot Fighting Game Camera and HUD

Add a Label child to HealthBar and rename it HealthText. Set Layout Mode to Anchors and choose the Left Wide preset.

image 88 - How to Build a Godot Fighting Game Camera and HUD

Use 100 as sample text and set Vertical Alignment to Center. In Label Settings, assign the project’s imported font, increase the font size, and add a dark Outline to keep the number readable against the red fill.

image 89 - How to Build a Godot Fighting Game Camera and HUD

Styling the HUD Panel Background

On Hud, open Theme Overrides > Styles > Panel and assign a new dark StyleBoxFlat. Make the health bar’s background slightly lighter than the panel, then set the panel’s Corner Radius to 12.

image 90 - How to Build a Godot Fighting Game Camera and HUD

Adding the Stamina Bar with a VBoxContainer

Add a VBoxContainer beneath Hud and rename it HudElements. Set its Layout Mode to Anchors and its preset to Full Rect.

image 91 - How to Build a Godot Fighting Game Camera and HUD

Move HealthBar beneath HudElements.

image 92 - How to Build a Godot Fighting Game Camera and HUD

Set HealthBar’s Y Custom Minimum Size to 50 so it remains visible inside the container. Adjust the container margins to add padding around the panel’s edges.

image 93 - How to Build a Godot Fighting Game Camera and HUD

Duplicate HealthBar within HudElements and make these changes:

  1. Rename the copy StaminaBar.
  2. Set its Y Custom Minimum Size to 20.
  3. Delete its HealthText child.
  4. Open Theme Overrides > Styles > Fill, choose Make Unique, and change the fill to a yellowish color.

Making the fill resource unique lets you change the stamina color without changing the health bar’s style.

image 94 - How to Build a Godot Fighting Game Camera and HUD

Setting Up Player Position Nodes

Both fighters’ HUDs initially appear on the left. Add a Control child beneath CanvasLayer named Player0Pos, with position X: 27, Y: 25.

Duplicate it as Player1Pos, choose the Top Right anchor preset, and set its position to X: 839, Y: 25. The HUD script will use these two markers to place each panel.

Godot editor showing Player0Pos and Player1Pos Control nodes in the scene tree, with the Inspector displaying Player1Pos positioned at (839, 25) with Top Right anchoring.

Connect Each Fighter’s HUD to Health Updates

Creating the Fighter HUD Script

Create fighter_hud.gd in Scripts/Fighter, extend Panel, and attach it to Hud. Add references to the Fighter root, the bars, the health label, and the position markers:

extends Panel

@onready var fighter : Fighter = $"../.."

@onready var health_bar : ProgressBar = $HudElements/HealthBar
@onready var health_text : Label = $HudElements/HealthBar/HealthText

@onready var stamina_bar : ProgressBar = $HudElements/StaminaBar

@onready var player_0_pos : Control = $"../Player0Pos"
@onready var player_1_pos : Control = $"../Player1Pos"

The relative path to fighter goes up through CanvasLayer to the Fighter node. The other references match the HUD hierarchy you just built.

Positioning the HUD

In _ready, use the fighter’s player_id to select its position marker:

func _ready ():
	# Position HUD based on player ID
	if fighter.player_id == 0:
		position = player_0_pos.position
	elif fighter.player_id == 1:
		position = player_1_pos.position

Player ID 0 uses the left marker, while ID 1 uses the right marker. Save and run the game to check that each panel appears on its corresponding side.

Two fighters facing each other in a stone arena with HUD panels positioned on the left and right sides of the screen for Player 1 and Player 2.

Updating the Health Bar

The project’s GlobalEvents.FighterDamaged signal reports when a fighter takes damage. Because GlobalEvents is an autoload, the HUD can access it without a separate node reference.

Extend _ready to set the bar’s maximum, display the initial health, and connect the damage event:

func _ready ():
	if fighter.player_id == 0:
		position = player_0_pos.position
	elif fighter.player_id == 1:
		position = player_1_pos.position

	# Health bar
	health_bar.max_value = fighter.max_health
	_update_health_bar(fighter)

	# Events
	GlobalEvents.FighterDamaged.connect(_update_health_bar)

Add _update_health_bar. It checks which fighter was damaged before updating the bar and label, so one player’s event does not change the other player’s HUD:

func _update_health_bar (damaged_fighter : Fighter):
	if damaged_fighter != fighter:
		return

	health_bar.value = fighter.current_health
	health_text.text = str(fighter.current_health)

Fixing Decimal Display

Because current_health is a float, the label initially shows values such as 92.0.

Two fighters in combat with health bars displaying decimal values of 100.0 and 92.0, showing the issue before converting to whole numbers.

Convert the health value to an integer before turning it into a string. int() truncates the decimal portion:

func _update_health_bar (damaged_fighter : Fighter):
	if damaged_fighter != fighter:
		return

	health_bar.value = fighter.current_health
	health_text.text = str(int(fighter.current_health))

The health number now displays as a whole number when damage updates arrive.

Two fighters in combat with health bars displaying whole number values of 100 and 60 after applying the int() conversion fix.

Preparing the Stamina Bar

Add a function that accepts a stamina value and updates the bar:

func _update_stamina_bar (stamina : float):
	stamina_bar.value = stamina

This is preparation only. Connecting a stamina update signal and setting the bar’s max_value belong to the later stamina implementation.

Summary

Each HUD now selects its screen position from the player ID and updates its health bar and number through the damage event. You can use that same event to add camera feedback next.

Add Damage-Triggered Camera Shake

The existing hit flash already indicates damage. A brief shake adds another visual response while retaining the camera-follow system.

Two 3D fighter characters facing each other on a stone arena with health bars displayed at the top of the screen.

Understanding Camera Offset Properties

Camera3D exposes H Offset and V Offset for horizontal and vertical view offsets. These let you shift the view without changing the camera node’s position, so the follow logic can continue positioning it between the fighters.

Godot Inspector panel showing Camera3D node properties including H Offset and V Offset fields set to 0.0.

Adding the Intensity Variable

In camera_controller.gd, add an intensity value below the exports:

var intensity : float = 0.0

This controls the maximum random offset applied each frame. Damage raises the intensity, and damping gradually reduces it toward zero.

Connecting to the FighterDamaged Signal

Add a damage callback that sets the initial shake intensity. It accepts a Fighter parameter to match the signal:

func _on_fighter_damaged (fighter : Fighter):
	intensity = 0.05

Connect the callback in _ready:

func _ready ():
	GlobalEvents.FighterDamaged.connect(_on_fighter_damaged)

Processing the Camera Shake

Inside _physics_process, after _camera_follow(delta), damp the intensity and assign random horizontal and vertical offsets:

if intensity > 0:
	intensity = lerpf(intensity, 0, delta * 10)
	h_offset = randf_range(-intensity, intensity)
	v_offset = randf_range(-intensity, intensity)

lerpf moves the intensity toward zero at a rate based on delta * 10. A higher damping multiplier shortens the shake; a lower one lets it last longer. randf_range chooses an offset between negative and positive intensity for each axis.

Creating Export Variables for Tweaking

Expose the amount and damping so you can adjust them in the Inspector:

@export var shake_amount : float = 0.05
@export var shake_damping : float = 10.0

Update the damage callback to use shake_amount:

func _on_fighter_damaged (fighter : Fighter):
	intensity = shake_amount

Then replace the hard-coded damping multiplier with shake_damping:

if intensity > 0:
	intensity = lerpf(intensity, 0, delta * shake_damping)
	h_offset = randf_range(-intensity, intensity)
	v_offset = randf_range(-intensity, intensity)

Complete Camera Shake Code

The complete camera controller combines midpoint tracking, horizontal smoothing, distance-based zoom, and damage-triggered shake:

extends Camera3D

@export var target_a : Node3D
@export var target_b : Node3D

@export var smoothing : float
@export var zoom_curve : Curve

var intensity : float = 0.0

@export var shake_amount : float = 0.05
@export var shake_damping : float = 10.0

func _ready ():
	GlobalEvents.FighterDamaged.connect(_on_fighter_damaged)

func _on_fighter_damaged (fighter : Fighter):
	intensity = shake_amount

func _physics_process(delta):
	_camera_follow(delta)

	if intensity > 0:
		intensity = lerpf(intensity, 0, delta * shake_damping)
		h_offset = randf_range(-intensity, intensity)
		v_offset = randf_range(-intensity, intensity)

func _camera_follow (delta : float):
	var center_pos : Vector3 = target_a.global_position.lerp(target_b.global_position, 0.5)
	var distance : float = target_a.global_position.distance_to(target_b.global_position)

	position.x = lerp(position.x, center_pos.x, smoothing * delta)

	position.z = zoom_curve.sample(distance)

The starting values of 0.05 for shake amount and 10.0 for damping give a subtle effect. Adjust them in the Inspector to tune the response to hits.

Recap and Next Steps

Your camera and HUD now work together to frame the fight and communicate damage. You have:

  • Centered the camera between two targets with smoothed horizontal tracking.
  • Mapped fighter separation to camera distance through a configurable zoom curve.
  • Built separate HUD panels and connected health bars and whole-number labels to damage events.
  • Added adjustable camera shake using offsets that leave the follow positioning intact.

The stamina update function is ready for the next stage, where you can connect the stamina system to the prepared bar. Continue with the Intermediate Godot Mini-Degree for the full guided fighting-game project.

Did you come across any errors in this tutorial? Please let us know by completing this form and we’ll look into it!

FREE COURSES
Python Blog Image - How to Build a Godot Fighting Game Camera and HUD

FINAL DAYS: Unlock coding courses in Unity, Godot, Unreal, Python and more.