Input System calling additional times after loading new scenes

For some reason when I call actions in a new scene it will call an extra time, which can cause a pair of errors like this:

MissingReferenceException while executing ‘canceled’ callbacks of ‘Combat/Aim[/DualShock4GamepadHID/leftTrigger]’

and

MissingReferenceException: The object of type ‘PlayerControls’ has been destroyed but you are still trying to access it.

When I then go back into the previous scene (or any new scene I imagine) and try to do the action again, I get another pair, so in my 2nd scene I get 4, 3rd I get 6, etc.

So it’s calling an additional time every time new scene is loaded and I’m not sure how to fix this.
I’m on the latest as of now (2021.1.0f1)

Any ideas or help is much appreciated!!

Do you use an event scheme? You probably don’t unregister your events. If you show us your code we can tell you more. If you decide to do so, please use the code formatting option in the post editor toolbar.

I’m not entirely sure that it means to “unregister” them. I thought that meant disabling them, but that doesn’t fix it.

Here’s my code. I marked the line it highlights when this particular error happens when I “un-aim” and go back to a normal rotation. Line 212.

public class PlayerControls : MonoBehaviour
{
    //player actions
    public PlayerControlInputs playerControls;
    public Combat combatScript;
    private PlayerStats playerStatScript;
    public float deadzone;
    private Rigidbody player;
    public float turnSmoothTime;
    private float turnSmoothVelocity;
    private Vector2 moveDirection;
    public bool isRunning;
    public float currentSpeed;
    public float runSpeed;
    public float strifeSpeed;
    public float walkSpeed;


    //main camera
    public Transform camera;
    public Transform cameraAnchor;
    public Transform focalPoint;
    private Vector2 lookDirection;
    public float lookSensitivity;
    public float minimumX;
    public float maximumX;
    private float horizontal;
    private float vertical;
    public float rotationSpeed;

    //Occlusion
    public Transform desiredCameraPosition;


    //Aiming camera
    private Vector3 standardCamLocation;
    public Vector3 playerModelRotation;
    public bool isAiming;
    public bool hasWeaponEquipped;
    public Transform aimingTransform;
    public Transform playerModel;

    public float reorientProgress;
    public Quaternion currentPlayerRotation;
    public float reorientSpeed;
    public GameObject reticle;
    private Quaternion targetAimRotation;
    private bool isUnaiming;

    //AdditionalControls
    //UI
    public GameObject startMenu;


    //quickturn
    private float quickturnProgress = 1;
    private float quickturnStartingValue;


    private void Awake()
    {
        playerControls = new PlayerControlInputs();
        player = this.gameObject.GetComponent<Rigidbody>();
        playerStatScript = gameObject.GetComponent<PlayerStats>();
        combatScript = this.gameObject.GetComponent<Combat>();

        playerControls.General.Move.performed += context => moveDirection = context.ReadValue<Vector2>();
        playerControls.General.Look.performed += context => lookDirection = context.ReadValue<Vector2>();
        playerControls.General.Run.started += context => ToggleRun();
        playerControls.General.Quickturn.performed += context => Quickturn();
        playerControls.Combat.Aim.performed += context => Aim();
        playerControls.Combat.Aim.canceled += context => Revert();
        playerControls.Combat.Reload.performed += ContextMenu => Reload();
        playerControls.UI.Pause.performed += context => Pause();

        playerModelRotation = playerModel.localEulerAngles;
        standardCamLocation = camera.transform.localPosition;
        camera.transform.position = desiredCameraPosition.position;

    }

    private void OnEnable()
    {
        playerControls.Enable();
    }

    void Pause()
    {
        startMenu.SetActive(true);
    }

