Blazor Web Assembly Canvas
In diesem Tutorial zeige ich wie man mit Blazor Web Assembly ein Canvas nutzt und dort mittels JS Interopt zeichnet. Das Projekt dient dazu herauszufinden wie performant diese Methode aktuell funktioniert und soll die Technik für ein Projekt evaluieren.
Blazor Web Assembly Canvas
Mit JavaScript kann man in ein HTML 5 Canvas die tollsten Dinge zeichnen. Viele mobile Spiele nutzen diese Zeichenfläche um die Spielwelt in 2D oder 3D (WebGL) zu zeichnen. Es gibt zahlreiche Frameworks die einen Entwickler dabei unterstützen. Doch wie funktioniert das mit einer Blazor Web Assembly Applikation? Ich habe mir angesehen ob man mit Blazor sinnvoll in Canvas zeichnen kann. Den Source Code für das Projekt findet man wie üblich auf meiner GitHub Seite.
Blazor und Canvas
Um die Hoffnungen gleich vorweg zu enttäuschen. Man kann aktuell mit C# nicht direkt in ein Canvas zeichnen. Man muss den Umweg über JS Interopt gehen. Zum Glück gibt es mit
Blazor.Extensions.Canvas
ein NuGet um den Implementierungsaufwand zu reduzieren. Leider scheint das Projekt aber aktuell zu pausieren, der letzte Commit war im April 2021 (Stand heute 03.01.2021). Aber vielleicht ist dort derzeit auch kein Update nötig…
Testprojekt
Ich habe ein neues Blazor Web Assembly Projekt erstellt und das NuGet hinzugefügt:
Ich habe zwei Klassen erstellt:
- Agent
Das ist ein beweglicher Agent der als Kreis in einer zufälligen Größe gezeichnet wird. Bei der Erstellung bekommt jeder Agent eine zufällige Position und einen Richtungsvektor. Die Updatelogik ist simpel: der Agent bewegt sich mit der konstanten Geschwindigkeit in die angegebene Richtung bis er am Rand des Canvas abprallt und die Richung ändert. - Vector
Eine Vektor Basisklasse für die Position und den Richtungsvektor der Agenten
Die Gamelogic ist recht simpel und in der Index.razor Datei zu finden. Nachdem 40 Agenten zufällig erstellt wurden wird laufend die Update Methode dafür aufgerufen:
[JSInvokable] public async ValueTask RenderInBlazor(float timeStamp) { await this._context.BeginBatchAsync(); await this._context.ClearRectAsync(0, 0, this.width, this.height); // draw all agents foreach(var agent in this.agents) { agent.Update(timeStamp * 0.001f); await agent.Draw(this._context); agent.Bounce(this.width, this.height); } await this._context.SetLineWidthAsync(1); // draw all lines for(int i=0; i<agents.Count; ++i) { var agent = agents[i]; for(int j=i+1; j<agents.Count; ++j) { var other = agents[j]; double dist = agent.Pos.GetDistance(other.Pos); if (dist > 200) continue; await this._context.BeginPathAsync(); await this._context.MoveToAsync(agent.Pos.X, agent.Pos.Y); await this._context.LineToAsync(other.Pos.X, other.Pos.Y); await this._context.StrokeAsync(); } } await this._context.EndBatchAsync(); }
Zuerst wird das Canvas gelöscht, danach alle Agenten gezeichnet und danac die Linien zwischen den Agenten. Da die Update Methode die verstrichene Zeit mit übergeben bekommt erreicht man ein fließende Bewegung. Mit den BeginBatch und EndBatch Methoden wurde bereits eine Optimierung eingebaut, damit alle JS Operationen als Batch gesammelt abgearbeitet werden und es dadurch zu weniger Overhead kommt. Werden alle Linien gezeichnet, dann sieht das Ergebnis so aus:
Bei nur 40 sich bewegenden Agenten bricht die Framerate ein. Die Prozessorauslastung ist für diese recht simple Zeichenoperationen enorm. Der Overhead durch JS Interopt ist im Vergleich zu eine nur JavaScript Alternative indiskutabel langsam.
Nur durch eine Optimierung ist eine vernünftige Darstellung möglich. In diesem Beispiel werden die Linien nur gezeichnet, wenn die Agenten einen bestimmten Abstand nicht überschreiten.
Fazit
In Canvas mit C# Code zeichnen macht aktuell nur bedingt Sinn. Der Weg über JS Interopt ist langsam und nicht für interaktive Applikationen wie Spiele sinnvoll. In diesem Fall müsste man den Code komplett in JavaScript schreiben und über JS Interopt lediglich einmalig mit Variablen aus C# aufrufen.