Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

BATTLEFIELD 4 Progress Indicator

4.92/5 (5 votes)
15 Dec 2013CPOL2 min read 23K   273  
Hide your personal gaming preference in plain sight with this control

Introduction

I like the game Battlefield 4.
The game is extremely well designed in almost every aspect.
I especially like the idea that you visually start the game from a Terminal where you see some flickering images (distorted) of the map being loaded while a prompt is awaiting input in the top right corner.

The prompt is also used as a progress indicator when pages are loaded on Battlelog.

Design

The prompt is designed so it show up like an old terminal prompt with 10 lines or so in height. There is a clear gap between each line to indicate a very old monitor of some sort.
There is also an artifact showing when the prompt is raised to full prompt, like a halo indicating a slow refresh rate.
The prompt is shown in 3 different modes depending on the state of the game. On Battlelog, it's always the same mode.

Progress Control

I decided to create the prompt as a progress control that could be used as any other progress control.
This would allow me and others that like Battlefield to show progress in our otherwise dull financial apps with a smile on our lips.

The progress indicator can be used like this:

XML
//
// Using Bf4ProgressIndicator
//
<Grid>
  <controls1:Bf4ProgressIndicator Margin="5" Value="{Binding ...}" PromptColor="White"/>
</Grid>  

The control has the following properties:

The indicator will switch between 3 modes:

  • Mode 1) Value is below 1/3 of maximum. A full prompt is just blinking.
  • Mode 2) Value between 1/3 of maximum and below 2/3 of maximum. Underscore moving upward.
  • Mode 3) Value above 1/3 of maximum. Switching between full prompt and underscore.

Each mode has its own style.
The style is set when the value changes and according to the rules above.

Here is the ProgressChanged method:

C#
/// <summary>
/// Progresses the changed.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The <see cref="DependencyPropertyChangedEventArgs"/> 
/// instance containing the event data.</param>
protected void ProgressChanged(Object sender, DependencyPropertyChangedEventArgs e)
{
  OnValueChanged(new RoutedPropertyChangedEventArgs<double>(
    e.OldValue != null?(double)e.OldValue:0, 
    e.NewValue!=null?(double)e.NewValue:0));
  var oldStyle = Style;
  var diff = Maximum - Minimum;
  Style = Value < Minimum + diff/3 ? style1 : Value > Maximum - diff/3 ? style3 : style2;
  if (oldStyle == Style) return;
  if (promptStoryboard != null && partPrompt != null) 
    promptStoryboard.Stop(partPrompt);
  if (promptStoryboard1 != null && partPrompt1 != null) 
    promptStoryboard1.Stop(partPrompt1);
  if (promptStoryboard2 != null && partPrompt2 != null) 
    promptStoryboard2.Stop(partPrompt2);
  if (promptStoryboard3 != null && partPrompt3 != null) 
    promptStoryboard3.Stop(partPrompt3);
  promptStoryboard = GetTemplateChild("PART_StoryBoard") as Storyboard;
  promptStoryboard1 = GetTemplateChild("PART_StoryBoard1") as Storyboard;
  promptStoryboard2 = GetTemplateChild("PART_StoryBoard2") as Storyboard;
  promptStoryboard3 = GetTemplateChild("PART_StoryBoard3") as Storyboard;
  partPrompt = GetTemplateChild("PART_Prompt") as Rectangle;
  partPrompt1 = GetTemplateChild("PART_Prompt1") as Rectangle;
  partPrompt2 = GetTemplateChild("PART_Prompt2") as Rectangle;
  partPrompt3 = GetTemplateChild("PART_Prompt3") as Rectangle;
  if (promptStoryboard != null && partPrompt != null) { 
    promptStoryboard.Begin(partPrompt, true); }
  if (promptStoryboard1 != null && partPrompt1 != null) { 
    promptStoryboard1.Begin(partPrompt1, true); }
  if (promptStoryboard2 != null && partPrompt2 != null) { 
    promptStoryboard2.Begin(partPrompt2, true); }
  if (promptStoryboard3 != null && partPrompt3 != null) { 
    promptStoryboard3.Begin(partPrompt3, true); }
} 

Switching between different styles with running animations will require the storyboards to be stopped and started manually.

The visual part of the progress indicator is created as a style.
Here is an example of the style for mode 1:

