New Input System in Online Multiplayer forces keyboard for player 0 and gamepad for player 1?

Hey,

I want to connect with two players to an online multiplayer session. Each player should be able to use keyboard or gamepad controls and be able to switch dynamically.

Setup
I am using the New Input System (1.11.1) and the First Person Controller from the Unity Starter Assets package [1] (1.3). Further, I use Netcode for GameObjects (2.1.1) and the Multiplayer Widgets (1.0.0) to create and join a lobby.

What works
I tested functionality using the multiplayer play mode in the editor as well as with stand-alone builds using two computers. Creating and joining a session works as expected and both players spawn a character respectively. Further, when only one player is in the session, then the input system works as expected and both, keyboard and gamepad controls work and I can dynamically switch between the input devices.

The Problem
However, when joining the session with the second player, then input devices are forced such that player 0 is assigned the keyboard and player 1 is assigned the gamepad control scheme. This is even the case when testing on two computers and even when the computer of player 1 has no gamepad.

More Details
I was already debugging a lot, so here is more info. The First Person Controller from the Unity Starter Assets package comes with two scripts as well as the player input component:

Player Input Component
I use the default action map (and only one exists). The ‘Default Scheme’ is set to <Any>. Setting it to KeyboardMouse does not resolve the problem and player 1 is still mapped to gamepad according to the editor’s inspector during play mode.

StarterAssetsInputs
Implements listeners for input events. This is a MonoBehaviour script. Added some events but basically unchanged from my side. Based on my debug logs, this script seems to work as expected.
Namely, in multiplayer play mode, when two players are in the session and the window of player 0 is active: Pressing inputs on the keyboard or the gamepad leads to a debug log in the input listener only for player 0 and NOT for player 1. The same is true in the other direction.

using System;
using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif

namespace StarterAssets
{
	public class StarterAssetsInputs : MonoBehaviour
	{
		[Header("Character Input Values")]
		public Vector2 move;
		public Vector2 look;
		public bool jump;
		public bool sprint;
		public bool tab_menu = false;

		[Header("Movement Settings")]
		public bool analogMovement;

		[Header("Mouse Cursor Settings")]
		public bool cursorLocked = true;
		public bool cursorInputForLook = true;

#if ENABLE_INPUT_SYSTEM
        public void OnMove(InputValue value)
		{
            Debug.Log("StarterAssetsInputs - move input received");

			MoveInput(value.Get<Vector2>());
		}

        public void OnLook(InputValue value)
		{
			if(cursorInputForLook)
			{
				LookInput(value.Get<Vector2>());
			}
		}

		public void OnJump(InputValue value)
		{
            JumpInput(value.isPressed);
		}

		public void OnSprint(InputValue value)
		{
			SprintInput(value.isPressed);
		}
#endif
		public void OnTabMenu(InputValue value)
		{
            TabMenuInput(value.isPressed);
        }

        public void MoveInput(Vector2 newMoveDirection)
		{
			move = newMoveDirection;
		} 

		public void LookInput(Vector2 newLookDirection)
		{
			look = newLookDirection;
		}

		public void JumpInput(bool newJumpState)
		{
            jump = newJumpState;
        }

        public void SprintInput(bool newSprintState)
		{
			sprint = newSprintState;
		}
		
		public void TabMenuInput(bool newTabMenuState)
		{
			tab_menu = newTabMenuState;
		}

		private void OnApplicationFocus(bool hasFocus)
		{
			SetCursorState(cursorLocked);
		}

		private void SetCursorState(bool newState)
		{
			Cursor.lockState = newState ? CursorLockMode.Locked : CursorLockMode.None;
		}
	}
}

FirstPersonController
Changed to a NetworkBehaviour script (which I think it has to be). Added the following wherever applicable:

        if (!IsOwner) return;

In the Update() method after the IsOwner check, only inputs from the ‘forced’ input device are received. I.e., in Update(), player 0 only receives inputs from the ‘forced’ keyboard input, while player 1 only receives inputs from the ‘forced’ gamepad input.
This implies, that the listener events are somehow not received in the first person controller?

using MimicSpace;
using Unity.Netcode;
using UnityEngine;
#if ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
using UnityEngine.Windows;
#endif

