Hi, everyone! 👋
✨ We've already made great progress in bringing our Hydra 🐉 to life, adding animations, movement, enemies 👾, and interactions. But to call it a complete game, we need one crucial element—the possibility of losing. 🎮
In this part, we’ll focus on UI improvements 🖥️ and game over mechanics ☠️. We’ll add a health bar ❤️ to indicate the Hydra’s remaining strength and implement a Game Over screen to signal defeat. These additions will make the gameplay more immersive and structured.
We're continuing our game development on MAUI, ensuring smooth UI rendering and responsive mechanics. Let’s dive in! 🚀

The user interface (UI) plays a crucial role in enhancing the gameplay experience, providing players with important visual feedback. To build our UI, I used Com.Igniscor.Maui.ProgressBar to implement a satiety bar, allowing players to track the Hydra's hunger level. Additionally, I utilized Com.Igniscor.Maui.RadialProgressBar to create a combo timer, which will encourage players to chain attacks effectively.
With these elements in place, the Game page now presents a more structured and informative layout, ensuring that players stay engaged and aware of their progress during the game. Below is how the updated code looks:
<support:NavBarlesPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:SkiaSharp.Views.Maui.Controls;
assembly=SkiaSharp.Views.Maui.Controls"
xmlns:support="clr-namespace:HungerHydra.Views.Support"
xmlns:helpers="clr-namespace:HungerHydra.Helpers"
xmlns:progressBarControl="clr-namespace:ProgressBarControl;
assembly=ProgressBarControl"
xmlns:radialProgressBarControl="clr-namespace:RadialProgressBarControl
;assembly=RadialProgressBarControl"
xmlns:viewModel="clr-namespace:HungerHydra.ViewModel"
x:DataType="viewModel:GameViewModel"
x:Class="HungerHydra.Views.GamePage">
<Grid RowDefinitions="*,9*">
<Grid x:Name="StatusBar"
ColumnDefinitions="*,*"
RowDefinitions="*,1.5*">
<Image
Grid.Row="0"
Grid.RowSpan="2"
Grid.ColumnSpan="2"
Source="{x:Static helpers:Constants+
Images.DirtStatusBarBackGround}"
Aspect="AspectFill"
VerticalOptions="Start" />
<progressBarControl:GradientProgressBar
Grid.Row="0"
Grid.ColumnSpan="2"
StartBackgroundColor="#F2E8C6"
EndBackgroundColor="#DAD4B5"
StartProgressColor="#952323"
EndProgressColor="#A73121"
Margin="10"
PercentageValue="{Binding Satiety}" />
<Label
VerticalOptions="Center"
Margin="10,0"
Grid.Row="1">
<Label.FormattedText>
<FormattedString>
<Span
TextColor="#F2E8C6"
Text="{Static helpers:Constants+
Texts.ScoreLabel}" />
<Span
TextColor="#F2E8C6"
Text=": " />
<Span
TextColor="#F2E8C6"
Text="{Binding Score}" />
</FormattedString>
</Label.FormattedText>
</Label>
<Grid x:Name="Container"
ColumnDefinitions="*,*"
Grid.Row="1"
Grid.Column="1"
Margin="0,0,0,5">
<radialProgressBarControl:RadialProgressBar
WidthRequest="{Binding Source={x:Reference Container},
Path=Height}"
HeightRequest="{Binding Source={x:Reference Container},
Path=Height}"
SweepAngle="360"
StartBackgroundColor="#F2E8C6"
EndBackgroundColor="#DAD4B5"
StartColor="#952323"
EndColor="#A73121"
PercentageValue="{Binding ComboProgressBarPercents}" />
<Label
FontSize="10"
VerticalOptions="Center"
HorizontalOptions="Center">
<Label.FormattedText>
<FormattedString>
<Span
Text="X"
TextColor="#F2E8C6" />
<Span
Text="{Binding Factor}"
TextColor="#F2E8C6"/>
</FormattedString>
</Label.FormattedText>
</Label>
</Grid>
</Grid>
<Image x:Name="BackGround"
Grid.Row="1"
Source="{x:Static helpers:Constants+
Images.GrassBackGround}"
Aspect="AspectFill" />
<controls:SKCanvasView x:Name="SpiderCanvas"
Grid.Row="1" />
<controls:SKCanvasView x:Name="HydraCanvas"
Grid.Row="1">
<controls:SKCanvasView.GestureRecognizers>
<TapGestureRecognizer Tapped="HydraCanvasTapped" />
</controls:SKCanvasView.GestureRecognizers>
</controls:SKCanvasView>
</Grid>
</support:NavBarlesPage>
And here’s what we got! Now, the screen displays essential game elements: the satiety bar shows how hungry our Hydra is, while the combo timer helps track the time for successful attacks.

