• Series
  • Blazor
  • BabylonJS
  • C#
  • .NET

BabylonJS and Blazor - Character Movement Part 1

For this article we will go over Character Movement (Part 1) as it was implemented on Blazor in C#, this article will be a long one where we go over setting up the Player input tracking and basic movement logic.

Checkout BabylonJS and Blazor - Character Movement Part 2, the next step in the series!

Demo and Source Code

Below is a running demo of the application in its finished state for Step 5, you can also open the demo in a new tab by going to BabylonJS Blazor Step 05.

Use the Arrow keys to move the around the map.

You can see the full source code in GitHub: canhorn/BabylonJS.Blazor.Game.Tutorial at step/05_Character-Movement-Part-1

Checkout the original step in the Series: Character Movement Part 1 | Babylon.js Documentation (babylonjs.com)

Implementation Overview

The Character Movement is broken into two steps in this Guided Learning, but the work done on the re-implementation mixed the two movement steps together in some areas. Part 1 was all about getting the base in place, tracking arrow key input and basic movement logic for the Player. A major part of the implementation was in the creation of the PlayerInput, encapsulating the keyboard checking logic, in tracking Pressed/Released state of the Arrow Keys.

The Player Controller hooks into the render loop of the game, and reads the PlayerInput setting the movement states onto the Player Mesh. The rotation of the mesh is also updated to the direction the Player moves. Movement logic for the player uses the built-in collision checking logic of BabylonJS, this which will come more into play in Character Movement Part 2 of the series where Gravity, Dash and Jump will be implemented.

The PlayerInput relies on the OnKeyDownTrigger and OnKeyUpTrigger events registered on the ActionManager of the Scene. The logic for the PlayerInput will track the Arrow Keys for Up/Down/Left/Right with a true/false if it was pressed/released. This state will then be checked by the Player logic every render frame, causing movement or actions to be done based on the current state of the PlayerInput at the time of the render frame.

Problem Area(s)

The Player's mesh would move inexpertly at one point during the implementation, moving as the camera was sliding further than it should. This turned out to be the case, the camera was moving based on the player by the PlayerInput and the internal camera movement logic. The UniversalCamera, when attached to the scene, has internal movement bound to the arrow keys. So the Player logic for the camera removed the attachment of the camera to the scene and fully managed by the PlayerInput by the state of the arrow keys.

Another problem area was with the ActionManager not capturing input to the Canvas on the first setup. This caused was issues only on first load and focus of the page, leading to the keyboard actions to not trigger the callbacks. I could not figure out exactly what caused the ActionManager not to wire up the keyboard events but to remedy the issue a blur then focus trigger during the game load logic fixed it.

Changes to Generator Project

This series show cases the Blazor TypeScript Interop Generator project and with that in mind a major enhancement was necessary to accommodate a feature the Game needed. This feature was the ability to return a value back to the JavaScript when an Action was called. During the ground check of the Player there is a time where the logic needs to know if a mesh is intersected by a Ray. This Ray will check meshes for their isPickable and isEnabled status, another problem is that it will check all meshes each render frame. This will be performance tuned in a future article, so be on the lookout for that it's a good one.

Checkout the Generator Project: canhorn/EventHorizon.Blazor.TypeScript.Interop.Generator

Source Code Example(s)

Below is the FloorRaycast code block in the Player.cs file, this snippet shows off the new feature of ActionResultCallback. The ActionResultCallback in this use case is a predicate that will call into the C# code from JavaScript. By using this pattern we have the ability to check each mesh if a ray intersects with a valid mesh. This gives a lot of flexibility with other types of coding patterns, encapsulating the logic of a predicate to callback into the C#/.NET code less complex.

