BabylonJS and Blazor - Getting Set Up
For this article we will go over creating a ASP.NET Core Blazor web application integrated with BabylonJS. The Guided Learning tutorial uses TypeScript and Webpack for the development, but we will be using C#/.NET and Web Assembly for our development.
Checkout BabylonJS and Blazor - Setting Up A State Machine, the next step in the series!
Demo and Source Code
A running Demo of what should be seen when the application is Set Up and running. You can open the below demo in a new tab by going to the BabylonJS Blazor Step 01 website.
You can also see the full source code in GitHub here: canhorn/BabylonJS.Blazor.Game.Tutorial at step/01_Getting-Set-Up
Compare the original step in the Series here: Getting Set Up | Babylon.js Documentation (babylonjs.com)
Set Up
Below is the list of software and tools we will need during the re-implementation of the game into Blazor. The project will be focused on .NET6, but the project should work equally well in .NET5.
Install the .NET6+ SDK
Here you can get the .NET SDK below, I have included two links, one for the download page and another for the .NET6 download page. At the time of writing this .NET6 was still in Preview, but the code will still work with .NET5.
Create new .NET Project
Creating a new project can be done from the command line, but an IDE with the blazorwasm template will also work, below is a code snippet for the command line to create a new Blazor Wasm Host project in the current directory. We want to make sure the Blazor is hosted, this will give us a full solution to work against.
dotnet new blazorwasm -ho -o .
Install Interop Proxy generation tool
Here is where the magic starts, we are going to install a tool that can be used to create a simple abstraction around the BabylonJS JavaScript API that will allow for us to interact with it by using just C# syntax. This tool works by reading a TypeScript definition file and introspecting the Abstract Syntax Tree to create the abstraction for Wasm Interop for the library. I packaged up the tool so it can be installed using the dotnet tool, for easy usage in multiple projects and re-generation processes.
# Install the Tool Globally
dotnet tool install -g EventHorizon.Blazor.TypeScript.Interop.Tool
You can checkout EventHorizon.Blazor.TypeScript.Interop.Tool in NuGet for more details.
In EventHorizon.Blazor.TypeScript.Interop.Tool you can view the Repository where the tool is held.
Generate Proxy from BabylonJS
Here we will generate the Blazor.BabylonJS.WASM project, the tool takes in a list of classes you want to generate against, it will also generate any referenced classes and interfaces. A list of sources are also includes, we use the TypeScript definition files for our version we will be working against.
ehz-generate -c Scene -c Engine -c DebugLayer -c HemisphericLight -c ArcRotateCamera -c MeshBuilder -a Blazor.BabylonJS.WASM -s https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/babylon.d.ts -s https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/gui/babylon.gui.d.ts
After the project is generated open the solution and add a reference to the _generated/Blazor.BabylonJS.WASM project, this will give us access to the BabylonJS abstraction from our new Blazor application.
Main Files
Here we will go over the files we will need and point out the structure of the project we will use. Below are a list of files and the expected contents, I will not go over all the cleanup but at the end of this article you will find a link to the source branch for this step with all the cleanup and updates necessary to get started with a BabylonJS in Blazor Game!
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>BabylonJS.Blazor.Game.Tutorial</title>
<base href="/" />
<link href="css/app.css" rel="stylesheet" />
<link href="BabylonJS.Blazor.Game.Tutorial.Client.styles.css" rel="stylesheet" />
<script src="https://code.jquery.com/pep/0.4.2/pep.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/ammo.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/cannon.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/Oimo.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/libktx.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/earcut.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/materialsLibrary/babylonjs.materials.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/loaders/babylonjs.loaders.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/serializers/babylonjs.serializers.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/BabylonJS/[email protected]/dist/gui/babylon.gui.min.js"></script>
</head>
<body>
<div id="app">Loading...</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/EventHorizon.Blazor.Interop/interop-bridge.js"></script>
</body>
</html>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
@code {
[Inject]
public IJSRuntime JSRuntime { get; set; }
protected override void OnInitialized()
{
EventHorizon.Blazor.Interop.EventHorizonBlazorInterop.JSRuntime = JSRuntime;
}
}
@inherits LayoutComponentBase
<div style="width: 100vw; height: 100vh;">
@Body
</div>
namespace BabylonJS.Blazor.Game.Tutorial.Client.HTML
{
using EventHorizon.Blazor.Interop;
using System.Text.Json.Serialization;
[JsonConverter(typeof(CachedEntityConverter<Canvas>))]
public class Canvas : HTMLCanvasElementCachedEntity
{
public static Canvas GetElementById(
string elementId
) => EventHorizonBlazorInterop.FuncClass(
entity => new Canvas(entity),
new string[] { "document", "getElementById" },
elementId
);
private Canvas(
ICachedEntity entity
)
{
___guid = entity.___guid;
}
}
}
namespace BabylonJS.Blazor.Game.Tutorial.Client.BabylonJSExtensions
{
using System.Text.Json.Serialization;
using BABYLON;
using EventHorizon.Blazor.Interop;
[JsonConverter(typeof(CachedEntityConverter<DebugLayerScene>))]
public class DebugLayerScene : Scene
{
private DebugLayer __debugLayer;
public DebugLayerScene(Engine engine, SceneOptions options = null)
: base(engine, options)
{
}
public DebugLayer debugLayer
{
get
{
if (__debugLayer == null)
{
__debugLayer = EventHorizonBlazorInterop.GetClass<DebugLayer>(
this.___guid,
"debugLayer",
(entity) =>
{
return new DebugLayer() { ___guid = entity.___guid };
}
);
}
return __debugLayer;
}
set
{
__debugLayer = null;
EventHorizonBlazorInterop.Set(
this.___guid,
"debugLayer",
value
);
}
}
}
}
@page "/"
<canvas id="game-window" style="width:100%; height: 100%;"
@onkeydown="HandleKeyDown"></canvas>
namespace BabylonJS.Blazor.Game.Tutorial.Client.Pages
{
using System;
using System.Threading.Tasks;
using BABYLON;
using BabylonJS.Blazor.Game.Tutorial.Client.BabylonJSExtensions;
using BabylonJS.Blazor.Game.Tutorial.Client.HTML;
using EventHorizon.Blazor.Interop.Callbacks;
using Microsoft.AspNetCore.Components.Web;
public partial class Index : IDisposable
{
private Engine _engine;
private DebugLayerScene _scene;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
CreateScene();
}
}
public void Dispose()
{
_engine?.dispose();
}
public void CreateScene()
{
var canvas = Canvas.GetElementById(
"game-window"
);
var engine = new Engine(
canvas,
true
);
// We extend the standard Scene with the DebugLayer getter in the DebugLayerScene
_scene = new DebugLayerScene(
engine
);
var light1 = new HemisphericLight(
"light1",
new Vector3(
0,
2,
8
),
_scene
);
var camera = new ArcRotateCamera(
"Camera",
(decimal)(Math.PI / 2),
(decimal)(Math.PI / 4),
2,
Vector3.Zero(),
_scene
);
_scene.activeCamera = camera;
camera.attachControl(
false
);
var sphere = MeshBuilder.CreateSphere(
"sphere",
new
{
diameter = 1
},
_scene
);
engine.runRenderLoop(new ActionCallback(
() => Task.Run(() => _scene.render(true, false))
));
_engine = engine;
}
protected void HandleKeyDown(
KeyboardEventArgs args
)
{
Console.WriteLine(args.Key);
if (args.ShiftKey && args.CtrlKey && args.AltKey && args.Key.ToLower() == "i")
{
if (_scene.debugLayer.isVisible())
{
Console.WriteLine("Hello");
_scene.debugLayer.hide();
}
else
{
_scene.debugLayer.show();
}
}
}
}
}
Running the Project
To run the application all we have to do is run a standard .NET run command from the Command Line, or if your using an IDE the standard run actions will also work. If you are using Visual Studio 2019 Preview, as of writing this, you can Debug the application, and even introspect the BabylonJS generated instances, it will even pull the JavaScript values as if they were standard C#/.NET objects!
dotnet run
You should see something similar to the image below when the application is running successfully!