Module/Plugin-in System Design Question

Started by
4 comments, last by Beray 1 year, 8 months ago

Good day,

So, I'm currently designing a Plugin-in/Module System in a game engine I'm working on, and I hit a small hurdle. So, I got it working, so it's not in regards to that. So, the way it works at the moment is you create a module with an Entry file that contains a LoadModule function:

public class Entry 
{
	public void LoadModule() 
	{
		// Any code to execute here
	}
}

Now than. In the main core library, the dll is dynamically loaded into the executable using Assembly.LoadFrom() method. From there it's added to an API registry and the code is executed using ((dynamic)module.Handle.unwrap()).LoadMoule(). The Handle is the ObjectHandle? created from the Assembly.

So, now that you got a very rough idea on how the module system I created works. I want to be able to add functionality as I please into the engine, so far it does work. For example, the window and the renderer (opengl) are two separate functions, the window gets called and opens, but in order for the glrenderer to work, it has to be referenced in the window project.

Originally I wrote the module system in C99, but I got tired of everything breaking every 5 minutes, because of I'm terrible at C99, so I implemented it in C#. Sorry, I'm bouncing all over the place. The aim I'm trying to do is keep a Global Registry of the Modules and have the modules reference JUST that Core library and use the library to get the instances of the API to implement more features into the engine. But, due to .NET's natures of referencing things, and my habit of not wanting to use unsafe code and Marshalling information back and forth to get the same effect as the C99 version, I'm stumped.

Example:

Class Entry
{
	public dynamic SomeModule;
	
	public Entry()
	{
		SomeModule = ApiRegistry.GetApi("SomeOtherModule");
	}
	
	void SomeNewFreature()
	{
		var something = SomeModule.DoSomething();
		var somethingElse = SomeModule.DoSomething(with, args);
	}
	
	public void LoadModule()
	{
		ApiRegistry.RegisterApi(this);
		ApiRegistry.AddFeature(this, SomeNewFeature);
	}
}

Something along those lines. Of course this works in C99 since all you have to do is include the header, and grab the struct of registry from the core file. But here it's not exactly doing this.

So, should I just reference the modules I need into the other modules I'm creating, example of the window referencing the renderer? Or Should I press forward with a global registry?

My aim is flexibility, to allow functionality to be added without messing with the core internals of the engine. If anyone has any advice in regards to this, I would love to hear it. Even constructive criticism works, but not the “Why are you making an engine when you have X, Y, Z available!” type, that's not constructive. If that's all you're going to ask, please refrain from doing so. I'm doing this for fun, experience, and because I want to say “I did this!"

Advertisement

This is where good interfaces and SOLID principles are powerful.

The module that you're loading can access any feature in the main program. If you're in any of it's functions, those functions can reach out to any system, function, variable, or name that it knows.

If you're in the main system you need to follow known interfaces, that will take advantage of the L and D in SOLID. Anything in the main program should depend on interfaces and base classes and won't need to know about details of the object. The main system won't need to know or care if it was implemented in the main system or if it was implemented in a plugin library, all it needs to know is that the list contains a bunch of objects that implement the interface. If you get an object pointer, a function pointer, a callback, or anything else, you know it refers to some concrete objects somewhere that implements the principle regardless of how it came into existance.

The first interface the main system calls is the registration functions, as you're already doing. The library registers itself in whatever way it needs to get added to lists of functionality in the main system. The library injects itself into whatever systems it needs, adds itself to lists, provides callbacks, and gives pointers to whatever objects it needs for the main system to use it.

As an example, this is how we built many store objects in The Sims. The downloaded library would have a registration function which would allow the object to add itself to the list of available objects. The external library would implement a set of interfaces, like saying it was a vehicle, or a stereo source, that it was hauntable, was repairable, or whatever other interfaces it implemented. Since it was added to the list, any system could say “find me the nearest repairable object” and it would be found. Then a Sim could be told to run the repair interaction on the thing, never knowing or caring what the thing was, and it would just work.

@frob Very clear, and straight to the point, I appreciate you! I will attempt to implement SOLID into the plugin-in system and see how it goes. I'm somewhat scratching my head since I mainly ever worked with data files (.json, .xml, .pak, etc.) that I knew the current state and the expected object. This will be a bit of a challenge, but I won't give up that easily.

To ensure I got the gist of it, I need to implement base classes and interfaces, that the registry can reference in order to call the methods of the DLL, without having to actually know what the object is?

Beray said:
I need to implement base classes and interfaces, that the registry can reference in order to call the methods of the DLL, without having to actually know what the object is?

Yes, and ultimately it depends on whatever it is that you're implementing.

For example if you're already implementing a command pattern in your game, you can have an interface where all the objects implement DoCommand(). When you register it you add your objects to the list, and since they implement DoCommand() they are viable targets. This can work for tools that register in a menu. All they need to do in their initialization is register their callback in the menu system. The user can hit the button whenever they want.

Or maybe your plugin responds to an event. During registration it can register with the event listeners, creating a callback and tying itself to an event ID. When the event fires the main system will iterate through all listeners tied to the event ID and run their callback, which in turn will trigger the plugin's code. In that case the interface is the callback signature, providing a function pointer that meets the required signature.

Or maybe it's providing a fully constructed object, then adding the object to the list. It could initialize an object and add it to a pool. Now it's in the list, and as long as it meets the same base type as the other objects in the list, all the virtual functions will do whatever the other objects do. The engine won't know or care, it's just another object in the list that meets the same base class interface.

@frob Wow I got to say, you're completely right! I reconstructed the whole system to reference the interfaces and it works beautifully! Now I just have to finish restructuring everything so it makes sense because I just made a lot of hacky methods as a proof of concept.

I do appreciate your help, you guided me towards the correct solution, and I do love this approach much better!

This topic is closed to new replies.

Advertisement