// Client/Pages/Game/Player.cs:FloorRaycast
private Vector3 FloorRaycast(decimal offsetX, decimal offsetZ, decimal raycastLength)
{
    var raycastFloorPosition = new Vector3(
        _mesh.position.x + offsetX,
        _mesh.position.y - PLAYER_OFFSET,
        _mesh.position.z + offsetZ
    );
    var ray = new Ray(
        raycastFloorPosition,
        Vector3.Up().scale(-1),
        raycastLength
    );

    var pick = _scene.pickWithRay(
        ray,
        // Here is a new feature added to the Generator Library for this Tutorial.
        // Here we can see that we have an ActionResultCallback that can pass a bool value to the calling JavaScript
        // In this case we are checking if a Ray intersects with a Pickable and Enabled Mesh in the scene
        new ActionResultCallback<AbstractMesh, bool>(
            mesh => mesh.isPickable && mesh.isEnabled()
        )
    );

    if (pick.hit)
    {
        return pick.pickedPoint;
    }

    return Vector3.Zero();
}

Below is the SetupPlayerCamera code block in the Player.cs file, this is interesting because you can see how a complex set of objects can be used to create a flexible camera. The use of a TransformNode helps to give easier Pivot points and less complex logic with out having the overhead an empty mesh or using a pivot matrix.

// Client/Pages/Game/Player.cs:SetupPlayerCamera
private void SetupPlayerCamera()
{
    var cameraRoot = new TransformNode(
        "root",
        _scene,
        true
    );
    _cameraRoot = cameraRoot;
    cameraRoot.position = new Vector3(0, 0, 0);
    cameraRoot.rotation = new Vector3(0, (decimal)Math.PI, 0);

    var yTilt = new TransformNode(
        "ytilt",
        _scene,
        true
    );
    _yTilt = yTilt;
    yTilt.rotation = ORIGINAL_TILT;
    yTilt.parent = _cameraRoot;

    var camera = new UniversalCamera(
        "cam",
        new Vector3(0, 0, -30),
        _scene
    );
    _camera = camera;
    camera.lockedTarget = cameraRoot.position;
    camera.fov = 0.47m;
    camera.parent = _yTilt;

    _scene.activeCamera = camera;
}
// Client/Pages/Game/PlayerInput.cs
public class PlayerInput
{
    private Dictionary<string, bool> _inputMap;

    public decimal Horizontal { get; set; }
    public decimal Vertical { get; set; }
    public decimal HorizontalAxis { get; set; }
    public decimal VerticalAxis { get; set; }

    public PlayerInput(
        Scene scene
    )
    {
        // Here we create a new ActionManager on the Scene
        // This allows for use to have a fresh set of actions registered.
        scene.actionManager = new ActionManager(scene);

        _inputMap = new Dictionary<string, bool>
        {
            ["ArrowUp"] = false,
            ["ArrowDown"] = false,
            ["ArrowLeft"] = false,
            ["ArrowRight"] = false,
        };
        // Here we register our action callback for OnKeyDown
        // When the Player presses any key this will callback from BabylonJS to our passed in ActionCallbac,
        scene.actionManager.registerAction(
            new ExecuteCodeAction(
                ActionManager.OnKeyDownTrigger,
                // This ActionCallback expected one argument to be passed up to it.
                // The Argument is of type ActionEvent that gives us access to the key pressed and type of key down event.
                new ActionCallback<ActionEvent>(
                    args =>
                    {
                        var key = GetKeyFromSourceEvent(args.sourceEvent);
                        var type = GetTypeFromSourceEvent(args.sourceEvent);
                        _inputMap[key] = type == "keydown";
                        return Task.CompletedTask;
                    }
                )
            )
        );

        // This is the same logic as above, but will be triggered OnKeyUp events to the Action Manager
        scene.actionManager.registerAction(
            new ExecuteCodeAction(
                ActionManager.OnKeyUpTrigger,
                new ActionCallback<ActionEvent>(
                    args =>
                    {
                        var key = GetKeyFromSourceEvent(args.sourceEvent);
                        var type = GetTypeFromSourceEvent(args.sourceEvent);
                        _inputMap[key] = type == "keydown";
                        return Task.CompletedTask;
                    }
                )
            )
        );

        // This is another callback event provided by the Interop Library
        // This will process the keys from the ActionManager and turn them into public state on the PlayerInput object.
        scene.onBeforeRenderObservable.add((scene, state) =>
        {
            UpdateFromKeyboard();
            return Task.CompletedTask;
        });
    }
}

Cody's logo image, it is an abstract of a black hole with a white Event Horizon.

Cody Merritt Anhorn

A Engineer with a passion for Platform Architecture and Tool Development.