One of the things to keep in mind when doing this is cross-service communication. How is one service going to talk to another? In the example presented, each service is going to register for the types of events that it wishes to be notified of. It will be a bit clearer when the code is presented.
First things first, let's define the service interface.
interface IService
+ void RegisterEvents(ServiceManager m)
+ void ExecuteFrame(ServiceManager m)
Each IService must be able to register events with the service manager, and each service must expose a method that the service manager can call to process events.
The other primary component is the Message.
class Message
+ public IService Sender {get; set;}
+ public EventArgs EventArguments {get; set;}
A message contains a sender, and event arguments. Sender tells you what service the message came from, and EventArguments contains other arguments that a service would want.
The Services Manager class is responsible for keeping track of the services and routing the messages between the services.
class ServiceManager
+ public delegate void OnRecievedMessage(Message m);
+ Dictionary<Type, IService> _services;
+ List<Tuple<Type, IService, OnRecievedMessage>> _routes;
+ public void RegisterService(Type service) { ... }
+ public void UnregisterService(Type service) { ... }
+ private void UnregisterEventHandler(IService sender) { ... }
+ public void RegisterEventHandler
(Type eventsArgType, OnRecievedMessage m) { ... }
+ public void SubmitMessage
(IService sender,
Type eventArgsType, EventArgs message) { ... }
+ public void Execute() { ... }
** OnRecievedMessage is the delegate for when a service receives a message.
** _services is a dictionary of service types mapped to the service. That means for each type, there can be exactly one service.
** _routes specifies the path each message takes when it's dispatched. If you haven't seen a Tuple before, it can be considered to be a read only, fixed length list. This establishes a many to many mapping between the types of messages, and delegates that process messages.
If you had a process that was responsible for switching states, and needed to listen for the pause button, and a state responsible for processing immediate game events, they both might listen for a key down event.
The other thing to note is that any process may dispatch any type of event. If you are writing a multi-player game, and it supports local input as well as network input, the network service might send the same player movement actions as the physics service.
** RegisterService is used to register a new service with the game server manager. Notice that instead of registering an instance of an existing service, you register a type, so that the server manager itself instantiates the object.
public void RegisterService(Type service)
{
_services[service] =
(IService)Activator.CreateInstance(service);
_services[service].RegisterEvents(this);
}
** UnregisterService is used to take a service out of circulation, including all of the routes that said service might deliver messages.
public void UnregisterService(Type service)
{
var s = _services[service];
UnregisterEventHandler(s);
_services.Remove(service);
}
** UnregisterEventHandler is called internally to remove the services that have been registered when the service was created.
private void UnregisterEventHandler(IService sender)
{
for (int i = _routes.Count - 1; i >= 0; i--)
{
if (_routes[i].Item2 == sender)
_routes.RemoveAt(i);
}
}
** RegisterEventHandler is called by a service to add an event to the routing map. It's not really something that should be exposed to anything but the Service, but it is what it is. C# doesn't support the friend keyword like C++ does.
public void RegisterEventHandler(Type eventsArgType,
IService sender,
OnRecievedMessage m)
{
_routes.Add(
new Tuple<Type, IService, OnRecievedMessage>
(eventsArgType, sender, m));
}
** The SubmitMessage method is used to insert a message for the services to consume.
public void SubmitMessage(IService sender,
Type eventArgsType,
EventArgs message)
{
(from r in _routes
where r.Item1 == eventArgsType
select r.Item3)
.ToList()
.ForEach(m => m(new Message() {
EventArguments = message,
Sender = sender }));
}
** Execute is called each frame, so that each of the services can execute their core. Recall ExecuteFrame from the IService interface.
public void Execute()
{
_services.Values
.ToList()
.ForEach(x => x.ExecuteFrame(this));
}
Activation of the services would look something like this
var sm = new ServicesManager();
sm.RegisterService(typeof(InputService));
sm.RegisterService(typeof(GameService));
If we wanted to execute the game for 10 steps, it would look like this.
for (int i = 0; i < 10; i++)
sm.Execute();
To unregister a service.
sm.UnregisterService(typeof(GameService));
A sample implementation of InputEventArgs and InputService
class InputEventArgs:EventArgs
{
public char ScanCode { get; set; }
}
class InputService:IService
{
public void RegisterEvents(ServicesManager m)
{}
public void ExecuteFrame(ServicesManager m)
{
m.SubmitMessage(this,
typeof(InputEventArgs),
new InputEventArgs() {ScanCode = 'a' });
}
}
Each time ExecuteFrame is called, this sample service puts a new message into the queue.
This is the sample implementation for a game service, it a very trivial implementation.
class GameService : IService
{
public void RegisterEvents(ServicesManager m)
{
m.RegisterEventHandler(
typeof(InputEventArgs),
this,
(a) =>
{
Console.WriteLine(
(a.EventArguments
as InputEventArgs).ScanCode);
}
);
}
public void ExecuteFrame(ServicesManager m)
{ }
}
That more or less wraps it up. This isn't 'the' way to do it, there's certainly much better ways to do it. You could have each service executing in it's own thread. Some people would rather not use tuples. The takeaway should be that this is another tool you throw in your toolbox.
Awesome post! I've been reading about SOA on the macro level, and your explanation of a service model for games was really interesting.
ReplyDeleteIt seems that the service model requires an object of some kind to coordinate/manage them. This is what's interesting to me. I've been trying to figure out an optimal way to architect a program such that it be as modular/loosely-coupled as possible - just a bunch of unrelated objects. I'm coming to think, however, that while it is possible to make most of a program's components uncoupled, a manager of some kind is still required to communicate state between the components, passing messages. This is the strategy that some Javascript developers advocate for developing large JS apps.
Interesting! So, if a component manager is required, I wonder if there is a proposed standard practice for its interface and responsibilities...