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!
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);
}