c# - Is there a way to fill scriptable object field with a child class? - Stack Overflow

admin2025-04-30  1

I am making a space invaders-like project, and I want to be able to assign movement patterns to all the enemies, bullets etc in the form of scripts.

To do that I have a base class MovementPattern that looks like this

[RequireComponent(typeof(Rigidbody2D))]
public class MovementPattern : MonoBehaviour
{
    protected Rigidbody2D rb;

    protected MovementPatternData movementPatternData;
            
    protected virtual void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    public virtual void SetMovementPatternData(MovementPatternData newMovementPatternData)
    {
        movementPatternData = newMovementPatternData;
    }
    
    public virtual MovementPatternData GetMovementPatternData()
    {
        return movementPatternData;
    }
}

The movementPatternData class doesn't contain much, the plan is to have children classes of it that will contain data about their movement pattern, for example:

[Serializable]
public class MovementPatternData : ScriptableObject
{
}

[Serializable]
public class SinusMovementPatternData : MovementPatternData
{
    [SerializeField]
    public float amplitude = 1f;
    [SerializeField]
    public float frequency = 1f;
    [SerializeField]
    public Vector2 direction = 1f;
}

For now I've done something that I find quite ugly, I wrote the SinusMovementPattern like this:

public class SinusMovementPattern : MovementPattern
{
    [SerializeField]
    public SinusMovementPatternData sinusMovementPatternData;
    
    public override void SetMovementPatternData(SinusMovementPatternData newMovementPatternData)
    {
        base.SetMovementPatternData(newMovementPatternData);
        sinusMovementPatternData = newMovementPatternData;
    }
}

So functionally the SinusMovementPattern has both types of data, I just ignore the default MovementPatternData.

They look like this in inspector:

I'm guessing if I can initialize my data classes they will show up in the inspector instead of the empty fields. However when I try and initialize them for example with:

public SinusMovementPatternData sinusMovementPatternData = ScriptableObject.CreateInstance<SinusMovementPatternData>();

I get an error that says I should initialize them in start or awake functions, however I'd want that data class to be initialized and modifiable in the editor, before pressing "play."

My problem is related to the MovementPatternData objects, I'd want the children of MovementPattern to fill their own MovementPatternData field when they get created, and I'd want the type of that data object to "replace" the original MovementPatternData type to have the child type that corresponds to the current movement pattern... For example if I add a sinusMovementPattern to an object it should have its own SinusMovementPatternData that "replaces" the original MovementPatternData.

I understand from prior research that I should use a custom editor, scriptable objects and [SerializeReference] somewhere, I'd love extra guidance on that!

I am making a space invaders-like project, and I want to be able to assign movement patterns to all the enemies, bullets etc in the form of scripts.

To do that I have a base class MovementPattern that looks like this

[RequireComponent(typeof(Rigidbody2D))]
public class MovementPattern : MonoBehaviour
{
    protected Rigidbody2D rb;

    protected MovementPatternData movementPatternData;
            
    protected virtual void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    public virtual void SetMovementPatternData(MovementPatternData newMovementPatternData)
    {
        movementPatternData = newMovementPatternData;
    }
    
    public virtual MovementPatternData GetMovementPatternData()
    {
        return movementPatternData;
    }
}

The movementPatternData class doesn't contain much, the plan is to have children classes of it that will contain data about their movement pattern, for example:

[Serializable]
public class MovementPatternData : ScriptableObject
{
}

[Serializable]
public class SinusMovementPatternData : MovementPatternData
{
    [SerializeField]
    public float amplitude = 1f;
    [SerializeField]
    public float frequency = 1f;
    [SerializeField]
    public Vector2 direction = 1f;
}

For now I've done something that I find quite ugly, I wrote the SinusMovementPattern like this:

public class SinusMovementPattern : MovementPattern
{
    [SerializeField]
    public SinusMovementPatternData sinusMovementPatternData;
    
    public override void SetMovementPatternData(SinusMovementPatternData newMovementPatternData)
    {
        base.SetMovementPatternData(newMovementPatternData);
        sinusMovementPatternData = newMovementPatternData;
    }
}

So functionally the SinusMovementPattern has both types of data, I just ignore the default MovementPatternData.

They look like this in inspector:

I'm guessing if I can initialize my data classes they will show up in the inspector instead of the empty fields. However when I try and initialize them for example with:

