Introducing adapters in Mud Engine
I've had a pretty configurable startup sequence with the Mud Engine for a bit now, but I wasn't really happy with it. The issue that I had was that you still performed to much initialization logic in a boot strap class, or in the server app startup (not the server implementation themselves).
I wanted to make the whole configuration piece of the engine more extensible, and easier to plug into. The engine already uses a custom message brokering pipeline to publish messages to subscribers, and so I wanted to harness that a bit with the configuration.
Adapters
There is a new interface in the engine called IAdapter
. It provides a series of methods for letting adapters subscribe to messages published from within the engine, along with publishing their own. It's an IInitializableComponent
, so it can be initialized and deleted. This allows for adapters to perform cleanup, such as unsubscribing from the message broker, when they are being deleted.
The IAdapter
includes a Name
property so that the adapter can have a name; most importantly though is the Start(IGame)
method. This method allows your adapters to spin off their own background tasks and do what ever they want while the game continues to run.
The engine currently has a WorldManager
adapter, which initializes all of the worlds it is given and starts the world clocks/weather climate clocks etc. The engine server is being modified as well allowing it to become an adapter that starts up and runs when the game starts.
How it works
So how do adapters fit in to the workflow of the engine? Adapters are registered with a modified IGameConfiguration
interface. This interface is then given to an IGame
when you call Configure()
on the game instance. The default IGame
implementation (MudGame
) pulls all of the adapters out of the configuration class and initializes them when IGame.Initialize()
is invoked. It then starts each one of the adapters when the IGame.StartAsync()
method is invoked. When IGame.StartAsync()
is invoked, an internal game loop is created and the game will forever run. Because of this, adapters must not perform any long running or blocking tasks. If you have to perform a long running operation, such as running a server, spin it up on a worker thread and do the work there.
Example Game setup
I wrote a quick unit test to demonstrate how you can setup a game using adapters.
private async Task TestGameStartup()
{
// Mocks & Adapters
IWorldFactory worldFactory = Mock.Of<IWorldFactory>();
IAdapter server = Mock.Of<AdapterBase>();
IAdapter worldManager = new WorldManager(worldFactory);
// Create our game configuration
IGameConfiguration configuration = new GameConfiguration { Name = "Sample Mud Game", };
configuration.UseAdapter(server);
configuration.UseAdapter(worldManager);
// Setup and run the game.
IGame game = new MudGame();
await game.Configure(configuration);
await game.StartAsync();
}
The above code creates a fake world factory, as an IWorldFactory
implementation is required by the WorldManager
adapter. I also create a fake server by mocking an adapter base class that the engine provides.
Once my adapters are setup, I create a new GameConfiguration
and pass the adapters in to the config using the UseAdapter
method. There is also a generic version of this method (UseAdapter<T>
).
Lastly, we create a new MudGame
, configure the game using our new GameConfiguration
instance and then we start the game. Note that game.Initialize()
did not get called here. That is because the MudGame
class checks if it has been initialized upon starting and initializes itself and all its adapters if that has not happened yet.
At this point, the awaited StartAsync
method will not end until something within the engine signals it needs to end, or something calls game.Stop()
. It creates an internal game loop and runs.
There is another approach that can be used for those that want a custom game loop.
game.BeginStart(runningGame =>
{
Task.Run(() =>
{
while (runningGame.IsRunning)
{
// Thread.Sleep(1) is not available on Portable Libraries.
Task.Delay(1).Wait();
}
});
});
This will instead start the game and invoke your callback. This lets you run the game loop in a Task for things like an editor, and not block and UI threads. Your editor or client can still stop the game by invoking game.Stop()
. As long as your while
loop checks for IsRunning
, you're game loop will safely be shut down.
There's a lot of additional new stuff that I couldn't fit in to this post, so I'll work on making more frequent posts with the change that have taken place over the last couple of months.
For now, EOF.