namespace StarterAssets
{
	[RequireComponent(typeof(CharacterController))]
#if ENABLE_INPUT_SYSTEM
	[RequireComponent(typeof(PlayerInput))]
#endif
	public class FirstPersonController : NetworkBehaviour
	{
		[Header("Player")]
		[Tooltip("Move speed of the character in m/s")]
		public float MoveSpeed = 4.0f;
		[Tooltip("Sprint speed of the character in m/s")]
		public float SprintSpeed = 6.0f;
		[Tooltip("Rotation speed of the character")]
		public float RotationSpeed = 1.0f;
		[Tooltip("Acceleration and deceleration")]
		public float SpeedChangeRate = 10.0f;

		[Space(10)]
		[Tooltip("The height the player can jump")]
		public float JumpHeight = 1.2f;
		[Tooltip("The character uses its own gravity value. The engine default is -9.81f")]
		public float Gravity = -15.0f;

		[Space(10)]
		[Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]
		public float JumpTimeout = 0.1f;
		[Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]
		public float FallTimeout = 0.15f;

		[Header("Player Grounded")]
		[Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]
		public bool Grounded = true;
		[Tooltip("Useful for rough ground")]
		public float GroundedOffset = -0.14f;
		[Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]
		public float GroundedRadius = 0.5f;
		[Tooltip("What layers the character uses as ground")]
		public LayerMask GroundLayers;

		[Header("Cinemachine")]

		[Tooltip("The follow target set in the Cinemachine Virtual Camera that the camera will follow")]
        private GameObject _cinemachineCameraTarget;

        [Tooltip("How far in degrees can you move the camera up")]
		public float TopClamp = 90.0f;
		[Tooltip("How far in degrees can you move the camera down")]
		public float BottomClamp = -90.0f;

		// cinemachine
		private float _cinemachineTargetPitch;

		// player
		private float _speed;
		private float _rotationVelocity;
		private float _verticalVelocity;
		private float _terminalVelocity = 53.0f;

		private Vector2 _prevAnimMovement = new Vector2(0, 0);
		private float _animLerpCoef = 10f;

		// timeout deltatime
		private float _jumpTimeoutDelta;
		private float _fallTimeoutDelta;

		// connection ui
		private bool _tab_menu_shown;
	
#if ENABLE_INPUT_SYSTEM
		private PlayerInput _playerInput;
#endif
		private CharacterController _controller;
		private StarterAssetsInputs _input;
		private GameObject _mainCamera;
        private GameObject _connection_ui;
		private Animator _animator;

        private const float _threshold = 0.01f;

		private bool IsCurrentDeviceMouse
		{
			get
			{
#if ENABLE_INPUT_SYSTEM
				return _playerInput.currentControlScheme == "KeyboardMouse";
#else
				return false;
#endif
			}
		}

		/*
        public override void OnNetworkSpawn()
        {
            if (!IsOwner)
            {
                this.enabled = false;
            }
        }
		*/

        private void Awake()
		{
            if (!IsOwner) return;
            
			// get a reference to our main camera
            if (_mainCamera == null)
			{
				_mainCamera = GameObject.FindGameObjectWithTag("MainCamera");
			}
		}

		private void Start()
		{
            // NOTE IsOwner needs to be checked here and it needs to be a NetworkBehavior script, otherwise, the connection_ui is shared among players!
            if (!IsOwner) return;

			if (_connection_ui == null)
			{
	            _connection_ui = GameObject.FindGameObjectWithTag("ConnectionUI");
				_tab_menu_shown = false;
				_connection_ui.SetActive(_tab_menu_shown);
            }

            _controller = GetComponent<CharacterController>();
			_input = GetComponent<StarterAssetsInputs>();
#if ENABLE_INPUT_SYSTEM
			_playerInput = GetComponent<PlayerInput>();
#else
			Debug.LogError( "Starter Assets package is missing dependencies. Please use Tools/Starter Assets/Reinstall Dependencies to fix it");
#endif

            // add cinemachine camera target
            _cinemachineCameraTarget = GameObject.FindGameObjectWithTag("CinemachineTarget");

            // add animator
            _animator = GetComponent<Animator>();

            // reset our timeouts on start
            _jumpTimeoutDelta = JumpTimeout;
			_fallTimeoutDelta = FallTimeout;
		}

