Search Unity

NullReferenceException - first steps gone wrong :)

Discussion in 'Scripting' started by Cyberb00ster, Jul 6, 2019.

  1. Cyberb00ster

    Cyberb00ster

    Joined:
    Jun 22, 2019
    Posts:
    8
    Hello everyone,
    I'm trying to prototype a RTS game and I got a RTS script for the camera around GitHub, I started messing around following some tutorials to get some actors moving in the scene and selecting/deselecting units.

    I got to the point where I could select/deselect and make a unit move with a right click anywhere in the scene.

    However, I decided to "improve" my code introducing some changes to allow the right click to distinguish between an object "targetable" and one that it's not (later down the line this should enable targeting systems, attacks, etc.). The idea is that, if the unit I have currently selected is a "ResourceCollector" and the target is a "Resource" then, after the movement, I could trigger the collection of resources. If it's flagged as an enemy unit, then I could start attacking when in range, etc.

    But now I'm stuck whenever I right click, I got this:
    Code (Csharp):
    1.  
    2. NullReferenceException: Object reference not set to an instance of an object
    3. GenericObjectInfo.MoveUnit (UnityEngine.Vector3 target) (at Assets/Scripts/GenericObjectInfo.cs:88)
    4. RTSCameraController.RightClick () (at Assets/Scripts/RTSCameraController.cs:115)
    5. RTSCameraController.Update () (at Assets/Scripts/RTSCameraController.cs:92)
    6.  
    This is the RTSCameraController, attached to the main (and for now only) camera:
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [RequireComponent(typeof(Camera))]
    6.  
    7. public class RTSCameraController : MonoBehaviour
    8. {
    9.     #region Camera
    10.     public float ScreenEdgeBorderThickness = 5.0f; // distance from screen edge. Used for mouse movement
    11.  
    12.     [Header("Camera Mode")]
    13.     [Space]
    14.     public bool RTSMode = true;
    15.     public bool FlyCameraMode = false;
    16.  
    17.     [Header("Movement Speeds")]
    18.     [Space]
    19.     public float minPanSpeed;
    20.     public float maxPanSpeed;
    21.     public float secToMaxSpeed; //seconds taken to reach max speed;
    22.     public float zoomSpeed;
    23.  
    24.     [Header("Movement Limits")]
    25.     [Space]
    26.     public bool enableMovementLimits;
    27.     public Vector2 heightLimit;
    28.     public Vector2 lenghtLimit;
    29.     public Vector2 widthLimit;
    30.     private Vector2 zoomLimit;
    31.  
    32.     private float panSpeed;
    33.     private Vector3 initialPos;
    34.     private Vector3 panMovement;
    35.     private Vector3 pos;
    36.     private Quaternion rot;
    37.     private bool rotationActive = false;
    38.     private Vector3 lastMousePosition;
    39.     private Quaternion initialRot;
    40.     private float panIncrease = 0.0f;
    41.  
    42.     [Header("Rotation")]
    43.     [Space]
    44.     public bool rotationEnabled;
    45.     public float rotateSpeed;
    46.  
    47.     #endregion
    48.  
    49.     private GenericObjectInfo lastSelected;
    50.     private GenericObjectInfo currentSelected;
    51.  
    52.     // Use this for initialization
    53.     void Start()
    54.     {
    55.         initialPos = transform.position;
    56.         initialRot = transform.rotation;
    57.         zoomLimit.x = 15;
    58.         zoomLimit.y = 65;
    59.         lastSelected = null;
    60.         currentSelected = null;
    61.     }
    62.  
    63.  
    64.     void Update()
    65.     {
    66.  
    67.         # region Camera Mode
    68.         //check that ony one mode is choosen
    69.         if (RTSMode == true) FlyCameraMode = false;
    70.         if (FlyCameraMode == true) RTSMode = false;
    71.         #endregion
    72.  
    73.         Move();
    74.  
    75.         #region Zoom
    76.  
    77.         Camera.main.fieldOfView -= Input.mouseScrollDelta.y * zoomSpeed;
    78.         Camera.main.fieldOfView = Mathf.Clamp(Camera.main.fieldOfView, zoomLimit.x, zoomLimit.y);
    79.  
    80.         #endregion
    81.  
    82.         Rotate();
    83.  
    84.         if (Input.GetMouseButtonDown(0))
    85.         {
    86.             LeftClick();
    87.             Debug.Log("Leftclick on " + currentSelected.name + " Target movable " + currentSelected.isMovable);
    88.         }
    89.  
    90.         if (Input.GetMouseButtonDown(1))
    91.         {
    92.             RightClick();
    93.             Debug.Log("Right click on " + currentSelected.name);
    94.         }
    95.      
    96.  
    97.     }
    98.  
    99.     public void RightClick()
    100.     {
    101.         if (currentSelected != null)
    102.         {
    103.             Debug.Log("Right click on " + currentSelected.name);
    104.             currentSelected.AcquireTarget();
    105.             Debug.Log("TargetAcquired, " + currentSelected.name + " targeting is " + currentSelected.IsTargeting());
    106.  
    107.             if (currentSelected.IsTargeting())
    108.             {
    109.                 Debug.Log("Moving to Target" + currentSelected.GetCurrentTarget().name);
    110.                 currentSelected.MoveUnit(currentSelected.GetCurrentTarget());
    111.             }
    112.             else
    113.             {
    114.                 Debug.Log("Moving to Target Position" + currentSelected.GetCurrentTargetPosition());
    115.                 currentSelected.MoveUnit(currentSelected.GetCurrentTargetPosition());
    116.             }
    117.  
    118.         }
    119.              
    120.     }
    121.  
    122.  
    123.  
    124.  
    125.     public void LeftClick()
    126.     {
    127.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    128.         RaycastHit hit;
    129.  
    130.         if (Physics.Raycast(ray, out hit, 1000))
    131.         {
    132.             currentSelected = hit.collider.GetComponent<GenericObjectInfo>();
    133.  
    134.             if (currentSelected != null)
    135.             {
    136.  
    137.                 if (lastSelected == null || currentSelected.gameObject.name != lastSelected.gameObject.name)
    138.                 {
    139.  
    140.                     if (!currentSelected.IsSelectable())
    141.                     {
    142.                         if (lastSelected != null) {
    143.                             lastSelected.SetSelected(false);
    144.                             lastSelected = null;
    145.                         }
    146.                      
    147.                     }
    148.                     else if (lastSelected == null)
    149.                         {
    150.                             currentSelected.SetSelected(true);
    151.                             lastSelected = currentSelected;
    152.                         }
    153.                         else
    154.                         {
    155.                             lastSelected.SetSelected(false);
    156.                             currentSelected.SetSelected(true);
    157.                             lastSelected = currentSelected;
    158.                         }
    159.                 }
    160.             }
    161.             else if (lastSelected != null)
    162.             {
    163.                 lastSelected.SetSelected(false);
    164.                 lastSelected = null;
    165.             }
    166.  
    167.         }
    168.      
    169.  
    170.     }
    171.  
    172.     private void Move()
    173.     {
    174.         #region Movement
    175.  
    176.         panMovement = Vector3.zero;
    177.  
    178.         if (Input.GetKey("w") || Input.mousePosition.y >= Screen.height - ScreenEdgeBorderThickness)
    179.         {
    180.             panMovement += Vector3.forward * panSpeed * Time.deltaTime;
    181.         }
    182.         if (Input.GetKey("s") || Input.mousePosition.y <= ScreenEdgeBorderThickness)
    183.         {
    184.             panMovement -= Vector3.forward * panSpeed * Time.deltaTime;
    185.         }
    186.         if (Input.GetKey("a") || Input.mousePosition.x <= ScreenEdgeBorderThickness)
    187.         {
    188.             panMovement += Vector3.left * panSpeed * Time.deltaTime;
    189.         }
    190.         if (Input.GetKey("d") || Input.mousePosition.x >= Screen.width - ScreenEdgeBorderThickness)
    191.         {
    192.             panMovement += Vector3.right * panSpeed * Time.deltaTime;
    193.             //pos.x += panSpeed * Time.deltaTime;
    194.         }
    195.         if (Input.GetKey("q"))
    196.         {
    197.             panMovement += Vector3.up * panSpeed * Time.deltaTime;
    198.         }
    199.         if (Input.GetKey("e"))
    200.         {
    201.             panMovement += Vector3.down * panSpeed * Time.deltaTime;
    202.         }
    203.  
    204.         if (RTSMode) transform.Translate(panMovement, Space.World);
    205.         else if (FlyCameraMode) transform.Translate(panMovement, Space.Self);
    206.  
    207.  
    208.         //increase pan speed
    209.         if (Input.GetKey("w") || Input.GetKey("s")
    210.             || Input.GetKey("a") || Input.GetKey("d")
    211.             || Input.GetKey("e") || Input.GetKey("q")
    212.             || Input.mousePosition.y >= Screen.height - ScreenEdgeBorderThickness
    213.             || Input.mousePosition.y <= ScreenEdgeBorderThickness
    214.             || Input.mousePosition.x <= ScreenEdgeBorderThickness
    215.             || Input.mousePosition.x >= Screen.width - ScreenEdgeBorderThickness)
    216.         {
    217.             panIncrease += Time.deltaTime / secToMaxSpeed;
    218.             panSpeed = Mathf.Lerp(minPanSpeed, maxPanSpeed, panIncrease);
    219.         }
    220.         else
    221.         {
    222.             panIncrease = 0;
    223.             panSpeed = minPanSpeed;
    224.         }
    225.  
    226.         #endregion
    227.  
    228.         #region boundaries
    229.  
    230.         if (enableMovementLimits == true)
    231.         {
    232.             //movement limits
    233.             pos = transform.position;
    234.             pos.y = Mathf.Clamp(pos.y, heightLimit.x, heightLimit.y);
    235.             pos.z = Mathf.Clamp(pos.z, lenghtLimit.x, lenghtLimit.y);
    236.             pos.x = Mathf.Clamp(pos.x, widthLimit.x, widthLimit.y);
    237.             transform.position = pos;
    238.         }
    239.  
    240.  
    241.  
    242.         #endregion
    243.  
    244.     }
    245.  
    246.     private void Rotate()
    247.     {
    248.         #region mouse rotation
    249.  
    250.         if (rotationEnabled)
    251.         {
    252.             // Mouse Rotation
    253.             if (Input.GetMouseButton(2))
    254.             {
    255.                 rotationActive = true;
    256.                 Vector3 mouseDelta;
    257.                 if (lastMousePosition.x >= 0 &&
    258.                     lastMousePosition.y >= 0 &&
    259.                     lastMousePosition.x <= Screen.width &&
    260.                     lastMousePosition.y <= Screen.height)
    261.                     mouseDelta = Input.mousePosition - lastMousePosition;
    262.                 else
    263.                 {
    264.                     mouseDelta = Vector3.zero;
    265.                 }
    266.                 var rotation = Vector3.up * Time.deltaTime * rotateSpeed * mouseDelta.x;
    267.                 rotation += Vector3.left * Time.deltaTime * rotateSpeed * mouseDelta.y;
    268.  
    269.                 transform.Rotate(rotation, Space.World);
    270.  
    271.                 // Make sure z rotation stays locked
    272.                 rotation = transform.rotation.eulerAngles;
    273.                 rotation.z = 0;
    274.                 transform.rotation = Quaternion.Euler(rotation);
    275.             }
    276.  
    277.             if (Input.GetKeyDown("space"))
    278.             {
    279.                 rotationActive = false;
    280.                 if (RTSMode) transform.rotation = Quaternion.Slerp(transform.rotation, initialRot, 0.5f * Time.time);
    281.             }
    282.  
    283.             lastMousePosition = Input.mousePosition;
    284.  
    285.         }
    286.  
    287.  
    288.         #endregion
    289.     }
    290.  
    291.  
    292.  
    293.  
    294. }
    and this is the GenericObjectInfo, attached to almost anything in the game world (this is the main class I'm extending to set unit properties):
    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEngine.AI;
    5.  
    6. public class GenericObjectInfo : MonoBehaviour
    7. {
    8.     public bool isSelectable = false;
    9.     public bool isSelected = false;
    10.     public string objectName;
    11.     private NavMeshAgent agent;
    12.     public bool isMovable = false;
    13.     private Transform currentTarget = null;
    14.     private Vector3 currentTargetPosition;
    15.     public bool canBeTarget = false;
    16.     private bool targeting = false;
    17.  
    18.  
    19.     // Start is called before the first frame update
    20.     void Start()
    21.     {
    22.         isSelected = false;
    23.      
    24.         if (isMovable)
    25.         {
    26.             agent = GetComponent<NavMeshAgent>();
    27.         }
    28.     }
    29.  
    30.     // Update is called once per frame
    31.     void Update()
    32.     {
    33.      
    34.      
    35.     }
    36.  
    37.     public void AcquireTarget()
    38.     {
    39.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    40.         if (Physics.Raycast(ray, out RaycastHit hit, 1000))
    41.         {
    42.             GenericObjectInfo temp = hit.collider.GetComponent<GenericObjectInfo>();
    43.             if (temp !=null && temp.canBeTarget == true)
    44.             {
    45.                 currentTarget = hit.collider.gameObject.transform;
    46.                 targeting = true;
    47.                 Debug.Log("Target " + currentTarget.position);
    48.              
    49.             }
    50.             else if (temp == null || temp.canBeTarget == false) // for the sake of debugging. This else if should be in OR with the next else if
    51.             {
    52.                 targeting = false;
    53.                 currentTargetPosition = hit.point;
    54.                 Debug.Log("Target NULL or cannot be targeted");
    55.              
    56.             }
    57.          
    58.  
    59.  
    60.         }
    61.      
    62.     }
    63.  
    64.     public bool IsTargeting()
    65.     {
    66.         return targeting;
    67.     }
    68.  
    69.  
    70.     public bool IsSelectable ()
    71.     {
    72.         return isSelectable;
    73.     }
    74.  
    75.     public void SetSelectable (bool var)
    76.     {
    77.         isSelectable = var;
    78.         Debug.Log(this + " Set Selectable " + isSelectable);
    79.     }
    80.  
    81.     public void MoveUnit (Transform target)
    82.     {
    83.         agent.destination = target.position;
    84.         Debug.Log("Target set to " + agent.destination);
    85.     }
    86.     public void MoveUnit(Vector3 target)
    87.     {
    88.         agent.destination = target;
    89.         Debug.Log("Destination set to " + agent.destination);
    90.     }
    91.  
    92.  
    93.     public bool IsMovable()
    94.     {
    95.         return isMovable;
    96.     }
    97.  
    98.     public void SetMovable(bool var)
    99.     {
    100.         isMovable = var;
    101.     }
    102.  
    103.     public void SetSelected(bool selectionToggle)
    104.     {
    105.         isSelected = selectionToggle;
    106.         Debug.Log("this " + this.name + " selection is " + selectionToggle);
    107.     }
    108.     public void SetNewTarget(Transform enemy)
    109.     {
    110.         currentTarget = enemy;
    111.     }
    112.  
    113.     public bool GetCanBeTarget()
    114.     {
    115.         return canBeTarget;
    116.     }
    117.  
    118.     public void SetCanBeTarget(bool var)
    119.     {
    120.         canBeTarget = var;
    121.     }
    122.  
    123.     public Transform GetCurrentTarget()
    124.     {
    125.         return currentTarget;
    126.     }
    127.  
    128.     public Vector3 GetCurrentTargetPosition()
    129.     {
    130.         return currentTargetPosition;
    131.     }
    132.  
    133.  
    134.  
    135. }
    136.  
     
  2. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Your
    agent
    is null, i.e. no object has been assigned. That's the only thing that can throw an exception in your method
    MoveUnit(Vector3 target)
    .
    The other overload could throw that exception if the supplied
    Transform
    was null, but the stack trace points to the overload with the
    Vector3
    parameter.
     
  3. Cyberb00ster

    Cyberb00ster

    Joined:
    Jun 22, 2019
    Posts:
    8
    Thanks, I understand that, but even with the Transform overload throws the same exception, that's why I'm puzzled. Either I right click on objects of one type or on the other (difference is the bool variable canBeTarget). How come the agent is null?

    Based on what you are saying, the bug should be here:
    Code (CSharp):
    1. public void AcquireTarget()
    2.     {
    3.         Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
    4.         if (Physics.Raycast(ray, out RaycastHit hit, 1000))
    5.         {
    6.             GenericObjectInfo temp = hit.collider.GetComponent<GenericObjectInfo>();
    7.             if (temp !=null && temp.canBeTarget == true)
    8.             {
    9.                 currentTarget = hit.collider.gameObject.transform;
    10.                 targeting = true;
    11.                 Debug.Log("Target " + currentTarget.position);
    12.            
    13.             }
    14.             else if (temp == null || temp.canBeTarget == false) // for the sake of debugging. This else if should be in OR with the next else if
    15.             {
    16.                 targeting = false;
    17.                 currentTargetPosition = hit.point;
    18.                 Debug.Log("Target NULL or cannot be targeted");
    19.            
    20.             }
    21.        
    22.         }
    23.    
    24.     }
    However, I can read the Debug lines correctly in the console, thus the currentTarget and currentTargetPosition should be correct.
     
  4. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    No, the error says line 88, which is the MoveUnit(Vector3) method.
    As stated earlier, the only thing that can be null in this method is the agent field, and that's also what the stack trace points to (line 88).

    That is, agent has not been assigned.

    The only place where agent is assigned is the following line, which is in your Start method:
    Code (csharp):
    1. agent = GetComponent<NavMeshAgent>();
    Make sure it actually returns an agent.
     
  5. Cyberb00ster

    Cyberb00ster

    Joined:
    Jun 22, 2019
    Posts:
    8
    Code (csharp):
    1. agent = GetComponent<NavMeshAgent>();
    This should be applied to all GenericObjectInfo items that are flagged as movable, including the ones I'm trying to move.

    There is something that I do not understand.
    I tried to put a Debug.Log in the Start(), however it doesn't show up in the console.
    Code (CSharp):
    1.  void Start()
    2.     {
    3.         isSelected = false;
    4.      
    5.         if (isMovable)
    6.         {
    7.             agent = GetComponent<NavMeshAgent>();
    8.             Debug.Log("NavMeshAgent set for " + this.name);
    9.         }
    10.     }
    Furthermore, the item is set as Movable as I can see from here:
    Code (CSharp):
    1. Leftclick on Actor3 Target movable True
    2. UnityEngine.Debug:Log(Object)
    3. RTSCameraController:Update() (at Assets/Scripts/RTSCameraController.cs:87)
    4.  
    how it can be possible that there is no instance of the agent, if the Actor isMovable = true? This is a property that I setup in the editor:



    Gatherer is a GenericObjectInfo
    Code (CSharp):
    1. public class Gatherer : GenericObjectInfo
    I don't undersand, sorry :(
     
    Last edited: Jul 7, 2019
  6. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    So this post reads as if the Start method does not run at all.

    You could quickly check that by adding a log to the Start method, or by attaching the debugger and set a breakpoint to that start method.

    Usually I'd guess something has disbaled the component, however, your raycast seems to find the object and the component attached to it.

    First, make sure your Gatherer component does not implement its very own Start method. That would cause some trouble, as Unity will likely call that one and would then ignore the GenericObjectInfo's Start method completely.

    In order to fix this, you can mark the base class' Start method protected virtual, and override it in the subclass. The subclass should then call base.Start() before it proceeds with its own logic.
     
    Cyberb00ster likes this.
  7. Cyberb00ster

    Cyberb00ster

    Joined:
    Jun 22, 2019
    Posts:
    8
    Thank you!
    This fixed everything.


    What's the best practice around the Start() method, when I inherit from other classes?
    Should I rewrite the start copy-pasting everything, or is there something that I can declare to invoke both Start() methods?

    I most likely will end up needing to do something in the Start for the GenericObjectInfo and then other things for the other units that will inherit from it.
     
  8. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    Copy-pasting is the one of the worse solutions. It's prone to bugs (for instance, if there are changes and you forget to update all the other classes) and it also requires to mark everything as protected, if you want to move the full initialization to the subtype.

    You should either override those methods or add another method for which you enforce implementation in subclasses, and which is called in the base class.

    However, none of those solutions are fool-proof.

    Personally, I break my conventions in regards of access modifiers and always make methods such as Awake, Start, OnDestroy etc protected from the very beginning, even when there's no subclass.

    When I decide to subclass a component, I can at least get a warning that tells me I'm about to hide a method with the same signature. Otherwise, if it was private, i might forget about it completely, so having it protected has the result that the compiler draws some attention to it.

    As I said, this is also not fool-proof, because you can either ignore the warning altogether or you may even follow the hint which suggests that the "new" keyword can be used, in order to hide that method and make the warning disappear.
    Both is likely not desired, as member hiding introduces some special behaviour that can render polymorphism useless.

    Example:
    Code (csharp):
    1. public class CustomComponent : MonoBehaviour
    2. {
    3.     protected void Start()  // starting out with a non-virtual protected Unity message
    4.     {
    5.         // ...
    6.     }
    7. }
    Later, when you subclass it
    Code (csharp):
    1. public class AnotherComponent : CustomComponent
    2. {
    3.    // warning will be issued, as it hides an accessible member of the subclass
    4.    // if CustomComponent.Start() was private, you would not get a warning
    5.     protected void Start()
    6.     {
    7.         // ...
    8.     }
    9. }
    Now you're aware of the issue, unless you're the kind og developer that doesn't pay attention to warnings. In most cases, you should go for the following:

    Code (csharp):
    1. public class CustomComponent : MonoBehaviour
    2. {
    3.     protected virtual void Start()
    4.     {
    5.         // do your stuff
    6.     }
    7. }
    8.  
    9. public class AnotherComponent : CustomComponent
    10. {
    11.     protected override void Start()
    12.     {
    13.         base.Start();
    14.         // proceed with this type's logic
    15.     }
    16. }
     
    Last edited: Jul 7, 2019
    Cyberb00ster likes this.
  9. Cyberb00ster

    Cyberb00ster

    Joined:
    Jun 22, 2019
    Posts:
    8
    Thanks, this is most helpful.