8/17/2020 Admin

Introducing Blazor Automatic Kingdom


image

The Blazor application, Automatic Kingdom, is designed to allow users to create programs, using visual blocks, to control 3D objects and animation.

The code in this article is based on the 00.00.01 Alpha Release. To run the code, you will need Visual Studio Studio 2019 Preview (or higher) with .Net Core 5.

The application is built using the following technology stack:

  • Blockly – A JavaScript program that allows you to create block-based visual programming languages. Widely used in programs such as MIT’s Scratch 3.0 and Microsoft’s MakeCode, this provides a visual tool to allow the end user to control the intent of the final program using a visual design.
  • IronBlockBlockly automatically generates XML, JavaScript, Python, PHP, Lua, and Dart, but not C#. IronBlock converts the XML output to C#.
  • CS-Script.Core – While Iron Block creates valid C#, executing that C# code, inside a Blazor application, is actually quite complicated. The CS-Script library provides that important functionality.
  • EventHorizon.Blazor.Babylon.js – The ultimate goal is to display rich 3D graphics and animation using Babylon.js. However, Babylon.js is a JavaScript library. The EventHorizen library allows Babylon.js to be programmed using C# code.
  • Babylon.js – This library provides a powerful 3D graphics engine to display the final result.

 

Walk Thru

image

You can download and run the code, or use it at: https://automatickingdom.com.

image

The toolbox provides access to the blocks, including custom blocks, that allow you to define program execution logic on the main design screen.

image

In the current implementation, a input WorldObject that consists of either “Idle”, “Attacked”, or “Calm” will be passed to the code block, and a AnimationLoopResult value, consisting of either “Idle”, “Attack”, or “Walk” needs to be passed as the output.

image

Clicking the C# Code button displays the current logic as C# code.

image

Clicking the Run Code button displays an Avatar using Babylon.js.

image

You can select a value to input to the code, and view the result in the Babylon.js animation.

Blockly

Blockly is implemented in the Blazor Server application by using a Div with a @ref to the @blocklyDiv variable:

 

<div style="width: 100%">
    <div @ref="@blocklyDiv" style="display: inline-block; height: 480px; width: 100%"></div>
</div>

 

When the .razor control loads, the following code runs:

 

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await AutomaticKingdomInterop.DemoWorkspace(
                JSRuntime,
                blocklyDiv,
                toolbox,
                startBlocks
                );
        }
    }

 

This calls the following JavaScript Interop code that calls the JavaScript that creates the Blockly workspace:

 

        internal static ValueTask<object> DemoWorkspace(
            IJSRuntime jsRuntime,
            ElementReference blocklyDiv,
            ElementReference toolbox,
            ElementReference startBlocks)
        {
            return jsRuntime.InvokeAsync<object>(
                "BlocklyFunctions.createDemoWorkspace",
                blocklyDiv,
                toolbox,
                startBlocks);
        }

 

