Setting Up Unity XR For ECS

Hello and welcome to the first article in the OpenFall series, this is a set of tutorials were we will be creating a VR Titanfall clone. In this one we will be setting up Unity XR, DOTS, and the new Input system so that in the next one we can get right into coding.

Packages

The first thing we need to do as always when starting a new project is to download all the packages we need, and since we want to use all the shiny new features we will need to download a lot of stuff.

XR

Using the package manager install the XR Plugin Management Package, then in the settings check off the devices that you want to support. If you want to support OpenVR devices you can find instructions on how to install the package in the first part of this tutorial, but don’t install the SteamVR plugin as we want to make our game cross-platform.

DOTS

First thing first, install the entities package as this is the base of all the other DOTS packages, if it complains about needing any other packages install those too. Next we want to install the DOTS Editor as it provides useful debugging information. Then so we can actually see stuff we need to add the Hybrid Renderer. Last but not least we also want to add Unity Physics package so that we can interact with physics.

URP

Install the Universal RP package from the package manager, after that all we need to do is create a new render pipeline asset and set it in the graphics settings:

This asset is where all the relevant graphics settings are kept, I suggest turning on soft shadows and upping the main light shadow resolution so that they are less pixely.

Input

Finally the last package we want is the new input system, simply labeled Input system in the package manager. Install it and then enable it by going to the player settings -> other settings -> Active Input Handling and set it to ether both or the new input system depending on what you want, I’m going pure new input.

Setting Up Our Example Scene

Now that everything is installed we can get to codding, The very first thing we want to do is set up a hybrid camera rig, to do this we are going to have two CameraRigs, one ECS based, the other GameObject based.

First lets set up the GameObject one, this is the one that will hold our camera and update the transforms of our controllers. Create an empty game object as the root, add a camera and another two empty game objects as children, finally, name the objects appropriately. The last thing we need to do is add a tracked pose driver to each of the child objects so that they follow the headset and controllers motion:

And just like that, we have a fully functional XR based camera rig, now we need to do is create it on the ECS side. Duplicate the GameObject rig and remove the tracked pose drivers and camera (Or create it from scratch, probably faster) then on the root object of the duplicated rig click the convert to entity button checkbox. Now these GameObjects will automatically be converted into entities at run time. Now all that’s left is to sync their positions with the GameObject rig.

Syncing the rigs

To sync the two rigs we are going to use a pair of scripts, one to get an entity reference and the other to update the entities position. Create two scripts, I’m naming them TransformLink and TransfomLinkEntity, after creating them we are going to set up the TransfomLink Script first:

using UnityEngine;
using Unity.Entities;
using Unity.Transforms;

public class TransformLink : MonoBehaviour
{
    public Entity target; //This is our target entity
    public SyncType syncType;

    private EntityManager entityManager;
    void Start()
    {
        //we need a reference to the EntityManager to interact
        //with entities
        entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;
    }
    //we need to sync twice, once so that the data is updated for ecs, the other so that we have the latest position for our rig.
    private void Update()
    {
        Sync();
    }

    private void LateUpdate()
    {
        Sync();
    }
    private void Sync()
    {
        switch (syncType)
        {
            case SyncType.EntityToThis:
                //set our position to the entities position, translation and 
                //rotation are local so we need to get it from the local to 
                //world matrix if we want global
                transform.position = entityManager.GetComponentData<LocalToWorld>(target).Position;
                transform.rotation = entityManager.GetComponentData<LocalToWorld>(target).Rotation;
                break;
            case SyncType.ThisToEntityLocal:
                //Set the entity to our local position and rotation
                entityManager.SetComponentData(target, new Translation() { Value = transform.localPosition });
                entityManager.SetComponentData(target, new Rotation() { Value = transform.localRotation });
                break;
        }
    }
    public enum SyncType //our sync types
    {
        EntityToThis,
        ThisToEntityLocal
    }
}

We add the above script to all of the game objects in the GameObject CameraRig, set the sync type of the root object to EntityToThis and the sync type of all the others to ThisToEntityLocal.

Now as you have probably noticed, the public Entity field doesn’t show up in the inspector so we need to make another script to set it.

using Unity.Entities;
using UnityEngine;

//We need to inherit from IConvertGameObjectToEntity so that when the
//GameObject is converted to an entity we can run some extra 
//conversion code
public class TransformLinkEntity : MonoBehaviour, IConvertGameObjectToEntity
{
    public TransformLink transformLink; //reference to the 
                                        //Transformlink we want
                                        //to link to
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        transformLink.target = entity;//set the entity we are converted
                                      //as the target of this 
                                      //TrasformLink
    }
}

All that’s left is to add this to all of the objects in the ECSCameraRig set their TransformLink references to the corresponding TransformLink on the GameObjectCameraRig and we should be done. Add some cubes to the ECSCameraRig’s controller objects so that you can see them and test it out, if all goes well you should now have a pure ECSCameraRig that we can use as a pure ECS object. At this point you should have something like this: (Cubes not included)

Input

Now to test out our input system lets set up a very simple movement system. First thing we need to do is set up our bindings, if you are familiar with the SteamVR Bindings system it’s like that, but not really. Step one, create an Input actions asset. I suggest putting it in a folder marked input to keep everything neat. To create it, right-click and press this hard to find button:

