Search Unity

Best Way to Handle Player Input?

Discussion in 'Entity Component System' started by aeldron, Feb 28, 2019.

  1. aeldron

    aeldron

    Joined:
    Feb 12, 2013
    Posts:
    32
    Hello.

    I'm wondering what would be the best way to handle complex player input in an ECS architecture.

    All the examples I found so far work more or less in as follow:
    1. They have a single Player entity with a PlayerInputComponent, which keeps a bool for a mouseClick, and a float2 for mousePosition

    2. A PlayerInputSystem which processes Unity's Input.GetMouseButtonDown(0), etc and enters the data on all PlayerInputComponents it finds (in this case only a single entity).

    3. Other systems filtering and iterating over entities with the PlayerInputComponent and transforming data, e.g. PlayerMoveForwardSystem, PlayerAnimationSystem, etc.
    This approach works well when you have a single player entity, or at least only a few entities controlled by player input data. Now imagine we have a few hundred thousand entities depending on player input, with this approach we would be duplicating the data hundreds of thousands of times by attaching the PlayerInputComponent to all those entities. The data would be identical, as it's processed by the PlayerInputSystem once per frame.

    In a typical OOP approach we would have a single object containing all the player input data, and probably an event system dispatching the data to all the gameObjects listening to it.

    I was tempted to use ISharedComponentData, but the documentation says: "SharedComponentData should change rarely. Changing a SharedComponentData involves using memcpy to copy all ComponentData for that Entity into a different Chunk."

    Since we will be changing the PlayerInputComponent every frame, I suppose I should avoid making it a SharedComponentData?

    Is there a better way to do this without duplicating data?
     
  2. NoDumbQuestion

    NoDumbQuestion

    Joined:
    Nov 10, 2017
    Posts:
    186
    Input normally is for 1 player. You dont need ComponentSystem for that.
    Just have a normal Input MonoBehavior and write data to static class.

    Then your SystemComponent dont have to process ComponentGroup and only read data from static class.

    Unity demo sample is for teaching people how to use ECS. You dont have to put everything to ComponentSystem.
     
  3. tertle

    tertle

    Joined:
    Jan 25, 2011
    Posts:
    3,761
    Seems cleaner to me to use a ComponentSystem than a static class.

    Doesn't have to be complex. I've been meaning to make this more dynamic (for bindings etc) but here's a very simple example. At this time my project only has 1 player input so I can use the SetSingleton api, but it's easy enough to set up multiple players if that's what you have.

    Code (CSharp):
    1.     using Unity.Entities;
    2.     using Unity.Mathematics;
    3.     using UnityEngine.Experimental.Input;
    4.     using UnityEngine.Experimental.PlayerLoop;
    5.  
    6.     /// <summary>
    7.     /// The input system.
    8.     /// </summary>
    9.     [UpdateBefore(typeof(Update))]
    10.     public class InputSystem : ComponentSystem
    11.     {
    12.         /// <inheritdoc/>
    13.         protected override void OnUpdate()
    14.         {
    15.             var keyboard = Keyboard.current;
    16.             var mouse = Mouse.current;
    17.  
    18.             var x = (keyboard.aKey.isPressed ? -1 : 0) + (keyboard.dKey.isPressed ? 1 : 0);
    19.             var y = (keyboard.sKey.isPressed ? -1 : 0) + (keyboard.wKey.isPressed ? 1 : 0);
    20.  
    21.             this.SetSingleton(new InputValues
    22.             {
    23.                 Pointer = mouse.delta.ReadValue(),
    24.                 Scroll = mouse.scroll.y.ReadValue(),
    25.                 Movement = new float2(x, y),
    26.                 PrimaryDown = mouse.leftButton.wasPressedThisFrame,
    27.                 Primary = mouse.leftButton.isPressed,
    28.                 PrimaryUp = mouse.leftButton.wasReleasedThisFrame,
    29.             });
    30.         }
    31.     }
    This way I can directly use it in a job.

    Code (CSharp):
    1.         [BurstCompile]
    2.         private struct SelectionJob : IJobProcessComponentData<InputValues, Selection>
    3.         {
    4.             public NativeQueue<EntitySelected>.Concurrent EntitySelected;
    5.  
    6.             public bool IsSelected;
    7.  
    8.             /// <inheritdoc />
    9.             public void Execute([ReadOnly] ref InputValues input, [ReadOnly] ref Selection selection)
    10.             {
    11.                 if (!input.PrimaryDown || this.IsSelected)
    12.                 {
    13.                     return;
    14.                 }
    15.  
    16.                 this.EntitySelected.Enqueue(new EntitySelected { Entity = selection.Over });
    17.             }
    18.         }
     
    Last edited: Feb 28, 2019
    deus0, BitPax, Jack-Mariani and 4 others like this.
  4. aeldron

    aeldron

    Joined:
    Feb 12, 2013
    Posts:
    32
    Thanks tertle, that's very useful. How do you set the InputValues data on an entity? Does the SetSingleton method create an entity for you? Or do you still need to use
    EntityManager.SetComponentData(entity, inputValues);
     
  5. GilCat

    GilCat

    Joined:
    Sep 21, 2013
    Posts:
    676
    Here you can know more about Singleton usage.
    But basically you create one entity with the component once somewhere you want (I do it OnCreateManager of the system) and the just call SetSingleton/GetSingleton/HasSingleton form any system.

    Code (CSharp):
    1.     EntityManager.CreateEntity(typeof(InputValues));
    2.     SetSingleton(new InputValues());
     
    Last edited: Feb 28, 2019
    aeldron likes this.
  6. Srokaaa

    Srokaaa

    Joined:
    Sep 18, 2018
    Posts:
    169
    Hmmm, I usually denormalize such data. I have a ComponentData that holds whatever input can be useful and have a Job that writes this to every entity which archetype has such component. Then I can just use this data whenever I need it. Remember that writing data to memory is mostly free, reading from memory is expensive so it's better to have data always "at hand"
     
    NotaNaN likes this.
  7. aeldron

    aeldron

    Joined:
    Feb 12, 2013
    Posts:
    32
    I'm developing a system to handle input from VR controllers, this is what I came up with so far:
    1. An XRControllerState component to store data for the left and right controller. The component is generic enough so it can store the state for either Oculus or Vive controllers
    2. An OculusControllerInputSystem and a SteamVRControllerSystem to process the controller input data and pass it on to the xrController entities.
    3. I can then develop systems dependent on the XRControllerState data.
    The system is similar to what @tertle suggested, except that instead of a singleton player input component, I have two controller entities to store my controllerState data.

    Code (CSharp):
    1. using System;
    2. using Unity.Entities;
    3. using UnityEngine.XR;
    4.  
    5. public class OculusControllerInputSystem : ComponentSystem
    6. {
    7.     private ComponentGroup m_controllerDataGroup;
    8.  
    9.     protected override void OnCreateManager()
    10.     {
    11.         if (XRSettings.loadedDeviceName != "Oculus")
    12.         {
    13.             this.Enabled = false;
    14.             return;
    15.         }
    16.  
    17.  
    18.         var controllerArchetype = EntityManager.CreateArchetype(typeof(XRControllerState));
    19.         var leftController = EntityManager.CreateEntity(controllerArchetype);
    20.         EntityManager.SetComponentData(leftController, new XRControllerState { Controller = XRController.Left });
    21.         var rightController = EntityManager.CreateEntity(controllerArchetype);
    22.         EntityManager.SetComponentData(rightController, new XRControllerState{Controller = XRController.Right});
    23.  
    24.         m_controllerDataGroup = GetComponentGroup(typeof(XRControllerState));
    25.     }
    26.  
    27.     protected override void OnUpdate()
    28.     {
    29.         if (!XRSettings.enabled) return;
    30.  
    31.         var controllerState = m_controllerDataGroup.GetComponentDataArray<XRControllerState>();
    32.  
    33.         for (int i = 0; i < controllerState.Length; i++)
    34.         {
    35.             var oculusController = (controllerState[i].Controller == XRController.Left) ?
    36.                 OVRInput.Controller.LTouch : OVRInput.Controller.RTouch;
    37.  
    38.             // Can't use jobs here. Interaction with the Oculus SDK API needs to happen at the main thread.
    39.             controllerState[i] = new XRControllerState
    40.             {
    41.                 Controller = controllerState[i].Controller,
    42.                 ButtonOneWasPressedThisFrame = Convert.ToInt32(OVRInput.GetDown(OVRInput.Button.One, oculusController)),
    43.                 ButtonOneIsPressed = Convert.ToInt32(OVRInput.Get(OVRInput.Button.One, oculusController)),
    44.                 ButtonOneWasReleasedThisFrame = Convert.ToInt32(OVRInput.GetUp(OVRInput.Button.One, oculusController)),
    45.                 ButtonTwoWasPressedThisFrame = Convert.ToInt32(OVRInput.GetDown(OVRInput.Button.Two, oculusController)),
    46.                 ButtonTwoIsPressed = Convert.ToInt32(OVRInput.Get(OVRInput.Button.Two, oculusController)),
    47.                 ButtonTwoWasReleasedThisFrame = Convert.ToInt32(OVRInput.GetUp(OVRInput.Button.Two, oculusController)),
    48.                 TriggerWasPressedThisFrame = Convert.ToInt32(OVRInput.GetDown(OVRInput.Button.PrimaryIndexTrigger, oculusController)),
    49.                 TriggerIsPressed = Convert.ToInt32(OVRInput.Get(OVRInput.Button.PrimaryIndexTrigger, oculusController)),
    50.                 TriggerWasReleasedThisFrame = Convert.ToInt32(OVRInput.GetUp(OVRInput.Button.PrimaryIndexTrigger, oculusController)),
    51.                 GripWasPressedThisFrame = Convert.ToInt32(OVRInput.GetDown(OVRInput.Button.PrimaryHandTrigger, oculusController)),
    52.                 GripIsPressed = Convert.ToInt32(OVRInput.Get(OVRInput.Button.PrimaryHandTrigger, oculusController)),
    53.                 GripWasReleasedThisFrame = Convert.ToInt32(OVRInput.GetUp(OVRInput.Button.PrimaryHandTrigger, oculusController)),
    54.                 StartButtonWasPressedThisFrame = Convert.ToInt32(OVRInput.GetDown(OVRInput.Button.Start, oculusController)),
    55.                 StartButtonIsPressed = Convert.ToInt32(OVRInput.Get(OVRInput.Button.Start, oculusController)),
    56.                 StartButtonWasReleasedThisFrame = Convert.ToInt32(OVRInput.GetUp(OVRInput.Button.Start, oculusController)),
    57.                 ThumbstickWasPressedThisFrame = Convert.ToInt32(OVRInput.GetDown(OVRInput.Button.PrimaryThumbstick, oculusController)),
    58.                 ThumbstickIsPressed = Convert.ToInt32(OVRInput.Get(OVRInput.Button.PrimaryThumbstick, oculusController)),
    59.                 ThumbstickWasReleasedThisFrame = Convert.ToInt32(OVRInput.GetUp(OVRInput.Button.PrimaryThumbstick, oculusController)),
    60.                 TriggerSqueeze = OVRInput.Get(OVRInput.Axis1D.PrimaryIndexTrigger, oculusController),
    61.                 GripSqueeze = OVRInput.Get(OVRInput.Axis1D.PrimaryHandTrigger, oculusController),
    62.                 ThumbstickAxis = OVRInput.Get(OVRInput.Axis2D.PrimaryThumbstick, oculusController),
    63.                 Position = OVRInput.GetLocalControllerPosition(oculusController),
    64.                 Rotation = OVRInput.GetLocalControllerRotation(oculusController)
    65.             };
    66.         }
    67.     }
    68. }
    And here's an example of a system with dependency on the XRControllerState entities. I am grabbing only the data that the system depends on to avoid passing the whole XRControllerState data, but I'm not sure if this is the best approach.

    Code (CSharp):
    1. using Genesis.Unity.Toolkit.Components;
    2. using Unity.Burst;
    3. using Unity.Collections;
    4. using Unity.Entities;
    5. using Unity.Jobs;
    6. using Unity.Mathematics;
    7. using Unity.Transforms;
    8.  
    9. class OculusControllerMovementSystem : JobComponentSystem
    10. {
    11.     [BurstCompile]
    12.     struct OculusControllerMovementJob : IJobProcessComponentData<OculusController, Position, Rotation>
    13.     {
    14.  
    15.         [ReadOnly]
    16.         [DeallocateOnJobCompletion]
    17.         public NativeArray<ControllerMovement> ControllerMovementArray;
    18.  
    19.         public void Execute(
    20.             [ReadOnly] ref OculusController controllerType,
    21.             ref Position position,
    22.             ref Rotation rotation)
    23.         {
    24.             for (int i = 0; i < ControllerMovementArray.Length; i++)
    25.             {
    26.                 if (ControllerMovementArray[i].Controller == controllerType.Value)
    27.                 {
    28.                     position = new Position
    29.                     {
    30.                         Value = ControllerMovementArray[i].Position
    31.                     };
    32.  
    33.                     rotation = new Rotation
    34.                     {
    35.                         Value = ControllerMovementArray[i].Rotation
    36.                     };
    37.                 }
    38.             }
    39.         }
    40.     }
    41.  
    42.     struct ControllerMovement
    43.     {
    44.         public XRController Controller;
    45.         public float3 Position;
    46.         public quaternion Rotation;
    47.     }
    48.  
    49.     protected ComponentGroup m_controllerStateGroup;
    50.  
    51.     protected override void OnCreateManager()
    52.     {
    53.         m_controllerStateGroup = GetComponentGroup(typeof(XRControllerState));
    54.     }
    55.  
    56.     // OnUpdate runs on the main thread.
    57.     protected override JobHandle OnUpdate(JobHandle inputDeps)
    58.     {
    59.         var controllerStates = m_controllerStateGroup.GetComponentDataArray<XRControllerState>();
    60.         var controllerMovementJob = new OculusControllerMovementJob();
    61.         controllerMovementJob.ControllerMovementArray =
    62.             new NativeArray<ControllerMovement>(controllerStates.Length, Allocator.TempJob);
    63.  
    64.         for (int i = 0; i < controllerStates.Length; i++)
    65.         {
    66.             controllerMovementJob.ControllerMovementArray[i] = new ControllerMovement
    67.             {
    68.                 Controller = controllerStates[i].Controller,
    69.                 Position = controllerStates[i].Position,
    70.                 Rotation = controllerStates[i].Rotation
    71.             };
    72.         }
    73.  
    74.         return controllerMovementJob.Schedule(this, inputDeps);
    75.     }
    76. }
     
    NotaNaN likes this.