The JavaScript that actually creates the Blockly workspace, and passes the result back to the Div, is contained in the _Host.cshtml file:

 

        window.BlocklyFunctions = {
            createDemoWorkspace: function (blocklyDiv, toolbox, startBlocks) {
                demoWorkspace = Blockly.inject(blocklyDiv,
                    {
                        media: 'media/',
                        toolbox: toolbox
                    });
                Blockly.Xml.domToWorkspace(startBlocks,
                    demoWorkspace);
            }

 

Note: You can use the Blockly Developer Tools to create custom Blockly blocks and the custom toolbox.

 

IronBlock

IronBlock takes the XML created by Blockly, that is retrieved using this line of code:

 

XMLText = await AutomaticKingdomInterop.GetXML(JSRuntime);

… and turns it into C# code:

 

            var parser = new Parser()
                .AddStandardBlocks()
                .Parse(XMLText);
            var syntaxTree = parser.Generate();
            string code = syntaxTree.NormalizeWhitespace().ToFullString();
            // Get C# Code
            var script = GenerateScript(code);

 

CS-Script.Core

The C# code, produced by IronBlock, needs to be adjusted (for example to add required imports and namespaces). We do that using the ConvertScript method:

 

    public static string ConvertScript(string code)
    {
        string FinalCode = "";
        // Remove existing opening bracket
        FinalCode = code.Substring(1, (code.Length - 1));
        string StartingCode =
            @"using System.Collections.Generic;
                using System.Linq;
                public class Script
                {";
        // Add a class around existing code
        FinalCode = StartingCode + FinalCode;
        // Make AnimationLoop Public
        FinalCode = FinalCode.Replace(
            "dynamic AnimationLoop(dynamic WorldObject)", 
            "public dynamic AnimationLoop(dynamic WorldObject)");
        return FinalCode;
    }

 

CS-Script allows that code to loaded and executed:

 

        // Load the code
        dynamic CsScript = CSScript.Evaluator.LoadCode(ConvertScript(code));
        // Call the AnimationLoop methods, passing it the WorldObject
        var result = CsScript.AnimationLoop(WorldObject);

 

The result variable represents the output of the C# code.

 

EventHorizon.Blazor.BabylonJS

We now need to display the animation in Babylon.js.

EventHorizon.Blazor.BabylonJS allows us to use C# code to easily do that.

 

First, a method set up the scene:

 

        public async ValueTask CreateScene()
        {
            var canvas = await Canvas.GetElementById(
                "game-window"
            );
            var engine = await Engine.NewEngine(
                canvas,
                true
            );
            var scene = await Scene.NewScene(
                engine
            );
            var light0 = await PointLight.NewPointLight(
                "Omni",
                await Vector3.NewVector3(
                    0,
                    100,
                    8
                ),
                scene
            );
            var light1 = await HemisphericLight.NewHemisphericLight(
                "HemisphericLight",
                await Vector3.NewVector3(
                    0,
                    100,
                    8
                ),
                scene
            );
            var Player = await SceneLoader.ImportMesh(
                null,
                "assets/",
                "Player.glb",
                scene,
                new EventHorizon.Blazor.Server.Interop.Callbacks.ActionCallback<
                    AbstractMesh[], IParticleSystem[], Skeleton[], AnimationGroup[]>(
                    async (arg1, arg2, arg3, arg4) =>
                {
                    foreach (var animation in arg4)
                    {
                        await animation.stop();
                        _animationMap.Add(await animation.get_name(), animation);
                    }
                    if (_animationMap.Count > 0)
                    {
                        _runningAnimation = _animationMap.First().Value;
                        await _runningAnimation.start(true);
                    }
                })
            );
            var camera = await ArcRotateCamera.NewArcRotateCamera(
                "ArcRotateCamera",
                (decimal)(System.Math.PI / 2),
                (decimal)(System.Math.PI / 4),
                3,
                await Vector3.NewVector3(0, 1, 0),
                scene
            );
            await camera.set_lowerRadiusLimit(2);
            await camera.set_upperRadiusLimit(10);
            await camera.set_wheelDeltaPercentage(0.01m);
            await scene.set_activeCamera(camera);
            await camera.attachControl(
                canvas,
                false
            );
            await engine.runRenderLoop(() => Task
            .Run(() => scene.render(true, false)));
            _engine = engine;
        }

Then another method allows the animations to be triggered:

 

        public async ValueTask RunAnimation(string name)
        {
            if (_runningAnimation != null)
            {
                await _runningAnimation.stop();
                _runningAnimation = null;
            }
            if (_animationMap.ContainsKey(name))
            {
                await _animationMap[name].play(true);
                _runningAnimation = _animationMap[name];
            }
        }

 

Future plans for Automatic Kingdom

  • Multiplayer applications
  • Allowing custom blocks to be created in the application

 

Links

AutomaticKingdom.com

AutomaticKingdom Github (00.00.01 Alpha Release)

Implement WorldObject #4

https://github.com/richorama/IronBlock

https://github.com/canhorn/EventHorizon.Blazor.TypeScript.Interop.Generator

Blockly

www.babylonjs.com

An error has occurred. This application may no longer respond until reloaded. Reload 🗙