Custom modding API with serialized interfaces

Hey guys, so I am wanting to add like a “modding API” to a “node” gameobject where the player can add additional functionality during runtime. Things like making the node move, giving the node custom data, add light / mesh components, etc etc.

I was thinking I would just use interfaces.

public interface IMovable
{
    public float speed { get; set; }
    public float3 direction { get; set; }

    void Move() {}
    void MoveToSpot() {}
}

public interface IAttackable { }
// etc

But interfaces are not serializable so I thought I would create a “generic” struct to hold whatever data the component might need.

public struct Component
{
    public int      ID;
    public bool[]   bools;
    public int[]    ints;
    public float[]  floats;
    public string[] strings;
    public int3[]   int3s;
    public float3[] float3s;
}

This way I can save it in a dictionary indexed by node ID or something. I would then assign them to each node on map load and let the node handle what to do with each component.

public class Node 
{
    public Component[] components;
}

I believe this would work but I’m curious if there is a better way? How would you all tackle this?

Also, I realize this is very similar to ECS but I would like to hear everyones opinion on not using it first. I tried learning it for a few days but tutorials were sparse and out of date and it just seems like its over my head. But if you all recommend it then I’ll try and take another whack at it. :expressionless:

Unity can serialize interfaces for use in the inspector via the SerializeReference attribute: Unity - Scripting API: SerializeReference

You will just need some custom inspector support.

But if this is for runtime use only, then inspector serialisation isn’t a concern. Any competent serialiser can handle by-reference serialisation.

1 Like

Thanks for reply spiney199. That looks like what I am trying to do.

I thought of another problem though. What if I wanted to use these components in burst? I would have to keep them as structs with no lists so I would probably have to create a separate hashmap for each component type right?

public NativeParallelHashMap<int, MovableData> movableInterfaces;
public NativeParallelHashMap<int, AttackableData> attackableInterfaces;

public struct MovableData
{
    public float speed;
    public float3 direction;
}

In the long run do you think it would be better to just bite the bullet and switch it to entities? It looks like you can easily save an entity via binary, Namespace Unity.Entities.Serialization | Entities | 1.3.2.

I think how I should do it is just create separate lists for each component in the map indexed by node ID.

        // NODES:  a list of all nodes in the map indexed by ID
        [NonSerialized] public NativeParallelHashMap<int, Node> nodes;

        // COMPONENTS:  we have to save maps of all our various component data for the node objects
        [NonSerialized] public NativeParallelHashMap<int, Root.Data.Component.Position> position; 
        [NonSerialized] public NativeParallelHashMap<int, Root.Data.Component.Transform> transform; 
        [NonSerialized] public NativeParallelHashMap<int, Root.Data.Component.Shape> shape; 

And then my node will only hold an ID and a tag list.

//
//  Node:   a struct that contains the essential data for a node.  a node is the base for 
//          every type of "object" in a map.. a shape, item, player, etc etc
//
//          whenever a node needs more data or functionality it gets tagged with a "component"
//


namespace Root.Data 
{
    [Serializable]

    public struct Node
    {
        // ID:  a unique identifier for each node
        public int ID;

        // TAGS:  a bitmask for tagging a node with components
        public int tags;
    }
}

I could then check the nodes tags to see if it has a specific component, etc etc.

//
//  Transform:      if a node has the "transform" tag then this lets us know 
//                  that the node needs a dedicated gameobject in the scene
//


namespace Root.Data.Component
{
    [Serializable]

    public struct Transform
    {
        // POSITION:  this is separate from the "mapped" component which is a grid position
        public float3 position;

        // ROTATION:  the transforms rotation
        public float3 rotation;

        // SCALE:  the transforms scale
        public float3 scale;
    }
}