Search Unity

Question Question regarding Lists

Discussion in 'Scripting' started by McPeppergames, Mar 25, 2023.

  1. McPeppergames

    McPeppergames

    Joined:
    Feb 15, 2019
    Posts:
    103
    I have two Lists, one for GameObjects and another for a struct (with some int, float, color, bool data).
    Sample:
    Code (CSharp):
    1. public struct MyStruct
    2. {
    3.    public int index;
    4.    public float speed;
    5.    ...
    6.  
    7.    public MyStruct(int index,float speed,...)
    8.    {
    9.        this.index = index;
    10.        this.speed = speed;
    11.        ...
    12.    }
    13.  
    14. }
    15.  
    16. public List<GameObject> listOfEnemies;
    17. public List<MyStruct> listOfStructs;
    Now when using:
    listOfEnemies.Add (enemy)

    this is working!

    BUT when doing the same with:
    listOfStructs.Add (acreatedstruct)

    this is giving an error! ERROR: NullReferenceException: Object reference not set to an instance of an object

    I can fix this error by doing this first before adding something:
    listOfStructs = new List<MyStruct>();

    Then the error is gone.

    My QUESTION: Why do I not need to do this for the GameObject list? Why is the one list working without creating the "new List<>" and the other not?

    Any help welcome!
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,726
    Because you have marked those fields
    public
    , this informs Unity:

    "try to deserialize any data you have into them"

    If there is no data saved in the prefab / scene then the list won't be initalized either.

    If anything is saved for that field, even zero GameObjects, it will initialize the list.

    You should mark those fields
    private
    if you are initializing them all in your class.

    Here's the general skinny:

    Serialized properties in Unity are initialized as a cascade of possible values, each subsequent value (if present) overwriting the previous value:

    - what the class constructor makes (either default(T) or else field initializers)

    - what is saved with the prefab

    - what is saved with the prefab override(s)/variant(s)

    - what is saved in the scene and not applied to the prefab

    - what is changed in Awake(), Start(), or even later etc.

    Make sure you only initialize things at ONE of the above levels, or if necessary, at levels that you specifically understand in your use case. Otherwise errors will seem very mysterious.

    Here's the official discussion: https://blog.unity.com/technology/serialization-in-unity

    Field initializers versus using Reset() function and Unity serialization:

    https://forum.unity.com/threads/sensitivity-in-my-mouselook-script.1061612/#post-6858908

    https://forum.unity.com/threads/crouch-speed-is-faster-than-movement-speed.1132054/#post-7274596

    Be careful with those structs or they will baffle you in other ways because they are not classes:

    Remember also the difference between Value Types vs Reference Types:

    https://forum.unity.com/threads/hel...a-game-object-with-code.1047332/#post-6779456
     
    Last edited: Mar 25, 2023
    Olipool, orionsyndrome and samana1407 like this.
  3. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,108
    @McPeppergames
    Let's see

    This can't be working the way you're describing it. You are missing something in your code that makes it behave weirdly.

    listOfEnemies is just a variable name. You need to declare the type of data this variable name is supposed to refer to. For simplicity sake, let's just say it's a List, no angled brackets.
    So
    Code (csharp):
    1. List listOfEnemies;
    Let's pretend for a moment this is a valid declaration.
    Now the variable is declared so that the compiler knows what to expect from it, but it refers to nothing. Why?
    Because that's the default behavior for reference types.

    Here's an explanation:
    You see, there are some data types like integers and floating point numbers which are considered basic and simple. Because they're seen as basic and simple and limited in size they can be used much faster than some other, perhaps more complex data structures. For this reason they "live" closer to the CPU, at least most of the time. In C# we call these types value types, as opposed to reference types.

    Reference types work by storing their contents on some remote location, but then keep a primitive pointer locally. This pointer is then used to address the allocated space on demand. This is exactly what value types try to avoid, they don't keep the pointers, they actually pass their value around by producing a copy.

    Let's get back to a variable pointing to nothing. By default, C# won't actually allocate anything before you say so, and when you declare a reference type variable it will be set to null immediately (basically the internal pointer has nowhere to point at). Value types have no nulls, they assume some initial value (aka default), for example 0.

    So you declared the variable and it now points to null. What would happen if you were to use it?
    Code (csharp):
    1. listOfEnemies.Add(myEnemy);
    This would produce a runtime error. Why?
    Because you have never created anything this variable would point to. It refers to null, and thus there is no Add method in it, because when you call a method, you operate on some instance in memory.

    To actually create an instance in memory in C# (and many other languages), we use the keyword new.
    This keyword has to be used with an object constructor in tandem, and will allocate the necessary memory and return a valid reference to it. And because the variable was declared with the List type, the only thing it can refer to is an object of type List.
    Code (csharp):
    1. listOfEnemies = new List();
    2. listOfEnemies.Add(myEnemy);
    Now this would work, if myEnemy was something compatible with the List.

    Btw it's worth pointing out that primitive types do not usually need new (because you can use literals). However not all value types work like that. For example, structs are value types which require new. This has nothing to do with what your question, just keep that in mind.

    But how can we tell if myEnemy's type is compatible with List? The List is something that owns, collects and manages multiple elements of some type, but we have never specified which type is it. That's why the generic List declaration must include the angled brackets.
    Code (csharp):
    1. List<Enemy> listOfEnemies;
    2. listOfEnemies = new List<Enemy>();
    3. listOfEnemies.Add(myEnemy);
    List<Enemy> is supposed to be read as 'a list of type Enemy'

    This should have been the case for the previous scenario as well. You should investigate into what you're doing wrong so that it doesn't fire an error, because it seems that you've lost control over your code! (stay to the end because I'll explain to you what's wrong)

    A type of List<whatever> is always a reference type. This is because there is nothing simple or primitive with potentially enormous lists of any data, however simple or primitive it is on its own. So the default value for a variable declared as a list will always be null, until it gets reassigned to something that is specifically allocated with new.

    Yes, but if something is necessary I wouldn't call it a fix, though. A fix is when something is broken or insurmountable. Here you're fully expected to declare type and allocate a new object.

    Now this is an illusion that you have inadvertently created for yourself. It is incredible how complicated things can get if you don't understand the underlying system properly.

    Let's get to what Kurt-Dekker was saying above. In Unity, when you create a MonoBehaviour derivative, all public fields will be automatically serialized. This adds a layer of complexity to your C# code that's somewhat invisible.

    In this particular case, your GameObject list would deserialize on its own, which means it would fetch its state from the scene file, and even if it was empty, the object would be allocated on its own, because that's how deserialization works, it will never get back to null. So no error when you try to access it!

    edit:
    The reason why this doesn't work the same with your struct is because you haven't made that struct serializable so Unity decides to not serialize that list. And so it fires the error as expected.

    To make your struct serializable you must add the attribute [System.Serializable] before the struct declaration and either leave its fields public or explicitly add [UnityEngine.SerializeField] in front of them.

    I'd advise always doing the latter because it is much easier to tell which fields are intended for serialization. If you did that, you would be able to track much more easily what is going on in this particular scenario.
     
    Last edited: Mar 25, 2023
    Bunny83 likes this.
  4. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    322
    This serialization of Unity can also lead to hard-to-find bugs.

    One example:
    If you have a Zombie gameobject that contains a private field "spottedPlayer of the type Player.
    And you have some checks for the behavior such as:
    if (spottedPlayer==null) IdleAround();


    So your Zombie will just idle around when it has not spotted any Player.
    And later you decide you want to see the spottedPlayer in the inspector so you decide to make Player serializable.
    When the Zombie gameobject then is created, the spottedPlayer is serialized and deserialized and by that create a default Player object. From that point on you Zombie won't be able to idle around because spottedPlayer is never null.

    Just for the back of your head, it surely killed me for a few times to find those bugs :D
     
  5. McPeppergames

    McPeppergames

    Joined:
    Feb 15, 2019
    Posts:
    103
    Wow! Thank you all so much for your fast feedback! I really appreciate all your help and info!
    Seems like I now have some homework to do ;)

    Thanks again!

    Have a great weekend all!
     
    Olipool likes this.
  6. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    322
  7. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,998
    How does that avoid the null problem? SerializeReference just "allows" you to serialize almost anything that is serializable, but you have to take care of that. So by default reference types would be null to begin with unless you actually create an instance. The issue here was that he relied on the inspector to create the list for him. That is what happened with the "listOfEnemies" because it's serializable and the inspector will initialize it for you. When you turn that field into a SerializeReference (given a new component that doesn't have any serialized data yet), that field would be null.

    So SerializeReference in general can produce more issues that it may solve. You should know when and how to use it and why. If you have issues with uninitialized variables being null, that's usually the case because you did not initialize it, like in the case of the OP. This would have fixed the issue:

    Code (CSharp):
    1. public List<MyStruct> listOfStructs = new List<MyStruct>();
    Some people just get used to Unity / the inspector initializing their variables for them. This is fine for serialized data, but if the field is not serializable, it will be ignored by Unity. That's why you should focus on the actual usage of the fields. If they should be edited and initialized in the inspector, they need to be serializable. If you have fields that are only used in your code, you should take care of the initialization of those fields and probably mark them as NonSerialized to make that clear.
     
  8. Olipool

    Olipool

    Joined:
    Feb 8, 2015
    Posts:
    322
    Sorry for being unclear. With null problem I was addressing the issue that you may have variables in your code and rely on null checks for logic to happen e.g. if a zombie has a target to follow or not. If the target is serializable and gets shown in the inspector, it will always be NOT null. So using SerializeReference can help with that if I understand the docs correctly (please tell me if I am mistaken).
     
  9. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,998
    No, that's not right :). The inspector will only initialize fields of data that is serialized "inline". So custom Serializable classes or arrays / Lists of a serializable type. References to UnityEngine.Object derived types are actually serialized as "reference" and is the only category that can actually be null when nothing is assigned.

    Fields marks as SerializeReference actually can not be used on fields of UnityEngine.Object derived types. To quote the link you posted:
    Your logic always needs a null check if its conceptionally possible for your game to not have a target. Keep in mind when a gameobject is destroyed, references to that object do not magically get null, but Unity's overloaded == operator "pretends" that the reference is null.

    This thread wasn't really about serialization. The field in question was just used at runtime and just wasn't initialized. So I think it's better to not side-track this thread too much :)
     
    Olipool likes this.