http://gameprogrammingpatterns.com/component.html 中将Component模式归类于Decouple Pattern, 用来解耦. 那Component之间如何交互呢. MonoGame提供了GameService.
官方的介绍如下:https://msdn.microsoft.com/en-us/library/bb203873.aspx
Game services
Game services are a mechanism for maintaining loose coupling between objects that need to interact with each other. Services work through a mediator—in this case, Game.Services. Service providers register with Game.Services, and service consumers request services from Game.Services. This arrangement allows an object that requires a service to request the service without knowing the name of the service provider.
Game services are defined by an interface. A class specifies the services it provides by implementing interfaces and registering the services with Game.Services. A service is registered by calling Game.Services.AddService specifying the type of service being implemented and a reference to the object providing the service. For example, to register an object that provides a service represented by the interface IMyService, you would use the following code.1
Services.AddService( typeof( IMyService ), myobject );
Once a service is registered, the object providing the service can be retrieved by Game.Services.GetService and specifying the desired service. For example, to retrieve IGraphicsDeviceService, you would use the following code.1
IGraphicsDeviceService graphicsservice = (IGraphicsDeviceService)Services.GetService( typeof(IGraphicsDeviceService) );
Game Components Consuming Game Services
The GameComponent class provides the Game property so a GameComponent can determine what Game it is attached to. With the Game property, a GameComponent can call Game.Services.GetService to find a provider of a particular service. For example, a GameComponent would find the IGraphicsDeviceService provider by using the following code.1
IGraphicsDeviceService graphicsservice = (IGraphicsDeviceService)Game.Services.GetService( typeof( IGraphicsDeviceService ) );
上面的介绍已经很清楚了, 可以追加一点的是, 提供Service的可以是GameComponent, 也可以不是GameComponent.
对于Game services, 我看过解释得最好的是下面这个链接(非常棒)http://zacharysnow.net/2010/02/18/creating-and-consuming-services-in-your-xna-game.html
全文摘录如下:
Creating and consuming services in your XNA Game
February 18, 2010 .NET 0 Comments
The GameServiceContainer implements the IServiceProvider interface and the MSDN documentation says about the IServiceProvider interface:
Defines a mechanism for retrieving a service object; that is, an object that provides custom support to other objects.
This article will attemp to describe how can you use the GameServiceContainer in your XNA game, in both your GameComponent(s) and your game’s entity objects.< !—more—>The most obvious place to use the GameServiceContainer is in your GameComponent(s). But first, lets talk about Coupling(耦合). Let’s assume you have the following components:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class FooComponent : GameComponent
{
public FooComponent(Game game)
: base(game)
{
}
public int DoFoo()
{
// Do something and return an int.
}
}
class BarComponent : GameComponent
{
FooComponent foo;
public BarComponent(Game game)
: base(game)
{
this.foo = new FooComponent(game);
}
public void DoBar()
{
int result = this.foo.DoFoo();
// Do something based on result.
}
}
There’s nothing wrong with the code, but BarComponent has a dependency on FooComponent(BarComponent直接依赖FooComponent). BarComponent directly interacts with FooComponent and therefore any change made to FooComponent indirectly affects BarComponent(任何对FooComponent的改动都会间接影响BarComponent). For instance, let’s assume the constructor for FooComponent needs to be modified. That means we now have to update not only the FooComponent class but as well the BarComponent class(例如修改了FooComponent的构造方法, 则BarComponent也需要做相应修改). Throw in a few more components with dependencies on FooComponent and you could start to get headache really fast. This design is highly coupled(高耦合).
Let’s try a slight redesign:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class FooComponent : GameComponent
{
public FooComponent(Game game)
: base(game)
{
}
public int DoFoo()
{
// Do something and return an int.
}
}
class BarComponent : GameComponent
{
FooComponent foo;
public BarComponent(Game game, FooComponent foo)
: base(game)
{
this.foo = foo;
}
public void DoBar()
{
int result = this.foo.DoFoo();
// Do something based on result.
}
}
We’ve now eliminated the construction of the FooComponent from within the BarComponent. The design is better but still not that great. BarComponent is still directly relying on and communicating with FooComponent. We want to change BarComponent so that it has no direct dependency on a concrete implementation of FooComponent. 这也是一种依赖注入, 但还不彻底
We’ll create an interface(面向接口编程):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34interface IFooService
{
int DoFoo();
}
class FooComponent : GameComponent, IFooService
{
public FooComponent(Game game)
: base(game)
{
}
public int DoFoo()
{
// Do something and return an int.
}
}
class BarComponent : GameComponent
{
IFooService foo;
public BarComponent(Game game, IFooService foo)
: base(game)
{
this.foo = foo;
}
public void DoBar()
{
int result = this.foo.DoFoo();
// Do something based on result.
}
}
We can now change FooComponent as much as we want and BarComponent will be unaffected. BarComponent now communicates with FooComponent through the IFooService interface(接口隔离). This also allows us to have multiple implementations of DoFoo():1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class SimpleFooComponent : GameComponent, IFooService
{
public SimpleFooComponent(Game game)
: base(game)
{
}
public int DoFoo()
{
return 5; // The class says “Simple”
}
}
class ComplexFooComponent : GameComponent, IFooService
{
public ComplexFooComponent(Game game)
: base(game)
{
}
public int DoFoo()
{
int result = 0;
// Do some very complex calculationreturn result;
}
}
We can pass BarComponent an instance of SimpleFooComponent or ComplexFooComponent. Whatever the situation may call for.
Where does GameServiceContainer fit into all of this? You can use the GameServiceContainer to hold all your “Services”. Add whatever class will implement the IFooService and then from within your BarComponent you can query for it:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26class BarComponent : GameComponent
{
IFooService foo;
public BarComponent(Game game)
: base(game)
{
}
public override void Initialize()
{
this.foo = this.Game.Services.GetService(typeof(IFooService)) as IFooService;
if(this.foo == null)
throw new InvalidOperationException(“IFooService not found.”);
}
public void DoBar()
{
int result = this.foo.DoFoo();
// Do something based on result.
}
}
// In your Game’s constructor
this.Services.AddService(typeof(IFooService), new SimpleFooComponent(this));
Not only does BarComponent no longer require an instance of IFooService in its constructor, it also no longer matters if the instance of IFooService is constructed before or after the BarComponent. So long as all the services BarComponent requires are in the GameServiceContainer before Initialize() is called, it doesn’t matter what order your components are constructed in. Now, suppose that BarComponent didn’t necessarily depend on IFooService and instead the behavior of DoBar() is changed based on whether or not IFooService is available:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class BarComponent : GameComponent
{
IFooService foo;
public BarComponent(Game game)
: base(game)
{
}
public override void Initialize()
{
this.foo = this.Game.Services.GetService(typeof(IFooService)) as IFooService;
}
public intDoBar()
{
// If the IFooService is available, delegate to the DoFoo() method.if(this.foo != null)
return this.foo.DoFoo();
int result = 0;
// Otherwise do some other calculation.return result;
}
}
这一点是很微妙的, Initialize对BarComponent是全局的, DoBar对BarComponent是局部的, 从Initialize移到DoBar, 进一步降低耦合性
Service providers don’t always have to be GameComponent(s). Our BarComponent needs a Camera class now:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44interface ICamera
{
Matrix Transform { get; }
}
class IdentityCamera : ICamera
{
public Matrix Transform
{
get { return Matrix.Identity; }
}
}
class MovingCamera : ICamera
{
public Matrix Transform
{
get;
set;
}
}
class BarComponent : DrawableGameComponent
{
ICamera camera;
public BarComponent(Game game)
: base(game)
{
}
public override void Initialize()
{
this.camera = this.Game.Services.GetService(typeof(ICamera)) as ICamera;
}
public override void Draw(GameTime gameTime)
{
Matrix transform = this.camera.Transform;
// Draw based on the transform matrix
}
}
// In your Game’s constructor.
this.Services.AddService(typeof(ICamera), new MovingCamera());
BarComponent uses the camera’s Transform matrix and doesn’t care how it is calculated. It’s completely decoupled from the camera’s implementation.
In closing, using the GameServiceContainer and interfaces makes your classes more loosely coupled. This makes it easier to make changes to the way your game works. Your classes also become more reusable as you can now mix and match service providers and consumers as needed. If you need a specific implementation of a camera for your game, you can still use the BarComponent so long as your camera class implements the ICamera interface.
Loosely coupling your classes has the added benefit of making them more testable. That’s another blog post though.
GameServiceContainer和IServiceProvider在Monogame中的实现如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73// MIT License - Copyright (C) The Mono.Xna Team
// This file is subject to the terms and conditions defined in
// file 'LICENSE.txt', which is part of this source code package.
using System;
using System.Collections.Generic;
using Microsoft.Xna.Framework.Utilities;
namespace Microsoft.Xna.Framework
{
public class GameServiceContainer : IServiceProvider
{
Dictionary<Type, object> services;
public GameServiceContainer()
{
services = new Dictionary<Type, object>();
}
public void AddService(Type type, object provider)
{
if (type == null)
throw new ArgumentNullException("type");
if (provider == null)
throw new ArgumentNullException("provider");
if (!ReflectionHelpers.IsAssignableFrom(type, provider))
throw new ArgumentException("The provider does not match the specified service type!");
services.Add(type, provider);
}
public object GetService(Type type)
{
if (type == null)
throw new ArgumentNullException("type");
object service;
if (services.TryGetValue(type, out service))
return service;
return null;
}
public void RemoveService(Type type)
{
if (type == null)
throw new ArgumentNullException("type");
services.Remove(type);
}
public void AddService<T>(T provider)
{
AddService(typeof(T), provider);
}
public T GetService<T>() where T : class
{
var service = GetService(typeof(T));
if (service == null)
return null;
return (T)service;
}
}
}
public interface IServiceProvider
{
Object GetService(
Type serviceType
)
}
这种设计方法也是一种设计模式: Service Locator(http://gameprogrammingpatterns.com/service-locator.html)
The Service Locator pattern is a sibling to Singleton in many ways, so it’s worth looking at both to see which is most appropriate for your needs.
The Unity framework uses this pattern in concert with the Component pattern in its GetComponent() method.在Unity3D中, GameObject.GetComponent/GetComponentInParent也是同样的方式.
Microsoft’s XNA framework for game development has this pattern built into its core Game class. Each instance has a GameServices object that can be used to register and locate services of any type.
如同前一篇Component模式在Unity3D和XNA中的异同, Service Locator模式在Unity3D和XNA中的异同是一样的. 区别在于Unity3D中是在Entity的level, 而XNA是在Game的level.
这种模式并非游戏专用, 在MSDN中有更一般化的介绍https://msdn.microsoft.com/en-us/library/ee413842.aspx
You have classes with dependencies on services whose concrete types are specified at compile time. In the following example, ClassA has compile time dependencies on ServiceA and ServiceB. The following diagram illustrates this.
Classes with dependencies on services
This situation has the following drawbacks:
- To replace or update the dependencies, you must change your classes’ source code and recompile the solution.
- The concrete implementation of the dependencies must be available at compile time.
- Your classes are difficult to test in isolation because they have a direct reference to their dependencies. This means that these dependencies cannot be replaced with stubs or mock objects.
- Your classes contain repetitive code for creating, locating, and managing their dependencies.
The next section describes how to address these issues.
Objectives
Use the Service Locator pattern to achieve any of the following objectives:
- You want to decouple your classes from their dependencies so that these dependencies can be replaced or updated with little or no change to the classes.
- You want to write logic that depends on classes whose concrete implementation is not known at compile time.
- You want to be able to test your classes in isolation, without the dependencies.
- You do not want the logic that locates and manages the dependencies to be in your classes.
- You want to divide your application into loosely coupled modules that can be independently developed, tested, versioned, and deployed.
Solution
Create a service locator that contains references to the services and that encapsulates the logic that locates them. In your classes, use the service locator to obtain service instances. The following diagram illustrates how classes use a service locator.
How classes use a service locator
The Service Locator pattern does not describe how to instantiate the services. It describes a way to register services and locate them. Typically, the Service Locator pattern is combined with the Factory pattern and/or the Dependency Injection pattern. This combination allows a service locator to create instances of services.