Search Unity

Word to The Wise: Structs vs Classes

Discussion in 'Scripting' started by jakejolli, Jan 29, 2019.

  1. jakejolli

    jakejolli

    Joined:
    Mar 22, 2014
    Posts:
    54
    I wrote a fairly simple script that takes an array of GameObjects, and toggles whether or not they're active in the scene after a set delay for each GameObject. Everything in the script seemed fine. All my logic checked out, but it just wouldn't work right.

    I re-jigged this friggin' code half a dozen times before I realized what the problem was.

    The original code (which doesn't work as expected):

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class DelayedObjectToggler : MonoBehaviour {
    6.  
    7.     [SerializeField]
    8.     private ObjectTimePair[] objectsToToggle;
    9.  
    10.     private float timeElapsed = 0f;
    11.  
    12.     private int toggledObjectCount = 0;
    13.  
    14.     void Update() {
    15.         timeElapsed += Time.deltaTime;
    16.  
    17.         for (int i = 0; i < objectsToToggle.Length; i++) {
    18.             ObjectTimePair obj = objectsToToggle[i];
    19.  
    20.             if (!obj.hasToggled && timeElapsed >= obj.toggleDelay) {
    21.                 obj.objectToToggle.SetActive(!obj.objectToToggle.activeSelf);
    22.                 toggledObjectCount++;
    23.                 obj.hasToggled = true;
    24.             }
    25.  
    26.             if (toggledObjectCount >= objectsToToggle.Length) {
    27.                 this.enabled = false;
    28.             }
    29.         }
    30.     }
    31. }
    32.  
    33. [System.Serializable]
    34. public struct ObjectTimePair {
    35.     public GameObject objectToToggle;
    36.     public float toggleDelay;
    37.     public bool hasToggled;
    38. }
    39.  
    All of my logic was sound. I stepped through it a ton of times, I just couldn't understand why it wasn't working... And then I noticed that values weren't being updated in the inspector. "Weird", I thought.

    At first, I thought this was a quirk of how Unity handled serialization, so I essentially made three new arrays of types GameObject, float and bool to copy the struct's values into in the Start function, and operate on them in there. This worked, but it just didn't smell right, and I knew it was bad practice.

    Then I thought, maybe it's because of the line

    ObjectTimePair obj = objectsToToggle[i];
    .

    So, I changed the whole code to refer directly to the object in the array:

    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. public class DelayedObjectToggler : MonoBehaviour {
    6.  
    7.     [SerializeField]
    8.     private ObjectTimePair[] objectsToToggle;
    9.  
    10.     private float timeElapsed = 0f;
    11.  
    12.     private int toggledObjectCount = 0;
    13.  
    14.     void Update() {
    15.         timeElapsed += Time.deltaTime;
    16.  
    17.         for (int i = 0; i < objectsToToggle.Length; i++) {
    18.  
    19.             if (!objectsToToggle[i].hasToggled && timeElapsed >= objectsToToggle[i].toggleDelay) {
    20.                 objectsToToggle[i].objectToToggle.SetActive(!objectsToToggle[i].objectToToggle.activeSelf);
    21.                 toggledObjectCount++;
    22.                 objectsToToggle[i].hasToggled = true;
    23.             }
    24.  
    25.             if (toggledObjectCount >= objectsToToggle.Length) {
    26.                 this.enabled = false;
    27.             }
    28.         }
    29.     }
    30. }
    31.  
    32. [System.Serializable]
    33. public struct ObjectTimePair {
    34.     public GameObject objectToToggle;
    35.     public float toggleDelay;
    36.     public bool hasToggled;
    37. }

    Again, this worked, but it looked messy and it didn't feel right. I did some googling, and realized that structs are value types, and so, by saying

    ObjectTimePair obj = objectsToToggle[i];


    and only operating on obj, I was making a copy and manipulating that, not the original object.

    So, in the end, I changed my struct to a class (a reference type), which still felt a little dirty, but the least so of everything else I'd tried, and everything worked like a charm, with inspector values updating as expected, etc.

    Word to the wise: little details matter, and a single word in your code can ruin your whole day.
     
  2. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,537
    Yes.

    This is the fundamental difference between classes and structs in C#.

    A class is a reference type.
    A struct is a value type.

    See classes have what is called "object identity". The idea is that an object is a thing, it has significance as itself. It is not its state, it is itself... its state is just its current state. You as a person have identity... you may have combed hair today, but if you have messy hair tomorrow, you're still you. Your identity is not measured by your state, but rather by the fact that you ARE you.

    Where as value types... int, Vector3, that sort... they don't have identity. If you have two 5's, they're both 5. They're not unique from one another. They're both equal. If the 5 changes to 7, it's no longer a 5. Same with Vector3. <0,0,0> and <1,0,0> aren't equal not because they identify differently... it's just they don't have the same value. If you change your vector, it's just a new value. Modifying a value just makes it a new value.

    Now you might be thinking, "yeah, but if I lerp my position, I'm lerping my position Vector. So thusly it is an identity. It's my position."

    But no... it's your Transform's state. The Transform is what carries identity, the value/Vector is just the state of the Transform. The Transform is a thing... if you have 2 Transforms in the same position, with the same rotation and scale... they're still unique from one another. They aren't equal. Just like if you had 2 red balls... they're similar, but not the same balls.

    The sense of object identity is a fundamental part of OOP. The fact 2 things can be in similar states but still be unique from one another. And C# uncovers object identity for OOP via the "class" construct. But it maintains a "struct" for simple non-oop data structures (of course there are exceptions, like pointers in unsafe mode).

    ...

    Note in your code you could have kept it a struct. You just would have had to say:
    Code (csharp):
    1. objectsToToggle[i] = obj
    After modifying the value.

    That or just modify the value in place in the array:
    Code (csharp):
    1. objectsToToggle[i].hasToggled = true;
    That's how structs do.
     
    sonmium, ow3n, luguina and 1 other person like this.