Inputs, ECS and Conversion workflow

Hello,

I know the new input system started around the same time as ECS and I recall some post about the new input system not being complitely incompatible with ECS but at the same time not disgned for it.

So now that ECS and DOTS in general is taking shape (espacially with the convertion workflow IMO) is there any plan to revesit the integration of the input system with ECS/DOTS.

I know I can use the new Input system with the DOTS stack and I do so but I feel I’m using the bare minumum of what the new input system has to offer.
For exemple, I set up everything through code because the Player Input component dos not get converted.
I also can’t use the multiplayer capabilities of the input system because I have no easy way to process input event and affect distinctly entities.

Any way, seeing the direction the other aspect of the Unity engine are taking, I would hate to see this input system get “old” in comparison to others.

A bit of same question here what the optimal way of using it with dots.
Why not using it in DOTSSample?

I’m gonna look over the input in my game in following days, but my current setup (following is for using DOTS with multiplayer) is to generate the InputActionCollection class which with the +performed adds the input to a InputCollectingSystem running in default world. It resets each frame and provides a static method to get the input for the frame, and a Reset method to reset some input that should not be used twice if multiple simulation frames come after each other. So a system in the client simulation world gets the input and from the InputCollectionSystem and reset it.

Considering testing of processing the events manually and see if this provides a cleaner setup.
Is it better to use the event performed, or should you read the values directly from the map for performance?

I came up with an idea but did not have time to test it yet.

Create an empty game object with :
-the player input component for the new input system
-the convert to entity set to convert and inject
-a c# script that implements IConvertGameObjectToEntity loking something like that

public class InputActionForwarder : MonoBehaviour, IConvertGameObjectToEntity
{
    public GameObject PlayerGameObject;
    private Entity PlayerEntity;
    private EntityManager EntityManager;

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        PlayerEntity = conversionSystem.GetPrimaryEntity(PlayerGameObject);
        EntityManager = dstManager;
    }

    public void ForwardInput(InputAction.CallbackContext ctx)
    {
        EntityManager.SetComponentData<JumpTrigger>(PlayerEntity, new JumpTrigger() { Value = ctx.ReadValue<bool>() });
    }

}

Then all you would have to do is set it up in the inspector to populate the PlayerGameObject and set the player input component to send a message to ForwardInput method.

Again, not tested…

I confirm that it works.
Seems an elegent way to bridge to the ECS world.

