Dr. Neil's Notes
Software > Coding
.NET Console Animations
Introduction
After getting .NET 6 and Visual Studio Code running on a Raspberry Pi, I played around with some simple .NET 6 console code. The following are my notes on the console animations I created. These will work on any platform supported by .NET 6 (Windows, Mac, Linux). If you want to get a Raspberry Pi setup to run .NET code, follow the instructions in the .NET Development on a Raspberry Pi document. This document assumes you have installed .NET 6 and Visual Studio Code. This code will run on a Mac, Windows, Linux, and even a Raspberry Pi.
A video that accompanies this Note can be found here
Creating a new .NET project
Start by creating a folder for your code projects. I created a folder called dev. Open a Terminal session and navigate to where you want to create your folder (eg Documents) and enter
mkdir dev
This makes the directory dev
navigate to that directory
cd dev
then open Visual Studio Code. Note the 'dot' after the code, this tells Visual Studio Code to open the current folder.
code .
Your terminal entries should look something like this:
~ $ cd Documents/
~/Documents $ mkdir dev
~/Documents $ cd dev/
~/Documents/dev $ code .
~/Documents/dev $
In Visual Studio Code create a new folder in your dev folder, call it ConsoleAnimations
Make sure you have the Explorer open (Ctrl+Shift+E), then click the New Folder icon, and name the new folder ConsoleAnimations
Open the Terminal window in Visual Studio Code, you can use the menu to select Terminal - New Terminal or press Ctrl+Shift+`
The Terminal will open along the bottom of your Visual Studio Code window and it will open in the folder you have opened with Visual Studio Code. In this case it will be your dev folder.
Change the directory to the new folder you just created.
cd ConsoleAnimations/
To create the .NET 6 console application use the command
dotnet new console
The default name of the new project is the name of the folder you are creating the project in. The output should look like this.
~/Documents/dev/ConsoleAnimations $ dotnet new console
The template "Console App" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on /home/pi/Documents/dev/ConsoleAnimations/ConsoleAnimations.csproj...
Determining projects to restore...
Restored /home/pi/Documents/dev/ConsoleAnimations/ConsoleAnimations.csproj (in 1.13 sec).
Restore succeeded.
You should also notice that files have been created in the Explorer view of Visual Studio Code
You can run the new application from the Terminal window in Visual Studio Code with
dotnet run
This dotnet run
command will compile the project code in the current folder and run it.
~/Documents/dev/ConsoleAnimations $ dotnet run
Hello, World!
As you can see it does not do much yet, other than output Hello, World!
Creating a first animation
The first animation is a super simple spinning line, like you sometimes see when a console application is waiting for something to finish.
In Visual Studio Code, open the Program.cs file that was created with the project previously. You should see the file in the explorer (as seen in the image above). Click on the file to open it.
It has one line of code above which is a comment.
Console.WriteLine("Hello, World!");
Delete both lines, to leave you with an empty file.
Enter the following code into the file
string frames = @"/-\|";
Console.CursorVisible = false;
while (Console.KeyAvailable is false)
{
foreach(var c in frames)
{
Console.Write($"\b{c}");
await Task.Delay(300);
}
}
Console.WriteLine("Finished");
Console.CursorVisible = true;
In the Terminal window enter the dotnet run
command again to compile and run the application.
When the program runs, it will display a spinning line until you enter a key. You can press any key in the terminal to end the program.
Let's break down what this code is doing. The first line is defining a string, a collection of characters, named frames, as it represents the frames of the animation. The code will enumerate through each character, and display it over the previous character to create the animation.
Then the cursor for the console is hidden using Console.CursorVisible = false;
. At the end of the program the cursor is made visible again.
The next line creates a loop that will run until a key is pressed in the console. Console.KeyAvailable
will return true when a key has been pressed, and so it is checked that Console.KeyAvailable
is false
. While a key is not available, do everything in the brackets, again and again.
The foreach
loop takes each character c
in the string frames
, and writes it out, proceeded by a backspace, the \b
character is a backspace. After the output of each character the program waits for 300 milliseconds before continuing. The await Task.Delay
method tells the program to sleep (or delay) before taking the next step.
When a key press is available, the while
loop will finish and the program outputs that it has finished.
Creating an Animate method
To make code easier to manage it is broken down into components of functionality. In this step a method will be created to encapsulate the animation code. As this program grows you will see why this is useful.
Edit your program.cs file to create an Animate method as shown.
string frames = @"/-\|";
Console.CursorVisible = false;
await Animate(frames);
Console.WriteLine("Finished");
Console.CursorVisible = true;
async Task Animate(string frames)
{
while (Console.KeyAvailable is false)
{
foreach(var c in frames)
{
Console.Write($"\b{c}");
await Task.Delay(300);
}
}
}
In the Terminal window enter the dotnet run
command again to compile and run the application.
When the program runs, it will display the same spinning line until you enter a key.
In the code changes the while
loop has been moved into a method called Animate
. This Animate method takes a parameter of type string. The string is used to define the frames to animate.
The async Task
at the start of the method tells the compiler and runtime that this method can run on a different thread. This means the Animate method could be called and then code could continue running afterwards. The await
is used when calling the Animate
method to tell the runtime to wait until the method has completed before continuing to run the code.
This process of taking some existing code and restructuring the code without changing the behaviour is called refactoring.
Animating multiple lines
In this step the animation will go beyond a single character to multiple lines. Each line will still only have a single character at this point. This will be extended further in following steps.
Edit the program.cs file to support two lines for animations, as follows:
string[] frames = new string[]{@"/-\|", @"._._"};
Console.CursorVisible = false;
await Animate(frames);
Console.WriteLine("Finished");
Console.CursorVisible = true;
async Task Animate(string[] frames)
{
Console.Clear();
int length = frames[0].Length;
while (Console.KeyAvailable is false)
{
for(int i = 0; i < length; i++)
{
foreach(var f in frames)
{
Console.WriteLine(f[i]);
}
await Task.Delay(300);
Console.CursorTop = 0;
}
}
}
In the Terminal window, enter the dotnet run
command again to compile and run the application.
When the program runs, it will clear the terminal window, then display two lines, the top line has the same spinning line as before, the line below will show a dot/line transition, you might see it as a shrinking line, or a growing dot.
There are quite a few code changes in this step.
The frames string
is now an array of strings, the []
notation after the variable type tells the compiler this is not a single string, it is a collection of strings. In maths you might call this a single dimensional array. In programming it is also called an array.
The Array
is initialized with two strings, the first is the string used so far in this code, the second is a string of the same length that will define the frames to animate on the second line.
The Animate method has also changed the parameters to now take an array of string
rather than a single string.
The first line of the method now clears the console (or terminal) window, of all contents. This provides the canvas for the animation to be displayed in the console.
A new integer (number) variable is set to the length of the first string in the collection of strings passed into the method with int length = frames[0].Length;
This code assumes all the strings in the collection are the same length, which is true. If we extended this further we might like to change this to use a parameter to set the length of the string, as making assumptions in code is never a good idea.
Inside the while
loop we now have a new for
loop, for(int i = 0; i < length; i++)
This will count the variable i
from 0 to 3, The length of the string is 4 characters, however the escape clause in the for
loop is to stop when i
is no longer less than the length i < length;
, and 3 is the last number that is less than 4.
The foreach
loop inside the for
loop enumerates through each of the strings in the frames array and writes out the character at position i
in a new line.
In this code there are only two animating lines, however you could add more lines and this would still work.
Once the lines are all written to the console window, the delay of 300 milliseconds is awaited.
Finally the cursor position is set to the top of the terminal to start again in the next character (i
) of the string.
Animating multiple characters on multiple lines
To extend the previous step, the animation will now support multiple characters on multiple lines. To achieve this the animation frames will be longer than a single character in the strings in the array of frames. The length of each frame will need to be defined. The following changes to the code will achieve this.
string[] frames = new string[]{@"/ -- \ | ", @" . .. ..."};
Console.CursorVisible = false;
await Animate(3, frames);
Console.WriteLine("Finished");
Console.CursorVisible = true;
async Task Animate(int width, string[] frames)
{
Console.Clear();
int length = frames[0].Length;
while (Console.KeyAvailable is false)
{
for(int i = 0; i < length; i+=width)
{
foreach(var f in frames)
{
Console.WriteLine(f.Substring(i, width));
}
await Task.Delay(300);
Console.CursorTop = 0;
}
}
}
In the Terminal window, enter the dotnet run
command again to compile and run the application.
When the program runs, it will clear the terminal window, then display two lines, the top line has the same spinning line as before, the line below will show a series of dots appear. It is that exciting yet, however the code is now animating multiple characters on multiple lines.
The following code changes enable this new behaviour.
The frames
string array now is initialized with two longer strings. Each string consists of four blocks of three characters. It is important both strings are the same length, otherwise this code will not work. Each three character block in the string represents a frame on a line.
The Animate
method has been changed to take an initial parameter that specifies the width of each frame. In this code the width is 3
.
In the for
loop the variable i
is incremented by the width of the frame on each loop. This points i
to the offset of the next frame, using the code i+=width;
, until all frames have been output.
The Console.WriteLine
method has been changed to output the Substring
of characters from the offset i
, and for 3 (the width) characters. A Substring is useful method of the string class, it lets the code retrieve a section of the string.
Animating a face winking
Let's extend the animation to something a bit more fun, lets use the same code shown in the previous step to create a face that winks. Only the frames string and the width of the frames needs to be changed to achieve this.
string[] frames = new string[]
{
@" ",
@" O O O o O - O o ",
@" /\ /\ /\ /\ ",
@" ---- ---- -- ---- ",
};
Console.CursorVisible = false;
await Animate(12, frames);
Console.WriteLine("Finished");
Console.CursorVisible = true;
async Task Animate(int width, string[] frames)
{
Console.Clear();
int length = frames[0].Length;
while (Console.KeyAvailable is false)
{
for(int i = 0; i < length; i+=width)
{
foreach(var f in frames)
{
Console.WriteLine(f.Substring(i, width));
}
await Task.Delay(300);
Console.CursorTop = 0;
}
}
}
In the Terminal window, enter the dotnet run
command again to compile and run the application.
When the program runs, it will clear the terminal window, then display four lines lines. These should resemble a face that winks.
To make it easier to see the frames in the code, the strings are set out in the file above each other. You can see the animation emerging by looking at the code.
The only other change made was to set the width parameter in the Animate
method to 12
, like this await Animate(12, frames);
.
Adding an extra dimension to the animation
Each string
in the frames collection in the previous step represents all the different frames for the animation on a single line. In this step you will change this so that a collection (Array
) of strings represents a single frame. Then the collection of frames will be a collection of string collections, an array of arrays, known as a two dimensional array.
string[] frame1 = new string[] {@" ",
@" O O ",
@" /\ ",
@" ---- ",
};
string[] frame2 = new string[] { @" ",
@" O o ",
@" /\ ",
@" ---- ",
};
string[] frame3 = new string[] { @" ",
@" O - ",
@" /\ ",
@" -- ",
};
string[] frame4 = new string[] { @" ",
@" O o ",
@" /\ ",
@" ---- ",
};
string[][] frames = new string[][] {frame1, frame2, frame3, frame4};
Console.CursorVisible = false;
await Animate(frames);
Console.WriteLine("Finished");
Console.CursorVisible = true;
async Task Animate(string[][] frames)
{
Console.Clear();
while (Console.KeyAvailable is false)
{
foreach(var frame in frames)
{
foreach(var line in frame)
{
Console.WriteLine(line);
}
await Task.Delay(300);
Console.CursorTop = 0;
}
}
}
In the Terminal window, enter the dotnet run
command again to compile and run the application.
In the terminal window the same animation of the winking face will appear as you observed in the previous step. However you should notice the code is now simpler, and the frames are easier to define as a block. This will have other advantages as you see in the following steps.
In this iteration of the code each frame has been defined as an array of strings, one string per line of the frame. The collection of frames is now defined as string[][]
, which is the notation to define an array of arrays.
The length of each line of each frame is no longer calculated, as the updated code outputs the whole of each line for each frame. This has the advantage that not all lines of the frame need to be the same length. However be aware that the same line on each frame should be the same length, for example if you extend the fourth line in frame3, then you should extend he fourth line in the other frames the same amount.
The for
loops are now simpler too, it is no longer necessary to increment a counter (i
in previous steps) by the length of the line in a frame. The code can take each line of each frame.
With each line, the whole line can be output, using a substring to represent a line is no longer required; Console.WriteLine(line);
.
The behaviour of the code has not changed from the previous step, yet the code has been restructured, this is called refactoring.
Frame reuse
In the previous step frame2 and frame4 are identical. This means you could delete frame4 and reuse frame2 in the animation as follows.
string[] frame1 = new string[] {@" ",
@" O O ",
@" /\ ",
@" ---- ",
};
string[] frame2 = new string[] { @" ",
@" O o ",
@" /\ ",
@" ---- ",
};
string[] frame3 = new string[] { @" ",
@" O - ",
@" /\ ",
@" -- ",
};
string[][] frames = new string[][] {frame1, frame2, frame3, frame2};
Console.CursorVisible = false;
await Animate(frames);
Console.WriteLine("Finished");
Console.CursorVisible = true;
async Task Animate(string[][] frames)
{
Console.Clear();
while (Console.KeyAvailable is false)
{
foreach(var frame in frames)
{
foreach(var line in frame)
{
Console.WriteLine(line);
}
await Task.Delay(300);
Console.CursorTop = 0;
}
}
}
In the Terminal window, enter the dotnet run
command again to compile and run the application.
In the terminal window the same animation of the winking face will appear as you observed in the previous step. The frame2 is reused in the collection of frames:
string[][] frames = new string[][] {frame1, frame2, frame3, frame2};
This step is another refactoring.
Get Creative with the animations
With the code you can now focus on editing the frames to make new animations without changing any of the code. Here are some ideas for animations.
Star jumping
string[] frame1 = new string[] {@" ",
@" O ",
@" /(_)\ ",
@" | | ",
};
string[] frame2 = new string[] { @" \ O / ",
@" (_) ",
@" / \ ",
@" ",
};
string[][] frames = new string[][] {frame1, frame2};
Running
var frame1 = new string[] {
@" O ",
@" /_\_ ",
@" /_\ ",
@" / ",
};
var frame2 = new string[] {
@" O ",
@" /_\_ ",
@" _\\ ",
@" \ ",
};
var frame3 = new string[] {
@" O ",
@" /_\_ ",
@" _\\ ",
@" \ ",
};
string[][] frames = new string[][] {frame1, frame2, frame3};
You can try changing the delay between frames with this for a faster run. Reduce the number for a smaller time between frames, creating the illusion of a faster run.
await Task.Delay(200);
Rocket Launch
This is a bigger animation, the frames are bigger, so you might need to run this in a full screen terminal to get the full animation.
var frame1 = new string[] {
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" | ",
@" | ",
@" ^ ",
@" /_\ ",
@" /___\ ",
@" | | ",
@" |= = =| ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | | | ",
@" /|=|=|=|\ ",
@" / | | \ ",
@" / |#####| \ ",
@"| / \ | ",
@"| / \ | ",
@"|/ \| ",
};
var frame2 = new string[] {
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" | ",
@" | ",
@" ^ ",
@" /_\ ",
@" /___\ ",
@" | | ",
@" |= = =| ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | | | ",
@" /|=|=|=|\ ",
@" / | | \ ",
@" / |#####| \ ",
@"| / ^|^ \ | ",
@"| / ( ) \ | ",
@"|/ (|) \| ",
};
var frame3 = new string[] {
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" | ",
@" | ",
@" ^ ",
@" /_\ ",
@" /___\ ",
@" | | ",
@" |= = =| ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | | | ",
@" /|=|=|=|\ ",
@" / | | \ ",
@" / |#####| \ ",
@"| / (^|^) \ | ",
@"| / ((|)) \ | ",
@"|/ ((;|;)) \| ",
};
var frame4 = new string[] {
@" ",
@" ",
@" ",
@" ",
@" ",
@" | ",
@" | ",
@" ^ ",
@" /_\ ",
@" /___\ ",
@" | | ",
@" |= = =| ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | | | ",
@" /|=|=|=|\ ",
@" / | | \ ",
@" / |#####| \ ",
@"| / (^|^) \ | ",
@"| / ((|)) \ | ",
@"|/ ((;|;)) \| ",
@" ((((:|:)))) ",
};
var frame5 = new string[] {
@" ",
@" ",
@" ",
@" | ",
@" | ",
@" ^ ",
@" /_\ ",
@" /___\ ",
@" | | ",
@" |= = =| ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | | | ",
@" /|=|=|=|\ ",
@" / | | \ ",
@" / |#####| \ ",
@"| / (^|^) \ | ",
@"| / ( | ) \ | ",
@"|/ (( : )) \| ",
@" (( : : )) ",
@" (( : | : )) ",
@" (( :|: )) ",
};
var frame6 = new string[] {
@" ",
@" | ",
@" | ",
@" ^ ",
@" /_\ ",
@" /___\ ",
@" | | ",
@" |= = =| ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | | | ",
@" /|=|=|=|\ ",
@" / | | \ ",
@" / |#####| \ ",
@"| / (^|^) \ | ",
@"| / ( | ) \ | ",
@"|/ (( : )) \| ",
@" (( : : )) ",
@" (( : | : )) ",
@" (( : : )) ",
@" (( : )) ",
@" ( ) ",
};
var frame7 = new string[] {
@" /_\ ",
@" /___\ ",
@" | | ",
@" |= = =| ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | ",
@" | | | | ",
@" /|=|=|=|\ ",
@" / | | \ ",
@" / |#####| \ ",
@"| / (^|^) \ | ",
@"| / ( | ) \ | ",
@"|/ (( : )) \| ",
@" (( : : )) ",
@" (( : | : )) ",
@" (( : : )) ",
@" (( : )) ",
@" ( ) ",
@" | ",
@" | ",
@" ",
@" ",
};
var frame8 = new string[] {
@" | | ",
@" | | ",
@" | | ",
@" | | | | ",
@" /|=|=|=|\ ",
@" / | | \ ",
@" / |#####| \ ",
@"| / (^|^) \ | ",
@"| / ( | ) \ | ",
@"|/ (( : )) \| ",
@" (( : : )) ",
@" (( : | : )) ",
@" (( : : )) ",
@" (( : )) ",
@" ( ) ",
@" | ",
@" | ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
};
var frame9 = new string[] {
@" ( ) ",
@" | ",
@" | ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
@" ",
};
string[][] frames = new string[][] {frame1, frame2, frame3, frame4, frame5, frame6, frame7, frame8, frame9};
Conclusions
The steps presented in this note represent the process I went through to get a little fun console app working in .NET 6 on a Raspberry Pi. However they will work on any platform supported by .NET 6 (Mac, Windows, Linux).
I hope this has helped you understand a few aspects of building a .NET 6 console application.
Created: January 3, 2022 05:35:11