		private void Update()
		{
			if (!IsOwner) return;

			Debug.Log("ID " + OwnerClientId + " " + _playerInput.currentControlScheme);

            JumpAndGravity();
			GroundedCheck();
			Move();

			MoveAnimation();
            ToggleTabMenu();
        }

		private void LateUpdate()
		{
            if (!IsOwner) return;

            CameraRotation();
		}

		private void GroundedCheck()
		{
			// set sphere position, with offset
			Vector3 spherePosition = new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z);
			Grounded = Physics.CheckSphere(spherePosition, GroundedRadius, GroundLayers, QueryTriggerInteraction.Ignore);
		}

		private void CameraRotation()
		{
			// if there is an input
			if (_input.look.sqrMagnitude >= _threshold)
			{
				//Don't multiply mouse input by Time.deltaTime
				float deltaTimeMultiplier = IsCurrentDeviceMouse ? 1.0f : Time.deltaTime;

				_cinemachineTargetPitch += _input.look.y * RotationSpeed * deltaTimeMultiplier;
				_rotationVelocity = _input.look.x * RotationSpeed * deltaTimeMultiplier;

				// clamp our pitch rotation
				_cinemachineTargetPitch = ClampAngle(_cinemachineTargetPitch, BottomClamp, TopClamp);

                // Update Cinemachine camera target pitch
                _cinemachineCameraTarget.transform.localRotation = Quaternion.Euler(_cinemachineTargetPitch, 0.0f, 0.0f);

                // rotate the player left and right
                transform.Rotate(Vector3.up * _rotationVelocity);
			}
		}

		private void Move()
		{
			// set target speed based on move speed, sprint speed and if sprint is pressed
			float targetSpeed = _input.sprint ? SprintSpeed : MoveSpeed;

			// a simplistic acceleration and deceleration designed to be easy to remove, replace, or iterate upon

			// note: Vector2's == operator uses approximation so is not floating point error prone, and is cheaper than magnitude
			// if there is no input, set the target speed to 0
			if (_input.move == Vector2.zero) targetSpeed = 0.0f;

			// a reference to the players current horizontal velocity
			float currentHorizontalSpeed = new Vector3(_controller.velocity.x, 0.0f, _controller.velocity.z).magnitude;

			float speedOffset = 0.1f;
			float inputMagnitude = _input.analogMovement ? _input.move.magnitude : 1f;

			// accelerate or decelerate to target speed
			if (currentHorizontalSpeed < targetSpeed - speedOffset || currentHorizontalSpeed > targetSpeed + speedOffset)
			{
				// creates curved result rather than a linear one giving a more organic speed change
				// note T in Lerp is clamped, so we don't need to clamp our speed
				_speed = Mathf.Lerp(currentHorizontalSpeed, targetSpeed * inputMagnitude, Time.deltaTime * SpeedChangeRate);

				// round speed to 3 decimal places
				_speed = Mathf.Round(_speed * 1000f) / 1000f;
			}
			else
			{
				_speed = targetSpeed;
			}

			// normalise input direction
			Vector3 inputDirection = new Vector3(_input.move.x, 0.0f, _input.move.y).normalized;

			// note: Vector2's != operator uses approximation so is not floating point error prone, and is cheaper than magnitude
			// if there is a move input rotate player when the player is moving
			if (_input.move != Vector2.zero)
			{
				// move
				inputDirection = transform.right * _input.move.x + transform.forward * _input.move.y;
			}

			Vector3 movement = inputDirection.normalized * Mathf.Min(inputDirection.magnitude, 1.0f);

            // move the player
            _controller.Move(movement * (_speed * Time.deltaTime) + new Vector3(0.0f, _verticalVelocity, 0.0f) * Time.deltaTime);
        }

        private void MoveAnimation()
		{
            // move animation

            Vector2 curAnimMovement = new Vector2(_input.move.x, _input.move.y);
            curAnimMovement = curAnimMovement.normalized * Mathf.Min(curAnimMovement.magnitude, 1.0f);

            Vector2 lerpAnimMovement = Vector2.Lerp(_prevAnimMovement, curAnimMovement, _animLerpCoef * Time.deltaTime);

            _animator.SetFloat("animMoveX", lerpAnimMovement.x);
            _animator.SetFloat("animMoveY", lerpAnimMovement.y);

            _prevAnimMovement = lerpAnimMovement;
        }

