Search Unity

ScriptableObjects and derived classes not playing nice with the Inspector

Discussion in 'Scripting' started by veggiesama, Mar 19, 2019.

  1. veggiesama

    veggiesama

    Joined:
    Jul 11, 2017
    Posts:
    7
    My situation: I'm trying to use ScriptableObjects, abstract classes, and the Inspector, but Unity doesn't like any of it.

    I have abstract class StatusEffect (derived from ScriptableObject), derived class Slow, and a number of Slow assets. General fields like "duration" go into StatusEffect. More specialized fields go into Slow. Both of these also call functions like Apply(), End(), and Update(), but I don't want to over-complicate my question. Next, I use
    [CreateAssetMenu(menuName = "Status Effects/Slowed")]
    to make a Slowed asset from the Create menu. I want this asset to let me input data that overrides my original Slow effect. Maybe I want to slow by 90% instead of 25%. After modifying it with the Inspector, I want to pop that reference into the gun prefab. Bam. Slow bullets.

    What's wrong: Everything. I've gone down several rabbit holes but I seem to run into issues with either the code not working, Unity giving me a serialization error, or the Inspector acting strangely.

    Here's what I tried so far:

    Code (CSharp):
    1. public abstract class StatusEffect : ScriptableObject {
    2.     public float duration;
    3. }
    4.  
    5. public class Slowed : StatusEffect {
    6.  
    7.    public void Initialize()
    8.    {
    9.        duration = 10f;
    10.    }
    11. }
    12.  

    When I add the asset, the duration is undefined. Obviously, Initialize() is called through my code but not through the Inspector or otherwise automatically called through Unity. I'd like it to default to 10. If I have a dozen fields, I don't want to retype everything every time I create a new asset. So I tried something different.

    Code (CSharp):
    1. public abstract class StatusEffect : ScriptableObject {
    2.     public float duration;
    3. }
    4.  
    5. public class Slowed : StatusEffect {
    6.  
    7.    public void OnEnable()
    8.    {
    9.        duration = 10f;
    10.    }
    11. }
    12.  

    I tried using both OnEnable() and Awake(). When I add the asset, the inspector seems to show the intended value (10). I change duration from 10 to 2 through the Inspector. Save. Play. The Inspector resets to 10. What's the point of letting me edit it if it won't save my changes? This seems like the wrong way to do this, so I tried something else.

    Code (CSharp):
    1. public abstract class StatusEffect : ScriptableObject {
    2.     public float duration;
    3. }
    4.  
    5. public class Slowed : StatusEffect {
    6.     public float duration = 10f;
    7. }
    Visual Studio warns me: "Slowed.duration" hides inherited member "StatusEffect.duration." Use the new keyword if hiding was intended.

    When I use the inspector to look at the ScriptableObject asset, I can see that duration appears twice. I get the error "The same field name is serialized multiple times in the class or its parent class. This is not supported." Makes sense.

    Let's try adding "new", as suggested.

    Code (CSharp):
    1.  
    2. public abstract class StatusEffect : ScriptableObject {
    3.     public float duration;
    4. }
    5.  
    6. public class Slowed : StatusEffect {
    7.     new public float duration = 10f;
    8. }
    Seems inelegant and breaks polymorphism (right?). Also, duration is wrong during gameplay. When I inspect using Visual Studio, it tells me the correct value (10). When I post a Debug.Log() immediately afterwards, it tells me the wrong value (0). I'm not sure what's going on there.

    On further thought, I think it's using duration=10 during Slowed functions and duration=0 (default uninitialized value) during any StatusEffect functions. In other words--polymorphism isn't working. Derp.

    Code (CSharp):
    1.  
    2. public abstract class StatusEffect : ScriptableObject {
    3.     [HideInInspector] public float duration;
    4. }
    5.  
    6. public class Slowed : StatusEffect {
    7.     public float duration = 10f;
    8. }
    I removed new and added [HideInInspector] instead. Game seems to play correctly. Inspector isn't getting overridden. However, my console is spammed with "The same field name is serialized multiple times in the class or its parent class. This is not supported."

    Code (CSharp):
    1.  
    2. public abstract class StatusEffect : ScriptableObject {
    3.     [System.NonSerialized] public float duration;
    4. }
    5.  
    6. public class Slowed : StatusEffect {
    7.     public float duration = 10f;
    8. }
    Just like my third example, Visual Studio says: "Slowed.duration" hides inherited member "StatusEffect.duration." Use the new keyword if hiding was intended.

    At least the Inspector looks OK.

    I tried to combine both [System.NonSerialized] and [HideInInspector], but gameplay is giving me 0 duration again.

    What am I doing wrong? Do I just have the wrong assumptions about what this ScriptableObjects are capable of, am I going about something the wrong way, or is there some trick I'm just not seeing?
     
  2. TJHeuvel-net

    TJHeuvel-net

    Joined:
    Jul 31, 2012
    Posts:
    838
  3. veggiesama

    veggiesama

    Joined:
    Jul 11, 2017
    Posts:
    7
    You rock, seriously. I knew it'd be something that easy.

    For future time travelers, here's the right solution to get a default value in the Inspector that plays nicely with abstract ScriptableObject subclasses:

    Code (CSharp):
    1. public abstract class StatusEffect : ScriptableObject {
    2.  
    3.     public string statusName;
    4.     public StatusEffectTypes type;
    5.     public float duration;
    6. }
    7.  
    8. [CreateAssetMenu(menuName = "Status Effects/Slowed")]
    9. public class Slowed : StatusEffect {
    10.  
    11.     public float slowByPercentage = 0.2f;
    12.  
    13.     public void Reset()
    14.     {
    15.         duration = 10f;
    16.         statusName = "Slowed";
    17.         type = StatusEffectTypes.SLOWED;
    18.     }
    19. }
     
    TJHeuvel-net likes this.
  4. Suddoha

    Suddoha

    Joined:
    Nov 9, 2013
    Posts:
    2,824
    You wouldn't even need Reset(), if you just used the field initializer in the base class.

    Code (CSharp):
    1. public abstract class StatusEffect : ScriptableObject {
    2.     [...]
    3.     public float duration = 10f;
    4. }
    That'd be enough to cover the general case of having an initial value of 10 for everything that's newly created or reset and by default, this will apply to all subtypes as well. You can still use Reset to change the behaviour of subtypes, though.
     
  5. veggiesama

    veggiesama

    Joined:
    Jul 11, 2017
    Posts:
    7
    I was thinking of falling back to that, but I wanted default values for Slowed specifically and not the entire StatusEffect class. This is mostly overkill for what I actually need, but I'm treating it as practice before I build a pluggable AI framework.
     
  6. TJHeuvel-net

    TJHeuvel-net

    Joined:
    Jul 31, 2012
    Posts:
    838
    Thanks a bunch for posting your solution too!