Yes that works. But also this works as well.

 public class ActorMovementSystem : JobComponentSystem
    {
        //Player Input Variables
        private PlayerInputActions playerInputActions;
        private float3 playerMoveInput;
        private bool bPlayerDoSprint;
        private bool bPlayerDoJump;

        //Crete And Bind Player Actions
        protected override void OnCreate()
        {
            //Create Player Input Actions
            playerInputActions = new PlayerInputActions();
            playerInputActions.Enable();

            //Bind Player Actions
            playerInputActions.PlayerActionMap.Move.performed += ctx => playerMoveInput = new float3(ctx.ReadValue<Vector2>().x, 0, ctx.ReadValue<Vector2>().y);
            playerInputActions.PlayerActionMap.Move.canceled += ctx => playerMoveInput = float3.zero;
            playerInputActions.PlayerActionMap.Sprint.performed += ctx => bPlayerDoSprint = true;
            playerInputActions.PlayerActionMap.Sprint.canceled += ctx => bPlayerDoSprint = false;
            playerInputActions.PlayerActionMap.Jump.started += ctx => bPlayerDoJump = true;
            playerInputActions.PlayerActionMap.Jump.canceled += ctx => bPlayerDoJump = false;
        }

        //Disable Player Actions
        protected override void OnDestroy()
        {
            playerInputActions.Disable();
        }

        //Movement Job
        [BurstCompile]
        struct ActorMovementSystemJob : IJobForEach<Translation, Rotation, PhysicsVelocity, PhysicsMass, ActorActionMovementComponent>
        {
            public float deltaTime;
            private float3 input;
            private float3 originalVelocity;
            private float3 newVelocity;
            private float3 movementDirection;
            private quaternion newRotation;

            public void Execute(ref Translation translation, ref Rotation rotation, ref PhysicsVelocity physicsVelocity, ref PhysicsMass physicsMass, ref ActorActionMovementComponent action)
            {
                //Read Input Once | Clear Out Input
                input = action.moveInput;
                if (input.x >= -0.25f && input.x <= 0.25f) input.x = 0;
                if (input.z >= -0.25f && input.z <= 0.25f) input.z = 0;

                //Update Sprint State
                {
                    //Cancel Out Sprint If Input Is Zero
                    if (input.x == 0 && input.z == 0)
                        action.bDoSprint = false;
                }

                //Jump
                {
                    action.bDidJustStartJump = false;
                    if (action.bDoJump && action.bIsGrounded)
                    {
                        physicsVelocity.Linear.y = action.jumpForce;
                        action.bDidJustStartJump = true;
                    }
                }
      
                //Movement
                if (action.bIsGrounded && physicsVelocity.Linear.y <= 0.1f)
                {
                    originalVelocity = physicsVelocity.Linear;
                    newVelocity = originalVelocity;

                    //Read Movement Input
                    newVelocity.x = input.x;
                    newVelocity.z = input.z;

                    //Multiply Our Velocity By Our Movement Characters Speed
                    newVelocity *= action.walkRunSpeed;
                    if (action.bDoSprint)
                        newVelocity = math.normalizesafe(newVelocity, float3.zero) * action.sprintSpeed;

                    //Apply Back Our Original Velocity Y
                    newVelocity.y = originalVelocity.y;

                    //Set Linar Physics Velocity
                    physicsVelocity.Linear = newVelocity;
                }

                //Rotation
                if (action.bIsGrounded && physicsVelocity.Linear.y <= 0.1f)
                {
                    //Rotate Twords Movement Direction
                    if (input.x != 0 || input.z != 0)
                    {
                        movementDirection = math.normalize(input);
                        movementDirection.y = 0;
                        newRotation = quaternion.LookRotation(input, new float3(0, 1, 0));
                        rotation.Value = math.nlerp(rotation.Value, newRotation, action.rotationSpeed * 10 * deltaTime);
                    }

                    //Constrain Physics Rotation So We Don't Fall Over
                    physicsMass.InverseInertia = float3.zero;
                }
            }
        }

        //Player Input Job
        [BurstCompile]
        [RequireComponentTag(typeof(ActorPlayerComponentTag))]
        struct ActorMovementPlayerInputSystemJob : IJobForEach<ActorActionMovementComponent>
        {
            public float3 moveInput;
            public bool bDoSprint;
            public bool bDoJump;

            public void Execute(ref ActorActionMovementComponent actionMovementComponent)
            {
                //Set Inputs
                actionMovementComponent.moveInput = moveInput;
                actionMovementComponent.bDoSprint = bDoSprint;
                actionMovementComponent.bDoJump = bDoJump;
            }
        }

        //Create And Schedule Jobs
        protected override JobHandle OnUpdate(JobHandle inputDependencies)
        {
            //Player Input Job
            JobHandle inputJobHandle;
            {
                //Get Camrea Vector Properties
                var cameraForward = (Camera.main.transform.TransformDirection(Vector3.forward).normalized);
                cameraForward.y = 0;
                var cameraRight = new Vector3(cameraForward.z, 0, -cameraForward.x);

                //Normalize Camera Forward And Right
                cameraForward.Normalize();
                cameraRight.Normalize();

                //Get New Input DONT CHANGE playerMoveInput HERE OR INPUT WONT WORK PROPERLY
                var newInput = playerMoveInput.x * cameraRight + playerMoveInput.z * cameraForward;

                //Scheduale Job
                var playerInputJob = new ActorMovementPlayerInputSystemJob() { moveInput = newInput, bDoSprint = bPlayerDoSprint, bDoJump = bPlayerDoJump };
                inputJobHandle = playerInputJob.Schedule(this, inputDependencies);

                //Cancel Jump Input
                bPlayerDoJump = false;
            }

            //Movement Job
            var movementJob = new ActorMovementSystemJob();
            movementJob.deltaTime = Time.DeltaTime;
            return movementJob.Schedule(this, inputJobHandle);
        }
    }
}
2 Likes

