Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Question NullReferenceException after app reaload

Discussion in 'Input System' started by lostmekka, Aug 23, 2020.

  1. lostmekka

    lostmekka

    Joined:
    Aug 23, 2020
    Posts:
    1
    When I change something in the code while the game is running, Unity recompiles the code and the game continues to run with the new code. This however breaks when using the input system.

    Code (CSharp):
    1. public class Movement : MonoBehaviour {
    2.     private MyControls controls;
    3.  
    4.     private void Awake() {
    5.         controls = new MyControls();
    6.         controls.Enable();
    7.         // init callbacks
    8.     }
    9.  
    10.     private void Update() {
    11.         // use controls (some extra polling)
    12.     }
    13. }
    Because the MyControls instance cannot be serialized, it will be null when the game refreshes, leading to a NullReferenceException when the controls are used.

    I am assuming this would also occur, when loading savegames. (I did not implement that yet, but it is planned)

    My current workaround is to null-check the controls instance on every frame and re-initialize it when necessary, but this seems quite cumbersome:

    Code (CSharp):
    1. public class Movement : MonoBehaviour {
    2.     private MyControls controls;
    3.  
    4.     private void Awake() {
    5.         InitControls();
    6.     }
    7.  
    8.     private void InitControls() {
    9.         controls = new MyControls();
    10.         controls.Enable();
    11.         // init callbacks    
    12.     }
    13.  
    14.     private void Update() {
    15.         if (controls == null) InitControls();
    16.         // use controls (some extra polling)
    17.     }
    18.  
    19. }
    In my case it is even more cumbersome, because I have many entities that have controls and I need to switch between them. So I additionally need to keep track of the enabled flag of the input too.

    This seems like a problem many people run into, but I cannot find a simple solution to it. Is there a better solution for this?
     
    Last edited: Aug 23, 2020
  2. Sparot

    Sparot

    Joined:
    Aug 23, 2020
    Posts:
    9
    My knowledge with the new input system is still a bit rough, and I'm relatively new to the forums, so forgive me if this doesn't quite help.

    Part of the problem with repairing inputs and reattaching game objects each time is as you described, you're likely to run into NullExceptions, misspaired inputs, and other nasty things if you're creating and destroying your input classes on each scene or save game load.

    A reasonable method I've found to handle this, though I'm focused more on multiplayer, is to make your input classes global and disconnected from your movement classes. Using either unity events, serialized fields, or GameObject.Find, dynamically hook those inputs to your corresponding movement classes or whatever you might need to control in a given scene.

    Your inputs get initialized when the game starts under a singleton class(i.e. InputManager). Using DontDestroyOnLoad, your inputs will survive any scene loads, and will exist for the entire length of the game. Use action maps or enable/disable scripts to control what inputs your user is allowed to use at any given time.

    Code (CSharp):
    1.     class MyInputManager : MonoBehaviour
    2.     {
    3.         //If you need only one input. This is just a game object child below this class.
    4.         [SerializeField] MyControls MyPlayerInputChild;
    5.         //If you need several inputs. This is just a list of game object children below this class.
    6.         [SerializeField] MyControls[] MyPlayerInputChildren;
    7.  
    8.  
    9.         static MyInputManager t_mInstance = null;
    10.  
    11.         //Setup your input manager to be static.
    12.         void Awake()
    13.         {
    14.             if (t_mInstance == null)
    15.             {
    16.                 t_mInstance = this;
    17.                 DontDestroyOnLoad(gameObject);
    18.  
    19.                 //Only if you want to use OnSceneLoaded.
    20.                 SceneManager.sceneLoaded += OnSceneLoaded;
    21.             }
    22.             else
    23.             {
    24.                 Destroy(gameObject);
    25.                 return;
    26.             }
    27.         }
    28.  
    29.    
    30.         //Called at some point by some class that's doing your loading or setup, after you've finished making your characters and such.
    31.         void OnGameIsReady(PlayerMovement tPlayerMovement)
    32.         {
    33.             RegisterInput(tPlayerMovement);
    34.         }
    35.  
    36.         //Another, more haphazard way using OnSceneLoaded, though I highly advise you don't do it this way. Inputs could get registered way before you're ready to handle the player.
    37.         void OnSceneLoaded(Scene tScene, LoadSceneMode eMode)
    38.         {
    39.             //Probably should do some null checks in here.
    40.             GameObject tMyPlayer = GameObject.Find("PlayerCharacter");
    41.  
    42.             PlayerMovement tPlayerMovement = tMyPlayer.GetComponent<PlayerMovement>();
    43.  
    44.             RegisterInput(tPlayerMovement);
    45.         }
    46.  
    47.         //A third option is to register your inputs remotely when your players are created, though this could cause some weirdness.
    48.         public static void RegisterInput(PlayerMovement tPlayerMovement)
    49.         {
    50.             t_mInstance.MyPlayerInputChild.KartRacing.Move.started += tPlayerMovement.OnMove;
    51.             t_mInstance.MyPlayerInputChild.KartRacing.Move.performed += tPlayerMovement.OnMove;
    52.             t_mInstance.MyPlayerInputChild.KartRacing.Move.canceled += tPlayerMovement.OnMove;
    53.         }
    54.  
    55.  
    56.         //You'll need to unregister your inputs whenever your player movement is destroyed.
    57.         public static void UnregisterInput(PlayerMovement tPlayerMovement)
    58.         {
    59.             t_mInstance.MyPlayerInputChild.KartRacing.Move.started -= tPlayerMovement.OnMove;
    60.             t_mInstance.MyPlayerInputChild.KartRacing.Move.performed -= tPlayerMovement.OnMove;
    61.             t_mInstance.MyPlayerInputChild.KartRacing.Move.canceled -= tPlayerMovement.OnMove;
    62.         }
    63.     }
    64.  
    65.     class PlayerMovement : MonoBehaviour
    66.     {
    67.         float f_mForwardMovement = 0.0f;
    68.  
    69.         void FixedUpdate()
    70.         {
    71.             transform.position += transform.forward * f_mForwardMovement * Time.deltaTime;
    72.         }
    73.  
    74.         //You can also register your inputs when this class is created, but I would advise against it as it could create race conditions between players.
    75.         //void Start()
    76.         //{
    77.         //    MyInputManager.RegisterInput(this);
    78.         //}
    79.  
    80.         //This should unregister inputs before this class is destroyed.
    81.         void OnDestroy()
    82.         {
    83.             MyInputManager.UnregisterInput(this);
    84.         }
    85.  
    86.         //Called by your input classes using events.
    87.         public void OnMove(InputAction.CallbackContext tContext)
    88.         {
    89.             f_mForwardMovement = tContext.ReadValue<float>();
    90.         }
    91.     }

    Of course, with the Unity refresh, you'll likely still hit that null issue, but using a static input means you have one point of failure. The events should still be maintained.

    Hope that helps!