XML
<Style x:Key="Bf4ProgressIndicatorStyle1" TargetType="{x:Type local:Bf4ProgressIndicator}">
  <Setter Property="Background" Value="Transparent"/>
  <Setter Property="Foreground" Value="White"/>
  <Setter Property="Template">
    <Setter.Value>
      <ControlTemplate TargetType="{x:Type local:Bf4ProgressIndicator}">
        <Border BorderBrush="{TemplateBinding BorderBrush}" 
         BorderThickness="{TemplateBinding BorderThickness}" 
         Background="{TemplateBinding Background}" SnapsToDevicePixels="true">
          <Grid>
            <Grid.RowDefinitions>
              <RowDefinition Height="1*"/>
              <RowDefinition Height="9*" MinHeight="{TemplateBinding MinHeight}"/>
              <RowDefinition Height="1*"/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
              <ColumnDefinition Width="1*"/>
              <ColumnDefinition Width="1*"/>
              <ColumnDefinition Width="9*" MinWidth="{TemplateBinding MinWidth}"/>
              <ColumnDefinition Width="1*"/>
              <ColumnDefinition Width="1*"/>
            </Grid.ColumnDefinitions>
            <Rectangle x:Name="PART_Prompt" Grid.Row="1" Grid.Column="2" 
               Width="Auto" Height="Auto" Stretch="Fill">
            <Rectangle.Resources>
              <SolidColorBrush x:Key="PromptBrush" 
               Color="{Binding Path=PromptColor, 
               RelativeSource={RelativeSource AncestorType={x:Type local:Bf4ProgressIndicator}}}" />
            </Rectangle.Resources>
            <Rectangle.Fill>
              ...
            </Rectangle.Fill>
            <Rectangle.Effect>
              <DropShadowEffect BlurRadius="10" Color="{Binding Path=PromptColor, 
               RelativeSource={RelativeSource AncestorType={x:Type local:Bf4ProgressIndicator}}}" 
               Opacity="1" ShadowDepth="0" />
            </Rectangle.Effect>
            <Rectangle.Triggers>
              <EventTrigger RoutedEvent="Loaded">
                <BeginStoryboard>
                  <Storyboard x:Name="PART_StoryBoard">
                    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="PART_Prompt" 
                     Storyboard.TargetProperty="Opacity" AutoReverse="False" 
                     RepeatBehavior="Forever">
                      <LinearDoubleKeyFrame Value="1" KeyTime="0:0:.1" />
                      <LinearDoubleKeyFrame Value="1" KeyTime="0:0:.2" />
                      <LinearDoubleKeyFrame Value="0" KeyTime="0:0:.3" />
                      <LinearDoubleKeyFrame Value="0" KeyTime="0:0:.7" />
                    </DoubleAnimationUsingKeyFrames>
                  </Storyboard>
                </BeginStoryboard>
              </EventTrigger>
            </Rectangle.Triggers>
          </Rectangle>
          <Rectangle x:Name="PART_HighLight" Grid.Row="1" Grid.Column="1" 
           Grid.ColumnSpan="3" Width="Auto" Height="Auto" 
           Stretch="Fill" Opacity="0">
            <Rectangle.Fill>
            ...
            </Rectangle.Fill>
            <Rectangle.Triggers>
              <EventTrigger RoutedEvent="Loaded">
                <BeginStoryboard>
                  <Storyboard x:Name="PART_StoryBoard2">
                    <DoubleAnimationUsingKeyFrames Storyboard.TargetName="PART_HighLight" 
                     Storyboard.TargetProperty="Opacity" AutoReverse="False" 
                     RepeatBehavior="Forever">
                      <LinearDoubleKeyFrame Value=".25" KeyTime="0:0:.1" />
                      <LinearDoubleKeyFrame Value=".25" KeyTime="0:0:.15" />
                      <LinearDoubleKeyFrame Value="0" KeyTime="0:0:.2" />
                      <LinearDoubleKeyFrame Value="0" KeyTime="0:0:.7" />
                    </DoubleAnimationUsingKeyFrames>
                  </Storyboard>
                </BeginStoryboard>
              </EventTrigger>
            </Rectangle.Triggers>
          </Rectangle>
        </Grid>
      </Border>
    </ControlTemplate>
  </Setter.Value>
  </Setter>
</Style>

Sample Application

The sample application illustrates how the progress indicator is used and the different modes.
In the game, it's mostly used as white on black but the control allows any color to be used.

Points of Interest

I tried to keep the used images (Geometry) as static resources, but I did not find a way to dynamically change the color.
I ended up adding the geometry to each element in the visual tree so I could bind to a dependency property on the control.

History

  • 15th December, 2013: Version 1.0.0.0
  • 16th December, 2013: Added link to Nuget package

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)