Yes my first try was similar.
The adventage is you can control multiple entitites with one input. (updating my sugestion with a list of entittes would probably do the trick)
On the other and, it will be complicated to setup local multiplayer I think.
That’s already a lot of code for this basic behaviour and not very extensible, to add any other input, you’ll have to enable/disable each one individually.

EDIT : Support to control multiple entities at once is done see repo :wink:

If you want to have a look at my inplementation feel free to take a peek at GitHub - WAYN-Games/MGM: Repository containing exploratory samples of the Unity preview packages (DOTS and InputSystem).

Seems like changing to manual update and using it in ClientSimulationGroup works fine.

Haven’t bitpacked yet, but I think it’s nicer than using events overall.

using TripleG.ClientAndServer;
using Unity.Entities;
using Unity.Jobs;
using Unity.NetCode;
using UnityEngine;


namespace TripleG.Client
{
    [AlwaysUpdateSystem]
    [UpdateInGroup(typeof(ClientSimulationSystemGroup))]
    public class InputSystem : JobComponentSystem
    {
        InputSetup m_inputSetup;
        private GhostPredictionSystemGroup m_predictionGroup;
     
        protected override void OnCreate()
        {
            base.OnCreate();
            m_inputSetup = new InputSetup();
            m_inputSetup.Enable();
         
            m_predictionGroup = World.GetOrCreateSystem<GhostPredictionSystemGroup>();
        }

        protected override void OnDestroy()
        {
            base.OnDestroy();
            m_inputSetup.Disable();
        }

        protected override JobHandle OnUpdate(JobHandle inputDeps)
        {
            UnityEngine.InputSystem.InputSystem.Update();
         
            var inputFromEntity = GetBufferFromEntity<PlayerCommandData>();
            var targetTick = m_predictionGroup.PredictingTick;
            var frameInput = new FrameInput();
         
            frameInput.Jump = (byte)m_inputSetup.Player.Jump.ReadValue<float>();
            frameInput.Pickup = (byte)m_inputSetup.Player.Pickup.ReadValue<float>();

            frameInput.AimActionOne = (byte)m_inputSetup.Player.UseOrInteract.ReadValue<float>();
         
            frameInput.Action1 = (byte)m_inputSetup.Player.ActionA.ReadValue<float>();
            frameInput.Action2 = (byte)m_inputSetup.Player.ActionB.ReadValue<float>();
            frameInput.Action3 = (byte)m_inputSetup.Player.ActionC.ReadValue<float>();

            frameInput.playerMovementAxis = m_inputSetup.Player.Move.ReadValue<Vector2>();
         
            frameInput.playerRotationAxis = m_inputSetup.Player.Look.ReadValue<Vector2>();
         
            frameInput.spaceshipHorizontalMovementAxis = m_inputSetup.Spaceship.MoveHorizontal.ReadValue<Vector2>();
            frameInput.spaceshipVerticalMovementAxis = m_inputSetup.Spaceship.MoveVertical.ReadValue<float>();

            frameInput.spaceshipJawRotation = m_inputSetup.Spaceship.RotateYaw.ReadValue<float>();
            frameInput.spaceshipRollRotation = m_inputSetup.Spaceship.RotateRoll.ReadValue<float>();
            frameInput.spaceshipPitchRotation = m_inputSetup.Spaceship.RotatePitch.ReadValue<float>();

            inputDeps = Entities.WithReadOnly(frameInput).WithNativeDisableParallelForRestriction(inputFromEntity).ForEach((in CommandTargetComponent commandTarget) =>
            {
                if (commandTarget.targetEntity != Entity.Null)
                {
                    if (inputFromEntity.Exists(commandTarget.targetEntity))
                    {
                        // Get buffer array from entity and add new tick
                        var input = inputFromEntity[commandTarget.targetEntity];
                        input.AddCommandData(new PlayerCommandData
                        {
                            tick = targetTick,
                            frameInput = frameInput
                        });
                    }
                }
            }).Schedule(inputDeps);

            return inputDeps;
        }
    }
}
1 Like