        private void JumpAndGravity()
		{
			if (Grounded)
			{
				// reset the fall timeout timer
				_fallTimeoutDelta = FallTimeout;

				// stop our velocity dropping infinitely when grounded
				if (_verticalVelocity < 0.0f)
				{
					_verticalVelocity = -2f;
				}

				// Jump
				if (_input.jump && _jumpTimeoutDelta <= 0.0f)
				{
					// the square root of H * -2 * G = how much velocity needed to reach desired height
					_verticalVelocity = Mathf.Sqrt(JumpHeight * -2f * Gravity);
				}

				// jump timeout
				if (_jumpTimeoutDelta >= 0.0f)
				{
					_jumpTimeoutDelta -= Time.deltaTime;
				}
			}
			else
			{
                // reset the jump timeout timer
                _jumpTimeoutDelta = JumpTimeout;

				// fall timeout
				if (_fallTimeoutDelta >= 0.0f)
				{
					_fallTimeoutDelta -= Time.deltaTime;
				}

				// if we are not grounded, do not jump
				_input.jump = false;
			}

			// apply gravity over time if under terminal (multiply by delta time twice to linearly speed up over time)
			if (_verticalVelocity < _terminalVelocity)
			{
				_verticalVelocity += Gravity * Time.deltaTime;
			}
		}

		private void ToggleTabMenu()
		{
			if (_input.tab_menu)
			{
				_input.tab_menu = false;  // only enter function once
                _tab_menu_shown = !_tab_menu_shown;
                _connection_ui.SetActive(_tab_menu_shown);
            }
        }

		private static float ClampAngle(float lfAngle, float lfMin, float lfMax)
		{
			if (lfAngle < -360f) lfAngle += 360f;
			if (lfAngle > 360f) lfAngle -= 360f;
			return Mathf.Clamp(lfAngle, lfMin, lfMax);
		}

		private void OnDrawGizmosSelected()
		{
			Color transparentGreen = new Color(0.0f, 1.0f, 0.0f, 0.35f);
			Color transparentRed = new Color(1.0f, 0.0f, 0.0f, 0.35f);

			if (Grounded) Gizmos.color = transparentGreen;
			else Gizmos.color = transparentRed;

			// when selected, draw a gizmo in the position of, and matching radius of, the grounded collider
			Gizmos.DrawSphere(new Vector3(transform.position.x, transform.position.y - GroundedOffset, transform.position.z), GroundedRadius);
		}
	}
}

Conclusion
I am probably making a stupid mistake, however, I could not find any information regarding this oddly specific problem. Is there some setting that I am overlooking? Am I wrong and the FirstPersonController should not be a NetworkBehaviour script? Is there any example/tutorial on first person controllers in a multiplayer setup that I overlooked?
I am happy to provide more information.

Thank you in advance.

[1] Starter Assets - FirstPerson | Updates in new CharacterController package | Essentials | Unity Asset Store

This should not be the case unless the device pairing is bound to online connectivity and tries to pair with incorrect devices.

Most likely, you are calling Input system methods when a client’s player object spawns without checking the IsOwner flag first to ensure that only the Owner modifies and uses the Input System. Or you obtain the wrong (“the other player’s”) reference to some other component the script uses.

Not sure where this pattern is coming from but its ripe with issues and commonly applied.

Rather than early-out for non-owners, create a specific “SomethingOwner” script that is only enabled for the owner. You would call enable = false in Awake and the script would still receive OnNetworkSpawn event, where you’d call enable = IsOwner to ensure all other event methods (besides OnEnable/Start) will not run for non-owners. Curiously, you have already implemented the OnNetworkSpawn part but commented it out.

This gives you the guarantee that whatever you do inside this Owner-only script, besides OnNetworkSpawn/Despawn will be owner-only. And it forces you to separate any shared code into its own component.

In general, and particularly for Multiplayer, please shy away from any sort of string-based indexing, like “finding” things by name or tag.

The latter actually has a built-in property: Camera.main
For everything else, use any other way but “find by string” to obtain references.