To ensure everything functions correctly, I introduced several new properties in the ViewModel. These properties handle the logic behind the UI components, such as updating the satiety bar based on the Hydra's actions and managing the combo timer to keep track of attack sequences. With these additions, the game can now provide real-time feedback to the player, making interactions feel more responsive and engaging.
public float Satiety
{
get => _hydra.Satiety;
set
{
OnPropertyChanged();
if (value < 0)
{
_pageIsActive = false;
return;
}
else if (value > MaxSatiety)
{
_hydra.Satiety = MaxSatiety;
return;
}
_hydra.Satiety = value;
}
}
private int _score;
public int Score
{
get => _score;
set
{
_score = value;
OnPropertyChanged();
}
}
private int _factor;
public int Factor
{
get => _factor;
set
{
_factor = value > 5 ? 5 : value;
if (value != 1)
ComboProgressBarPercents = 1.0f;
OnPropertyChanged();
}
}
private float _comboProgressBarPercents;
public float ComboProgressBarPercents
{
get => _comboProgressBarPercents;
set
{
_comboProgressBarPercents = value;
if (value < 0)
{
_comboProgressBarPercents = 0.0f;
Factor = 1;
_isCombo = false;
}
OnPropertyChanged();
}
}
These constants and fields play a crucial role in shaping the gameplay mechanics.
private const float MaxSatiety = 1.5f;
private const float SpiderValue = 0.05f;
private const float StarvePerSecond = 0.0045f;
private const float ComboPerSecond = 0.032f;
private bool _isCombo;
MaxSatiety - determines the highest level of satiety the Hydra can reach.
SpiderValue - defines how much satiety is restored when consuming a spider.
StarvePerSecond - controls how quickly the Hydra loses satiety over time.
ComboPerSecond - dictates the speed at which the combo timer decreases.
_isCombo is a flag used to track whether a combo is currently active.
In the animation loop, we implemented logic to gradually decrease both satiety and combo progress, ensuring a dynamic challenge for the player.
Each frame, the Hydra's satiety level is reduced by StarvePerSecond, simulating the need for continuous feeding. Similarly, if a combo is active, the combo progress decreases at a rate defined by ComboPerSecond. This system encourages players to maintain a steady rhythm of consuming enemies to sustain their satiety and keep combos going.
Satiety -= StarvePerSecond;
ComboProgressBarPercents -= ComboPerSecond;
Additionally, the logic is updated to handle enemy interactions, specifically when the spider is killed during an attack:
if (_hydra.AnimationIndex >= _hydra.CurrentTileSets.Body.TilesCount - 5 && _hydra.State == HydraState.Attack &&
_spiders[i].Id == _hydra.AttackedEnemyId &&
_spiders[i].CurrentState != SpiderState.Die)
{
if (_isCombo)
Factor++;
else
{
_isCombo = true;
ComboProgressBarPercents = 1.0f;
}
Satiety += SpiderValue * Factor;
_spiders[i].Die();
}
You can see our progress so far! With the new UI elements in place and the satiety and combo mechanics fully implemented, the game now feels more dynamic and engaging.