    public void ToggleRun()
    {
        if (isRunning)
        {
            isRunning = false;
            currentSpeed = walkSpeed;
        }

        else if (isRunning == false)
        {
            isRunning = true;
            currentSpeed = runSpeed;
        }

        else
        {
            currentSpeed = walkSpeed;
            isRunning = false;
        }
   
    }
    private void FixedUpdate()
    {
   
        if (isAiming && playerStatScript.isBeingParalyzed != true)
        {
            // Debug.Log("walking");
            float h = moveDirection.x;
            float v = moveDirection.y;
            Vector3 dir = new Vector3(h, 0, v).normalized;

            if (dir.magnitude > deadzone)
            {
                currentSpeed = strifeSpeed;
                float targetAngle = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg + camera.eulerAngles.y;
                Vector3 moveDirection = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward;
                player.velocity = moveDirection.normalized * currentSpeed *Time.deltaTime;             
            }
            else player.velocity = Vector3.zero;
        }

        else if(isAiming != true && playerStatScript.isBeingParalyzed != true)
        {
            float h = moveDirection.x;
            float v = moveDirection.y;
            Vector3 dir = new Vector3(h, 0, v).normalized;

            if (dir.magnitude > deadzone)
            {
                float targetAngle = Mathf.Atan2(dir.x, dir.z) * Mathf.Rad2Deg + camera.eulerAngles.y;
                float angle = Mathf.SmoothDampAngle(transform.eulerAngles.y, targetAngle, ref turnSmoothVelocity, turnSmoothTime);
                transform.rotation = Quaternion.Euler(0f, angle, 0f);
                Vector3 moveDirection = Quaternion.Euler(0f, targetAngle, 0f) * Vector3.forward;
                player.velocity = moveDirection.normalized * currentSpeed * Time.deltaTime;
            }

            else
            {
                player.velocity = Vector3.zero;
            }

        }

        if (walkSpeed <= 75)
        {
            walkSpeed = 75;
        }

        if(runSpeed <=75)
        {
            runSpeed = 75;
        }

        if(strifeSpeed <= 75)
        {
            strifeSpeed = 75;
        }

    }

    void Reload()
    {
        if(isAiming)
        {
            combatScript.Reload();
        }
    }

    void Quickturn()
    {
        quickturnStartingValue = horizontal;
        quickturnProgress = 0;

    }

    void Aim()
    {
        if(hasWeaponEquipped)
        {
            //Debug.Log("setting rotation");
            standardCamLocation = camera.transform.localPosition;
            isAiming = true;
            isRunning = false;
            currentPlayerRotation = transform.rotation;
            reticle.SetActive(true);
        }

    }


    void Revert()
    {
        playerControls.Combat.Aim.Enable();
        isAiming = false;
        isUnaiming = true;
        currentSpeed = walkSpeed;
        if(reticle != null)
        {
            reticle.SetActive(false); // comes up as null for soem reason in new room at first?
        }
        currentPlayerRotation = transform.localRotation; //<---- this is where the null reference points to. If I get rid of this, the error doesn't happen for "PlayerControls"

    }

    private void Update()
    {
        if (isAiming)
        {
            if (reorientProgress < 1)
            {
                reorientProgress += Time.deltaTime * reorientSpeed;
                transform.rotation = Quaternion.Slerp(currentPlayerRotation, targetAimRotation, reorientProgress);            
                camera.transform.localPosition = Vector3.Lerp(standardCamLocation, aimingTransform.localPosition, reorientProgress);

            }

            else if(reorientProgress >= 1)
            {
                transform.rotation = targetAimRotation;
                reorientProgress = 1;
            }
        }

        else if(isUnaiming)
        {
            if (reorientProgress > 0)
            {

                reorientProgress -= Time.deltaTime * reorientSpeed;
                //turns camera
                transform.rotation = Quaternion.Slerp(Quaternion.Euler(0, targetAimRotation.eulerAngles.y, targetAimRotation.eulerAngles.z), currentPlayerRotation, reorientProgress);
                //turns player
                camera.transform.localPosition = Vector3.Lerp(standardCamLocation, aimingTransform.localPosition, reorientProgress);

            }

            else if (reorientProgress <= 0)
            {
                reorientProgress = 0;
                isUnaiming = false;
            }
        }

        if(quickturnProgress < 1)
        {
            quickturnProgress += Time.deltaTime * reorientSpeed;
            horizontal = Mathf.Lerp(quickturnStartingValue, quickturnStartingValue + 180, quickturnProgress);
            transform.rotation = Quaternion.Euler(transform.rotation.eulerAngles.x, horizontal, transform.rotation.z);
        }

    }
    private void LateUpdate()
    {
        CamControl();
        CameraOcclusionandCollisionDetection();
    }

    void CamControl()
    {
        if(playerControls.General.Look.enabled)
        {
            horizontal += lookDirection.x * rotationSpeed * Time.deltaTime;
            vertical -= lookDirection.y * rotationSpeed * Time.deltaTime;
            vertical = Mathf.Clamp(vertical, minimumX, maximumX);
            camera.transform.LookAt(focalPoint);
            cameraAnchor.rotation = Quaternion.Euler(vertical, horizontal, 0);
            if (isAiming)
            {
                targetAimRotation = Quaternion.Euler(vertical, horizontal, 0);

            }
        }
     
    }