public SinusMovementPatternData sinusMovementPatternData = ScriptableObject.CreateInstance<SinusMovementPatternData>();

I get an error that says I should initialize them in start or awake functions, however I'd want that data class to be initialized and modifiable in the editor, before pressing "play."

My problem is related to the MovementPatternData objects, I'd want the children of MovementPattern to fill their own MovementPatternData field when they get created, and I'd want the type of that data object to "replace" the original MovementPatternData type to have the child type that corresponds to the current movement pattern... For example if I add a sinusMovementPattern to an object it should have its own SinusMovementPatternData that "replaces" the original MovementPatternData.

I understand from prior research that I should use a custom editor, scriptable objects and [SerializeReference] somewhere, I'd love extra guidance on that!

Share Improve this question edited Jan 5 at 4:33 CPlus 4,94245 gold badges31 silver badges73 bronze badges asked Jan 4 at 21:32 Adam WizardAdam Wizard 216 bronze badges 5
  • Surely your scripted object could hold an array of scripted objects, so, the various moves become an individual object, like move left, and then you array them and process them in order, you can add delays or whatever as long as you set it up right – BugFinder Commented Jan 4 at 23:11
  • @BugFinder That is an interesting idea, but it wouldn't solve the problem I think? Wouldn't that just push the problem back and instead of having 1 scriptable object that is uninitialized... I'd have a whole list of them...? – Adam Wizard Commented Jan 4 at 23:22
  • My scriptable objects dont need to be initialised at run time.. I make one in say a folder of my project and its available.. – BugFinder Commented Jan 4 at 23:53
  • That's the problem, I don't want to have them as asset files, I don't want to create an extra asset file everytime I want something to move differently... :/ Like for example I'd like to have a thing move in a circle, and another thing move in a larger circle, then I'd need to make 2 different scriptable objects even though they are of the same type? correct me if I'm wrong, I thought it was possible to use scriptable objects without saving them as files – Adam Wizard Commented Jan 5 at 0:07
  • then theres no point them being scriptable objects :D – BugFinder Commented Jan 5 at 10:53
Add a comment  | 

1 Answer 1

Reset to default 0

Sounds a bit like you want to use Generics

public abstract class MovementPattern<TData> : MonoBehavior where TData : MovementPatternData 
{
    protected Rigidbody2D rb;
    protected TData data { get; private set; }

    protected virtual void Awake()
    {
        rb = GetComponent<Rigidbody2D>();
    }

    public void SetData(TData newData)
    {
        data = newData;
    }
}

and then do e.g.

public class SinusMovementPattern : MovementPattern<SinusMovementPatternData> 
{

}

Question though: If the base MovementPatternData is empty anyway, why even force them to use a common ancestor?

Of you rather wanted to have exchangeable movement patterns all along I would rather put the movement related code itself into the subclasses of MovementPatternData and not use different field type at all.

e.g.

public abstract class MovementPatternData : ScriptableObject
{
    // Just as an example
    public abstract IEnumerator Movement(Transform transform);
}

and then each pattern can bring the fields it requires for configuration

public class SinusMovementPatternData : MovementPatternData
{
    public float some field;

    public override IEnumerator Movement(Transform transform)
    {
        // do something with someField
    }
}

So the base component MovementPattern itself wouldn't require any child classes but the behavior itself would also be provided by the ScriptableObject


Now that I understand the purpose a bit better you could still go with the upper generic approach and then additionally also have the ScriptableObject inject itself into the according GameObject and add the component it corresponds to.

I think somewhat something like this

public abstract class MovementPatternData : ScriptableObject
{
    public abstract void InitializeObject(GameObject gameObject);
}

[CreateAssetMenu]
public class SinusMovementPatternData : MovementPatternData
{
    // specific data fields  

    // There's gotta be a way to also make this generic so you don't have to basically write the same 
    // for each pattern data implementation.
    // But rn I couldn't wrap my head around it ^^
    public override void InitializeObject(GameObject gameObject)
    {
        gameObject.AddComponent<SinusMovementPattern>().SetData(this);
    }  
}

This way in a spawner you could simply reference e.g.

public MovementPatternData[] patternDatas;

and then go

foreach(var patternData in patternDatas)
{
    var enemy = new GameObject("someName");
    patternData.InitializeObject(enemy);
}
转载请注明原文地址:http://anycun.com/QandA/1746026740a91531.html