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 
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