Well, the point to take away here is that if the controller - any character controller - was never designed to work in multiplayer ie with multiple instances in the scene, it’s better to start writing your own controller from scratch with multiplayer in mind and using the existing controller as a blueprint. Then you’ll know exactly at what point some odd behaviour occurs, and it’s easier to figure out why it does occur.

Do note that the FPS controller in this Starter Asset is woefully inadequate for even the simplest kind of game. It is overly simplistic in its behaviour and it’s hard to improve upon. On the plus side, it’s straightforward to deconstruct and rebuild this controller from scratch, it makes for a good learning experience.

Another thing to take away is that a character controller is not (should not be) tied to its visual representation. For input and networking to move the object, the point of view is completely irrelevant.

So if you can manage to make any object move with WASD and the thumbstick, and it picks the right devices for each player, and synchronizes movement on the network, you only have to add the Cinemachine first person camera setup (it’s a third person with all player offsets set to 0) and you got the same controller as the Starter Asset controller.

In any case, if you focus on just the input, then add networking, you can totally ignore the camera until after you got the first two working. The camera part is absolutely trivial to add later.

But … then you’ll be having fun figuring out how the first person weapon (aka “view model”) is not the weapon remote players see. Also having a third person player model for anyone but the first-person player. While the first-person player of course still needs to cast a shadow. And how to prevent the first person weapon from clipping into walls. And preventing the weapon from shooting through walls. Just to lay down what other tasks lie ahead of you. :wink:

The Starter Assets controller doesn’t provide any of that, however there’s good tutorials available for these things but you need to check for the modern solutions that work with URP/HDRP rather than the old tricks.

Thank you so much for your detailed answer.

I need to go through the scripts and the other components in more detail again when I have more time to see if I can find something. I tried one more thing which I found to be quite confusing.

As mentioned, keyboard inputs using the new input system only work for player 0 for some reason. Now, when using the legacy input system (definitely not planning to switch to it), the FirstPersonController (with the !IsOwner checks) works as expected. So, only the respective player jumps and the input works for either player correctly.

I.e., adding the following to StarterAssetsInputs:

public void Update()
{
    if (Input.GetKeyDown(KeyCode.J))        
    {
        JumpInput(true);
    }
}

Meanwhile, the New Input System (which is mapped to space to jump) only works for player 0, but not for player 1. For player 1, it only works when pressing jump on the gamepad.

Does that make any sense to you? I really don’t get why this is working. Does this imply that there is some faulty mapping of input devices to player ids? I almost can’t imagine it though, that sounds so odd.

It was used in many tutorials I found on youtube and in code snippets online.

Thanks for the clarification. I agree that it makes way more sense to disable non-owner objects in OnNetworkSpawn to never deal with irrelevant calls afterwards again.

Honestly, what is the intended solution to this? To make _connection_ui and _cinemachineCameraTarget into public variables and drag and drop the GameObjects in the inspector for initialization? Is this not ugly and error prone as well? Is there any better solution?

Thanks for all the other points you raised. I have one more question that arose regarding NetworkBehaviour vs MonoBehaviour scripts. Specifically, do I understand correctly that MonoBehaviour scripts run completely isolated on each client separately?
E.g., the input listeners in the StarterAssetsInputs should be player specific input events (synchronized movement is implemented in the FirstPersonController). I.e., input events by themselves do not need to be synchronized and should run completely isolated on each client. So, they should be MonoBehaviour scripts, right?

Thanks again, your answer was very insightful.

While I have no multiplayer experience, assigning references via the inspector the best way to reference anything if possible.

Otherwise if all the best practice methods are unavailable, you should always use FindObjectOfType<T> or similar, and reference objects via a component. There is generally never any reason to reference a game object over a component, as the component is always more useful.

Same with inspector references. Always reference via a component.

Much less so if you understand the limitations, eg a prefab can’t hold references to something outside of the prefab or anything in the scene. Those need to be referenced at runtime.

For more details why “find by string” is slow and dangerous, read this:

I also wouldn’t use any “Find” method for that matter. You really only need one central static instance that provides references. Objects add themselves to this registry in Awake, others get these references in Start. This avoids “Find” anything because find requires traversing the entire scene hierarchy, which will get slower as you add more things into the scene. The place of objects inside the scene may also matter.

