1.8实现GameServices
问题
如在教程1-6中解释的那样,你可以将代码放在可复用的GameComponent类中,这些组件可以是相机、粒子系统,用户输入,billboard等。
使用GameComponent类的一个最主要的好处是你可以很容易在它们之间切换,例如相机模式。要把第一人称相机模式变到四元数相机模式只需在Game类的Initialize方法中包含一条代码即可。
但你必须保证在切换组件时不会改变其余代码(这些代码都要使用相机)。
解决方案
你可以让相机组件拥有同一个接口,例如(自定义)ICameraInterface接口。当初始化相机组件时,你需要让Game类知道从现在开始Game中要包含一个实现ICameraInterface接口的组件。用XNA的话说就是组件将自己注册为一个ICameraInterface类型的GameService。
一旦做完上步,代码的其他部分只需简单地向Game类请求当前的ICameraInterface服务,Game类会返回提供当前ICameraInterface服务的相机。这意味着你的代码无需知道现在的相机模式究竟是第一人称相机模式还是四元数相机模式。
工作原理
接口(interface)用来定义一种程序的协定,一个接口包含功能(就是方法)的列表,当你的类实现一个接口后,就必须实现接口中所定义的方法。
下面的代码是如何定义一个ICameraInterface接口:
interface ICameraInterface { Vector3 Position { get;} Vector3 Forward { get;} Vector3 UpVector { get;} Matrix ViewMatrix { get;} Matrix ProjectionMatrix { get;} }
所有实现ICameraInterface接口的类必须实现这五个getter方法。无论是第一人称相机还是四元数相机,只要是实现这个接口,那么主程序就一定能访问这五个字段,对于其余代码,知道当前相机是第一人称模式还是四元数模式并不重要,唯一重要的是相机能产生View和Projection矩阵,或许还有一些其他的方向向量。所以你应该为Game类实现一个拥有ICameraInterface 的相机。
让GameComponent实现接口
本例中有两个相机组件,因为你不在屏幕上绘制东西,所以没必要使用DrawableGameComponent,所以相机组件是从GameComponent类继承的。你还要在组件上实现接口:
class QuakeCameraGC : GameComponent, ICameraInterface { . . . }
注意虽然一个类只能从一个基类继承(本例中是GameComponent类),但它可以实现多个接口。
下一步你必须确定在类中实现接口中定义的方法。在教程2-3和2-4的QuakeCamera类和Quaternion类中已经实现了。
注册ICameraInterface Service
在一个时间你必须有一个而且只能有一个相机类提供ICameraInterface服务。当你激活一个相机组件,必须要让Game类知道这个相机组件是当前的ICameraInterface,这样才能在其他代码请求当前ICameraInterface的提供者时让Game类返回这个相机。
你可以通过将这个相机注册为Game类的Services 集合的一个GameService实现这一步:
public QuakeCameraGC(Game game) : base(game) { game.Services.AddService(typeof(ICameraInterface), this); }
你将这个对象(新创建的第一人称相机组件)添加到接口集合中并表明提供ICameraInterface 服务。
使用方法
当你想使用相机时(例如获取View和Projection矩阵),你应要求Game类返回当前的ICameraInterface。在返回的对象中,你可以获取所有在ICameraInterface中定义的字段。
protected override void Draw(GameTime gameTime) { device.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, Color.CornflowerBlue, 1, 0); ICameraInterface camera; camera = (ICameraInterface)Services.GetService(typeof(ICameraInterface)); cCross.Draw(camera.ViewMatrix, camera.ProjectionMatrix); base.Draw(gameTime); }
在Draw方法中的代码不知道是第一人称相机还是四元数相机,只知道在ICameraInterface中定义的方法。这种做法的优点是你可以很容易的切换相机模式,因为其他代码并不知道这个变化。
注意在Game类的Initialize方法中你还定义了一个camera作为全局变量存储了当前的ICameraInterface的链接。
使用多个GameComponents
对于保证不同GameComponent类的互操作性来说,GameServices是很有用的。本例中你有一个提供IcameraInterface服务的相机组件,那么其他组件类就可以通过查询IcameraInterface服务的方式获取这个相机。
这意味着你无需提供不同组件间的生硬的链接,例如前面的教程中的丑陋的Update方法。本例中你创建一个提供IcameraInterface服务的相机组件,就可以从任意其他组件访问到这个服务,例如从billboard组件的Initialize方法中:
public override void Initialize() { device = Game.GraphicsDevice; camera = (ICameraInterface)Game.Services.GetService(typeof(ICameraInterface)); base.Initialize(); }
接下来,billboard组件的Update和Draw方法就可以访问到相机的字段了。
Game类需要初始化相机和billboard组件。相机组件会将自己订阅到IcameraInterface服务,允许billboard组件访问相机。
相机会自动更新,之后billboard组件也会自动更新,billboard组件可以通过IcameraInterface服务访问相机,然后绘制自己。整个处理过程需要在Game类中添加两行代码,你可以轻松地在相机的两种模式间切换。
改变GameComponent的更新顺序
本例中你的billboard组件需要相机组件的输出,所以你应保证相机在billboard之前更新。可以在Game类中将组件添加到Components 集合之前进行此步:
public Game1() { graphics = new GraphicsDeviceManager(this); Content.RootDirectory = "Content"; //GameComponent camComponent = new QuakeCameraGC(this); GameComponent camComponent = new QuatCamGC(this); GameComponent billComponent = new BillboardGC(this); camComponent.UpdateOrder = 0; billComponent.UpdateOrder = 1; Components.Add(camComponent); Components.Add(billComponent); }
首先你创建了相机和billboard组件,在将它们添加到Components集合前,你设置了它们的更新顺序,小的数字表示先更新。
使用GameComponent和GameServices切换相机模式只需改变程序中的一行代码。在前面的代码中, quaternion模式处在激活状态。如果你想切换到Quake模式,只需去掉QuakeCamera GameComponent类前的注释,并在QuatCam GameComponent类前加上注释。其余代码无需改动,因为需要的只是相机组件提供的IcameraInterface服务。
代码
前面显示的Game1构造函数代码就是获取相机并使billboard工作的所有代码,Initialize, (Un)LoadContent,Update和Draw方法为空。当相机组件创建后,它将自己注册到IcameraInterface服务:
public QuakeCameraGC(Game game) : base(game) { game.Services.AddService(typeof(ICameraInterface), this); }
无论何时其他代码需要使用相机,你只需要求Game类返回ICameraInterface服务即可:
ICameraInterface camera; camera = (ICameraInterface)Services.GetService(typeof(ICameraInterface)); cCross.Draw(camera.ViewMatrix, camera.ProjectionMatrix);