Now when you have this newly created asset selected you should see a large edit asset button in the inspector, press it. You should now see something like this, though without the bindings as you will have to add those:

To start off create an action map if you don’t already have one by pressing the plus next to the action maps header. Then name this map something like Pilot (since we are making a Titanfall game) or Player as these will be our main controls, remember that names are case sensitive and we will need them later. Next, create an action, name it something like Move or Movement. In the sidebar, you will see settings for that action we want this one to have an action type of Value and a Control Type of Vector 2. Now click the Plus button on the Action itself and select Add Binding. This tells the action where to get its data. In the sidebar set the path to the LeftHand XR Controller thumbstick or Left-Hand touchpad for the valve, or even better, create two bindings so it will work with both, you can add as many as you want. That’s all we need to do for now, I have a jump binding but we don’t need that yet.

Now the hard part about using the new input system with ECS is that it is largely event-based, and events don’t work with the ECS system, and while you can read the current state of variables it is messy so we are going to, as always it seems, come up with a hybrid solution.

The way I am going to handle input is by setting it up so that the events update a public static struct with all of our relevant values in it, this will make it super easy to check the values at any time, from any place. I’ve decided to put all this code into a game controller class:

using UnityEngine;
using UnityEngine.InputSystem;
using Unity.Mathematics;

public class GameController : MonoBehaviour
{
    public static GameController instance; //standard game controller 
                                           //instance
    public InputActionAsset input; // set this to the InputActionAsset 
                                   // we created earlier 
    public static PilotInput pilotInput; // this is the static struct
    
    void Start()
    {
        if(instance && instance != this) // standard singleton stuff
        {
            Destroy(this);
        }
        instance = this;

        //this sets up our events that update our input
        foreach(InputActionMap map in input.actionMaps) 
        {
            map.Enable(); // enable the map so it captures input
            switch (map.name) 
            {
                case "Pilot": //replace with your map name if needed
                    //once again replace the name if needed
                    map["Move"].performed += (InputAction.CallbackContext context) =>
                    {
                        //this code is run every time the event is 
                        //called, it updates our struct values
                        pilotInput.Movement = context.ReadValue<Vector2>();
                    };
                    //once again we aren't using this for anything yet
                    //and it will throw an error if you don't have 
                    //a jump action set up so delete it if you don't 
                    //want it
                    map["Jump"].performed += (InputAction.CallbackContext context) =>
                    {
                        pilotInput.Jump = (context.ReadValue<float>() < 0.5f);
                    };
                    break;
            }
        }

    }
    
    public struct PilotInput //Our struct to hold input data
    {
        public float2 Movement;
        public bool Jump;
    }
}

Add this script to an empty GameObject in your scene and set the input actions asset to the correct field any we are ready to do our first true DOTS programming.

Now there are a lot of good tutorials on what ECS is so I won’t go into that, my way to get good at something is to just do it so that is what we are going to do. This is just an example of how to use the input system with ecs, in the future, we aren’t going to bother with a component for input, we’ll just use a local copy of the struct.

First we need to create a component to store the input data so that our entities can access it:

using Unity.Entities;
using Unity.Mathematics;

[GenerateAuthoringComponent] //This basically auto-creates a script that 
//does the same thing as the TransformLinkEntity script we created before 
//but instead of setting a reference it adds this component to the entity 
//created from the game object it is placed on. Short answer, it makes it so 
//that we can add this component to a game object as easily as a regular 
//script
public struct PlayerInputData : IComponentData
{
    public float2 movement; 
    public bool jumping; 
}

Add the above component to the root of our entity camera rig the same way you would any other script, now we need to create a system to update our values:

using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;

public class PlayerInputSystem : SystemBase
{
    protected override void OnUpdate()
    {
        //We can run whatever code we want here so here is the place to 
        //get all of the varibles we need
        float2 movement = GameController.pilotInput.Movement;
        bool jumping = GameController.pilotInput.Jump;
        //create a job that runs on all the entities with a PlayerInputData Component (also make sure it is set as a ref so that we can edit it)
        JobHandle inputJob = Entities.ForEach((ref PlayerInputData input)=>
        {
            //we can only use job safe code in here
            input.movement = movement;
            input.jumping = jumping;
        }).Schedule(inputDeps);
        inputJob.Complete();//make sure the job is completed

    }
}

The above script is automatically run so you don’t need to worry about adding it to an object, It goes through all of the entities with an input component and updates the values so that we can use them later.

Movement

Now to test everything above let’s write a simple movement script, first let’s create a small component to store our speed:

using Unity.Entities;

[GenerateAuthoringComponent]
public struct SimpleMoveData : IComponentData
{
    public float speed;
}

Now I said it was to store our speed but it serves two purposes, the second is so that our movement system knows what entities to effect. Add this to the root of our Entity CameraRig and set the speed to something other than 0. Now to create the system:

using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;

public class SimpleMoveSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = Time.DeltaTime;
        Entities.ForEach((ref Translation translation, in SimpleMoveData simpleMove, in PlayerInputData input) =>
        {
            translation.Value += new float3(simpleMove.speed * input.movement.x, 0, simpleMove.speed * input.movement.y) * deltaTime;
        }).Run();

    }
}

This simply moves the player on two planes and should look something like this:


That’s all for this one, hope you learned something. The next one should come out at 2 EST next Friday.

Liked it? Take a second to support WireWhiz on Patreon!
Become a patron at Patreon!