When I wrote my last post Blazor Internals you need to know I came across this great project on github: AsteroidsWasm. This project is a remake in C# of the good-ol Asteroids game. See below a screenshot of the Blazor Wasm version of the game running in the browser. We can see the .NET assemblies loaded in the browser Network tab:
The interesting part is that the game runs on most popular UI frameworks. Here is an excerpt from the project’s Github page:
- Asteroids.WinForms – Reconstructed WinForms GUI that uses the game engine with a PictureBox as the main renderer. This is using the .NET Framework 4.8.
- Asteroids.WinForms.Core – Identical in code to the Asteroids.WinForms project but using .NET Core
- Asteroids.Wpf – Equivalent WPF GUI to the WinForms applications that uses a WPF WriteableBitmap as the main renderer with help from the WritableBitmapEx library. This is using the .NET Framework 4.8.
- Asteroids.Wpf.Core – Identical in code to the Asteroids.Wpf project but using .NET Core (see below for more info).
- Asteroids.Xamarin – The core Xamarin application that uses SkiaSharp for 2D rendering via a SKCanvasView.
- Asteroids.Xamarin.Android – Android GUI that uses the core Xamarin library.
- Asteroid.Xamarin.UWP – UWP GUI that uses the core Xamarin library.
- Asteroids.Blazor.Wasm – WebAssembly project that uses Microsoft’s Blazor Client to allow cross-compiling the C# code to WASM so it can be rendered in a browser (see below for more info).
- Asteroids.Blazor.Server – Similar to the Wasm project but instead uses Microsoft’s Blazor Server to execute the application server-side (see below for more info).
- Asteroids.Blazor.Electron – Similar to the above Blazor Server project but running inside Electron to execute the application as a Desktop application.
AsteroidsWasm Project Dependency Diagram
When opening the Visual Studio solution and visualizing the Project Dependency Diagram with NDepend, the architecture becomes clear. Each UI technology is implemented through one or few projects. All UI projects depend on the Asteroids.Standard project. Notice that boxes area is proportional with the number of lines of code of each component. Most UI projects are lightweight:
The project Asteroids.Standard
Asteroids.Standard was adapted from this 2004 WinForms project on CodePlex by Howard Uman, now hosted on GitHub here. We can read: It was chosen because it was already in C# and very straight forward in terms of inheritance and logic. Separating the logic from the UI layer was relatively simple.
The result is the project Asteroids.Standard. It is a .NET Standard 2.0 project that doesn’t bind with any UI framework. It only uses .NET standard types. This reminds me the concept of POCO, Plain Old CLR Objects. POCO classes contain data and logic but they don’t care about persistence, they are persistence ignorant. The same way the classes of Asteroids.Standard are UI framework ignorant. They don’t care about which UI framework renders them. Benefits are:
- Minimised complexity and dependencies on other layers which facilitates loose coupling. Higher layers only care about the POCOs, POCOs don’t care about anything.
- Increases testability through simplification. Even though AsteroidsWasm doesn’t provide tests, this core logic should be relatively easy to test
The astute to be UI framework ignorant is to abstract the UI layer. Doing so in this project is easy:
- All UI elements are vectors
- The palette consists of 4 colors only
Thus the UI layer can be abstracted by the interface Asteroids.Standard.Interfaces.IGraphicLine (implemented by the class Asteroids.Standard.Components.GraphicLine) and the enumeration Asteroids.Standard.Enums.DrawColor. There is also the concept of polygon implemented by IGraphicPolygon: a line with more than 2 points. Lines and polygons’ points are values of the standard structure System.Drawing.Point. Below we can see all types depending directly (in green) and indirectly (in light-green) on the interface IGraphicLine and IGraphicPolygon:
Frame Rendering Strategies
The interface Asteroids.Standard.Interfaces.IGraphicContainer is implemented by all UI projects. There is a single IGraphicContainer object at run-time. It is instantiated by each UI framework. It is then injected into Asteroids.Standard code through the GameController .Initialize() method. Note that the interface IGameController is also responsible for handling user key down/up events and sounds. However here we’ll focus on rendering.
The method IGraphicContainer.Draw(lines, polygons) is responsible for repainting the canvas. GameController.SetFlipTimer() initialize a System.Timers.Timer object that fires 60 times per second. Each UI project has its own strategy to implement the Draw() method.
- The WinForms project calls the Invalidate() method on a PictureBox derived class to redraw lines and polygons in the OnPaint() method.
- The WPF project calls _mainDispatcher.InvokeAsync() to redraw lines and polygons on a WPF WriteableBitmap object.
- The Blazor project relies on the SvgContentContainer class. This class derives from Microsoft.AspNetCore.Components.ComponentBase. In the BuildRenderTree() method, lines and polygons are rendered through an HTML SVG vector graphics.
- The Xamarin project calls the InvalidateSurface() method on a SkiaSharp.Views.Forms.SKCanvasView to redraw lines and polygons in the PaintSurface event.
- The UWP project relies on the Xamarin implementation.
/// Paint or repaint the canvas with the collections of lines and polygons (unfilled).
/// <param name="lines">Collection of <see cref="IGraphicLine"/>.</param>
/// <param name="polygons">Collection of <see cref="IGraphicPolygon"/>.</param>
Task Draw(IEnumerable<IGraphicLine> lines, IEnumerable<IGraphicPolygon> polygons);
I haven’t benchmarked myself the projects. Here is a note from the developers: Performance varies among the technologies with WinForms Core being the clear winner for desktop and Firefox for Blazor/Web. Wpf Core is a close second for desktop, however, the UWP app is also quite fast and has better sound support in that more than one can play at a time, out of the box.
This is not surprising that Winforms is the fastest implementation. Winforms is the win32 (Windows API) .NET wrapper. win32 became mainstream 25 years ago with Windows 95. At this time machines had a few MB of ram at best. Also CPU clock was in the 10 – 100 Mhz range. Thus win32 initially targets machines several orders of magnitude less powerful than regular today machines. This is not surprising that modern UI frameworks, designed for more powerful machines, perform slower than win32.
Of course we are talking here of a simplistic vector based game. Today GPUs are so powerful that graphical APIs targeting GPUs (Direct3D, OpenGL…) are much faster than win32 that run on the CPU.
I found this project interesting because it illustrates well how essential it is to loose couple the UI layer and the logic layer. If done well the UI layer contains only UI rendering related code and is thin enough. This project is a great start for any beginner to learn some architectural concepts like loose-coupling and injection.
The .NET community is now following the project MAUI announced by Microsoft in May 2020. MAUI is the evolution of Xamarin.Forms to unify .NET UI development experience.