A Razor UI Panel for the top HUD in a Breakout game. It renders lives, mode, score (with easing animation), level and personal best, and reads state from a BreakoutGame instance each Tick.
@using System
@using Sandbox
@using Sandbox.UI
@namespace Breakout
@inherits Panel
@if ( Game.IsValid() && Game.State != GameState.Menu )
{
<root>
<div class="chip lives">
<span class="chip-label">LIVES</span>
<div class="dots">
@for ( int i = 0; i < LifeSlots; i++ )
{
<div class="dot @(i < Game.Lives ? "on" : "off")"></div>
}
</div>
</div>
<div class="chip mode @ModeClass">
<span class="chip-label">@ModeName</span>
</div>
<div class="scorebox @(Flashing ? "flash" : "")">
<div class="score-label">SCORE</div>
<div class="score-value">@DisplayScore.ToString( "N0" )</div>
</div>
<div class="chip level">
<span class="chip-label">LEVEL</span>
<span class="level-num">@Game.Level</span>
</div>
@if ( bestScore > 0 )
{
<div class="chip best">
<span class="chip-label">BEST</span>
<span class="best-num">@bestScore.ToString( "N0" )</span>
</div>
}
</root>
}
@code
{
/// <summary>
/// The game this bar reads score, lives, level and mode from.
/// </summary>
[Parameter] public BreakoutGame Game { get; set; }
/// <summary>
/// When true, the score box plays its "just scored" flash.
/// </summary>
[Parameter] public bool Flashing { get; set; }
private double shownScore;
private bool scoreInit;
private long bestScore;
private int LifeSlots => Game.IsValid() ? System.Math.Max( Game.StartingLives, Game.Lives ) : 0;
private string ModeName => Game.IsValid() ? Game.ModeLabel : "CLASSIC";
private string ModeClass => Game.IsValid() ? Game.ModeLabel.ToLowerInvariant() : "classic";
/// <summary>
/// Runs every frame to refresh the personal best and ease the displayed score up toward the real score.
/// </summary>
public override void Tick()
{
base.Tick();
if ( !Game.IsValid() )
return;
bestScore = BreakoutLeaderboards.PersonalBest( Game.Mode );
// Roll the displayed score up toward the real score so it counts up smoothly instead of
// jumping. If the score drops (a new run resets it), snap straight to the new value.
double target = Game.Score;
if ( !scoreInit || target < shownScore )
{
shownScore = target;
scoreInit = true;
}
else if ( shownScore < target )
{
shownScore += (target - shownScore) * System.Math.Min( 1.0, Time.Delta * 12.0 );
if ( target - shownScore < 1.0 )
shownScore = target;
}
}
/// <summary>
/// The score to actually display, rounded down as it eases up.
/// </summary>
private long DisplayScore => (long)System.Math.Floor( shownScore );
protected override int BuildHash() => System.HashCode.Combine( DisplayScore, Game?.Lives, Game?.Level, Game?.State, Game?.Mode, Flashing, bestScore );
}