To provide clear feedback to the player when they lose, I implemented a Game Over popup using the Mopups package. This popup serves as a crucial element of the user experience, informing players that their session has ended and allowing them to restart or navigate back to the main menu.
To make the system more flexible, I created a base popup class, which will also serve as a foundation for future popups. This approach ensures consistency in UI design and simplifies the process of adding new popups for different in-game events.
public abstract class BasePopup<TReturnValue> : PopupPage
{
protected BasePopup()
{
BackgroundColor = Color.FromRgba(0, 0, 0, 128);
}
public virtual async Task<TReturnValue?> ShowAsync
(bool animate = true)
{
try
{
await MopupService.Instance.PushAsync(this, animate);
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
return default;
}
public virtual async Task HideAsync(bool animate = true)
{
try
{
await MopupService.Instance.RemovePageAsync(this, animate);
}
catch (Exception exception)
{
Console.WriteLine(exception.Message);
}
}
protected override bool OnBackButtonPressed()
{
return true;
}
}
I create the BasePopupViewModel to manage popup return values, and GameOverViewModel for the Game Over popup:
public abstract class BasePopupViewModel: BaseViewModel
{
private readonly TaskCompletionSource<DialogReturnValue>
_taskCompletionSource = new();
public Task<DialogReturnValue> ReturnValueAsync()
{
return _taskCompletionSource.Task;
}
public void SetReturnValue(DialogReturnStatuses status)
{
_taskCompletionSource.TrySetResult(new DialogReturnValue(status));
}
}
DialogReturnStatus and DialogReturnValue to get return value from popup.
public class DialogReturnValue
{
public readonly DialogReturnStatuses Status;
public DialogReturnValue(DialogReturnStatuses status)
{
this.Status = status;
}
}
public enum DialogReturnStatuses
{
None,
Positive,
Negative
}
Finally, here is the Game Over Popup XAML file, which defines the layout and appearance of the popup when the player loses:
<abstractions:BasePopup
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:helpers="clr-namespace:HungerHydra.Helpers"
xmlns:abstractions="clr-namespace:HungerHydra.Abstractions"
xmlns:viewModel="clr-namespace:HungerHydra.ViewModel"
x:TypeArguments="helpers:DialogReturnValue"
x:Class="HungerHydra.Popups.GameOverPopup"
x:DataType="viewModel:GameOverViewModel"
CloseWhenBackgroundIsClicked="False">
<Border
Margin="10,0"
VerticalOptions="Center"
Stroke="Transparent">
<Grid>
<Image
HeightRequest="{Binding Source={x:Reference MainContent},
Path=Height}"
WidthRequest="{Binding Source={x:Reference MainContent},
Path=Width}"
Source="{x:Static helpers:Constants+
Images.DirtStatusBarBackGround}"
Grid.Row="0"
Aspect="AspectFill" />
<Grid
x:Name="MainContent"
RowDefinitions="Auto,Auto,Auto,Auto"
VerticalOptions="Center">
<Label
Grid.Row="0"
HorizontalOptions="Center"
FontSize="20"
Text="{Static helpers:Constants+
Texts.GameOverPopupTitle}" />
<Label
Grid.Row="1"
HorizontalOptions="Center">
<Label.FormattedText>
<FormattedString>
<Span Text="{Static helpers:Constants+
Texts.CurrentScoreLabel}" />
<Span Text="{Binding Score}" />
</FormattedString>
</Label.FormattedText>
</Label>
<Label
Grid.Row="2"
HorizontalOptions="Center">
<Label.FormattedText>
<FormattedString>
<Span Text="{Static helpers:Constants+
Texts.HighScoreLabel}" />
<Span Text="{Binding HighScore}" />
</FormattedString>
</Label.FormattedText>
</Label>
<Grid
ColumnDefinitions="*,*"
Margin="5"
ColumnSpacing="2"
HorizontalOptions="Fill"
Grid.Row="3">
<Button
Text="{Static helpers:Constants+Texts.Retry}"
Clicked="OnPositiveButtonClicked" />
<Button
Grid.Column="1"
Text="{Static helpers:Constants+Texts.MainMenu}"
Clicked="OnNegativeButtonClicked" />
</Grid>
</Grid>
</Grid>
</Border>
</abstractions:BasePopup>
You can see our result below:

Code-behind for GameOverPopup:
public partial class GameOverPopup
{
private readonly GameOverViewModel _viewModel;
public GameOverPopup(string score, string highScore)
{
InitializeComponent();
BindingContext = _viewModel =
new GameOverViewModel(score,highScore);
}
private async Task ExecuteButtonClickAsync
(DialogReturnStatuses status)
{
if (IsBusy)
{
return;
}
IsBusy = true;
await HideAsync();
_viewModel.SetReturnValue(status);
IsBusy = false;
}
public override async Task<DialogReturnValue?> ShowAsync
(bool animate = true)
{
await base.ShowAsync(animate);
return await _viewModel.ReturnValueAsync();
}
private void OnPositiveButtonClicked(object sender, EventArgs e)
{
_ = ExecuteButtonClickAsync(DialogReturnStatuses.Positive);
}
private void OnNegativeButtonClicked(object sender, EventArgs e)
{
_ = ExecuteButtonClickAsync(DialogReturnStatuses.Negative);
}
}
In addition, I added the following methods in the GameViewModel to handle game reset and other logic:
private async Task GameOver()
{
_pageIsActive = false;
var result = await (new GameOverPopup(Score.ToString(),
Score.ToString())).ShowAsync();
switch (result?.Status)
{
case DialogReturnStatuses.Positive:
await Reset();
break;
case DialogReturnStatuses.Negative:
break;
}
}
private async Task Reset()
{
await MainThread.InvokeOnMainThreadAsync(async () =>
{
var page =
Shell.Current.Navigation.NavigationStack.LastOrDefault();
await Shell.Current.GoToAsync(nameof(GamePage));
Shell.Current.Navigation.RemovePage(page);
});
}
To ensure the Reset method works correctly, you need to register the GamePage route in the Shell. This ensures smooth navigation between pages and the proper execution of methods.
When the Satiety level drops below zero, GameOver is triggered via Task.Run. This allows the game to end automatically as soon as the player runs out of satiety points, without slowing down the gameplay.
public float Satiety
{
get => _hydra.Satiety;
set
{
OnPropertyChanged();
if (value < 0)
{
Task.Run(GameOver);
_pageIsActive = false;
return;
}
else if (value > MaxSatiety)
{
_hydra.Satiety = MaxSatiety;
return;
}
_hydra.Satiety = value;
}
}
And here’s what we’ve achieved so far! With the UI, gameplay mechanics, and Game Over functionality in place, everything is coming together. Now, let’s see how all the pieces fit in action!

Summary
We've made great progress in bringing our Hydra game to life! From adding dynamic movement and animations 🐉, to implementing enemies 👾 and a Game Over screen 💀, we've successfully built the foundation for a fun and interactive game. With a functional UI and engaging gameplay mechanics, players now have a clear path to victory—or defeat!
But this is not the end! There's still much to improve and refine as we continue working on the game. Stay tuned for more updates as we dive deeper into refining the game mechanics and adding new features. 🎮✨
The full code from this article will be here 📂.
📖 Missed the Previous Parts? Catch Up Here!
If you haven’t read the earlier parts of our Game Development on MAUI journey, check them out:
🔹 Part 1: Setting Up Animations – bringing our Hydra 🐉 to life with animations and sprite management.
🔹 Part 2: Movement Mechanics – implementing smooth movement, animation transitions, and adding shadows for depth.
🔹 Part 3: Enemies – adding enemies to the game, creating enemy animations, and implementing interactions between the Hydra and enemies..