Find is not transparent in how it actually finds something. And there’s no guarantee that there isn’t a second object with the same script, and thus you may find yourself on a long bughunt where everything seems in order until you realize you were working with the wrong script on the wrong object. This becomes even more pronounced in multiplayer because of automated spawning of networked objects.

This is a question of architecture. You can either process input only locally, and sync only the results of the input (movement, animation). Or you send the input state to the server (host) who then applies the input state - this however adds lag to every player’s input except the host.

Lastly, you can combine both. Move the client locally but also send the input to other clients so that they can make decisions, such as playing a jump animation with corresponding sound. This may be more efficient if input can be compressed to just a few bool flags combined into a byte or two, versus using the bloated NetworkAnimator which will sync everything in the Animator statemachine, including transition states - that’s totally overkill for most games.

I wish I knew. By default I believe it works that if you have only one player, that player can freely switch between K&M and Gamepad. But as soon as another player locally joins with the gamepad then the gamepad only works for the other player.

However you either need PlayerInputManager for this (maybe you added this by accident?) or specifically program it, eg calling some sort of Pair method of the Input system.

It could also be a bug so maybe checking for an input system upgrade and installing that is worthwhile.

I kinda guessed. I just wish those tutorial peeps would stop spreading broken patterns and start teaching proper coding since many many times the “easy” way isn’t actually that much easier. But yeah, nobody likes explaining exceptions and proper scene setup.

Thank you for the help and suggestions, I was able to resolve the issue. I’ll respond to messages before going into the solution.

Thank you for clarifying.

I did not think about that, that’s a nice solution. Thank you.

I double checked. There is/was no accidental PlayerInputManager anywhere. And I can’t imagine it is implemented in the default single player starter code I used.

Meanwhile, I also updated to 6000.0.25f1, but this did not resolve the issue.

The Solution:
In short, I implemented a minimal character controller from scratch using the new input system which works just fine. It works online for both players, and either player can dynamically switch between keyboard and gamepad controls. The working code:

using System;
using UnityEngine;
using UnityEngine.InputSystem;

public class CubePlayerNewInputSystem : MonoBehaviour
{
    private Rigidbody rigidbody;

    private void Awake()
    {
        rigidbody = GetComponent<Rigidbody>();
    }

    public void OnJump(InputValue value)
    {
        if (value.isPressed)
        {
            Debug.Log("Jump");
            rigidbody.AddForce(Vector3.up * 5f, ForceMode.Impulse);
        }
    }
}

I experimented with the PlayerInput components’ behaviour options as I expected the issue might arises from there (it does not). The code above uses the “Send Messages” setting. I also tested “Invoke Unity Events” as well as “Invoke C Sharp Events”. Just to be clear, I did not test “Broadcast Messages” but I would expect it to work since “Send Messages” also works. Anyways, I was able to get all 3 tested methods to work with the respective changes in the code/inspector.

In that regard, I was wondering: Is there any advantage of using “Invoke C Sharp Events” over “Send Messages”? If I understood it correctly, both are event listener methods. To me, “Invoke C Sharp Events” seems to be “Send Messages” with extra steps.

Debugging the original code:
The originally posted code also uses “Send Messages”. I invested some time into debugging that code aiming to identify where the issue is coming from. However, I couldn’t exactly pinpoint the issue. I think it has something to do with the communication between the StarterAssetsInputs and the FirstPersonController scripts. The FirstPersonController initializes the StarterAssetsInputs via GetComponent and then accesses some variables like a jump bool. So far, I could not identify any issue with that and I don’t plan to investigate that any further. Instead, I’ll write a character controller from scratch.

Thanks again

I ran in to the same issue. The solution for me to get the input system to work was to:

  1. Disable the input system on the player prefab
  2. Check IsOwner in OnNetworkSpawn override
  3. Enable input system if IsOwner
public override void OnNetworkSpawn()
{
    if(IsOwner)
    {
        _playerInput = GetComponent<PlayerInput>();
        _playerInput.enabled = true;
    }
}

Hopefully this helps someone with a similar problem.

1 Like

I’ve been stuck on this for hours. When I saw your comment I desperately decided to try it and it worked. Thank you very much for saving me from this trouble.