    void CameraOcclusionandCollisionDetection()
    {
        RaycastHit hit;

        if (Physics.Linecast(cameraAnchor.transform.position, desiredCameraPosition.position, out hit))
        {
            if (hit.collider.gameObject.tag != "Player")
            {
                camera.transform.position = hit.point;
            }

            if (camera.transform.position != desiredCameraPosition.position)
            {
                RaycastHit occludedHit;

                if (Physics.Linecast(desiredCameraPosition.position, cameraAnchor.position, out occludedHit))
                {
                    if (occludedHit.transform.gameObject.tag == "Player")
                    {
                        return;
                    }
                }

            }
        }

        else if (isAiming != true)
        {
            camera.transform.position = desiredCameraPosition.position;
        }

    }

    private void OnDisable()
    {
      playerControls.Disable();
    }
}

Do you make this object “DontDestroyOnLoad”? Because what you’re experiencing looks like you are double-registering.
This thing: playerControls.General.Move.performed += called event registration. You need to unregister when you are done. I usually advise against leaving like this even if you need it for the entire application lifespan. It is just bad habit. So what you can do is to unregister from these events [whatever event].performed -= pattern. It is important to unregister the same methods what you have registered. And I advise that way because if you change your mind later and destroy the object early, you’re leaking memory.
More info here:
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/events/how-to-subscribe-to-and-unsubscribe-from-events

2 Likes

Yeah its on the Player which is not destroyed on load.

So to unregister, I just put the same thing but -= and right before I load the scene or do I put it also in the variable space?

Thanks for your help! I’ve never used delegates before so learning the new input system has been a journey!

I usually register onEnable and unregister on onDisable. Also make sure you don’t create another player object when you load the next scene.

1 Like

Yup that’s what the issue was. I feel silly. I had the player prefab in every room and forgot to check for and destroy copies.

Plus I de-registered.

Thanks so much!!!

1 Like

Sorry but, do you manage to use one player input component per scene? I struggle to understand what happen when you have to spread this component into multiple scenes.

As i know from the documentation: 1 player input component = 1 user = 1 different control scheme

If i had one player input component per scene, i still have this issue?

Apologies for reviving a thread that’s over a year old, but since the issue wasn’t fully resolved and hasn’t been addressed in this manner before, I wanted to share a solution for those who might still be encountering this problem or stumble upon this post in search of answers.

The persistent event subscriptions, despite efforts to unsubscribe, particularly in the context of Unity’s scene management and C#'s handling of lambda expressions for event subscription and unsubscription, are due to lambda expressions creating a new delegate instance each time they are used. Therefore, attempting to unsubscribe using a lambda expression does not match the delegate instance you originally subscribed with, leaving the original subscription active.

To resolve this, it’s essential to use a consistent delegate instance for both subscribing and unsubscribing from events. This consistency can be achieved by either defining methods directly or using actions as intermediary variables. Here’s how you can adjust your code to ensure proper subscription management and why you should use OnEnable() and OnDisable() for this purpose:

Avoid subscribing with lambda expressions like this:

private void Awake()
{
    playerControls.Combat.Reload.performed += context => Reload();
}

private void Reload()
{
    // Reload code here
}

Instead, subscribe by directly referencing a method in OnEnable() and unsubscribe in OnDisable():

private void OnEnable()
{
    playerControls.Combat.Reload.performed += Reload;
}

private void OnDisable()
{
    playerControls.Combat.Reload.performed -= Reload;
}

private void Reload(InputAction.CallbackContext context)
{
    // Reload code here
}

Using OnEnable() for subscriptions and OnDisable() for unsubscriptions is crucial because Awake() is only called when the script instance is loaded, which doesn’t account for objects being enabled or disabled throughout the scene’s lifecycle. OnEnable() is called every time the object becomes active in the scene, making it the ideal place to set up event subscriptions. Similarly, OnDisable() is called when the object becomes inactive, allowing you to clean up subscriptions and prevent actions from being triggered when they shouldn’t be, such as after an object is destroyed or a scene is unloaded.

This approach ensures your event handlers are correctly managed, avoiding issues such as duplicate event firing and enhancing the robustness of your event handling logic, especially in complex Unity projects with multiple scene loads and object state changes.

1 Like

Thank you this is super useful! How would you handle a method subscribing/unsubscribing to an event that has different parameters than that event without using a Lamada expression?

I’m currently having a problem with a static event that has a few methods subscribed to that don’t all follow the parameters of this event. When reloading the scene the event is referencing destroyed objects due to methods subscribing through a Lamada expression, but how heck are those methods supposed to subscribe to this static method?

Sorry for only replying now, I didn’t see my notifications. If you’re still facing the problem you explained here, would you be able to provide some code examples so I can take a look?