Torque X提供了组件系统来继承游戏对象。如果你查看内置的入门包的话,你会发现大多数游戏功能都是在组件中实现的。这份文档描述了组件系统以及它是如何工作的。
继承游戏对象
在深入研究组件到底是什么之前,先来考虑一个基本的实现问题。
在大多数面向对象语言中,包括C#,给一个对象添加新方法的主要内置方法是继承该类并修改子类。例如,假设你需要为一个特殊的游戏对象添加物理和碰撞功能,这个类叫做Player。你实现了Player类的逻辑。然后,你发现你还有一个类叫做Monster,它也需要同样的功能。你可以从Player类中复制粘贴代码到Monster类——这显然是个坏主意,因为除了在Monster类中集成新代码会出现很多bug,更倒霉的是你现在有两份重复的代码需要维护。当然你也可以创造一个包含物理和碰撞功能的代码的新类,然后让Player类和Monster类同时继承这个新类。这听上去是合理的,所以你新建了BaseActor类,然后将物理和碰撞的代码放到这个类中去。
不久之后你希望你的Player类能够拾取游戏对象。所以你向Player类中添加了拾取的代码。但是如果Monster类也需要拾取的代码,这时拾取的代码就要提升至父类BaseActor。同时,你添加了一个NPC类,继承于BaseActor。当然你肯定不希望NPC也能够拾取物品。但是现在你陷入了困境,因为NPC从BaseActor继承了拾取的功能,并且没有方法来禁用这个功能。但是!你修正了这个bug:通过向BaseActor添加一个“启用拾取功能”的标志量,然后添加一些if语句。现在NPC继承于BaseActor,但是它的拾取功能被禁用了。
你也许会认为BaseActor中的代码此时一定非常丑陋。是的!你是对的,但是事实是,这仅仅是一个噩梦的开始。不久之后你的代码就会变得臃肿、复杂、晦涩并且难以维护。
上面的这个例子描述了滥用继承带来的问题。继承是很强大的工具,但是必须正确地使用。我要告诉你的是,还有其他的办法能够更好地解决这个问题。一个替代继承的方法是聚合。(省略聚合的定义若干字,不知道的回家打屁股)。最后,你所有的对象会变成一个包裹,诸如拾取、碰撞这些工具你只需要放入包裹或者从包裹中移除就可以了。
使用Torque X组件来解决问题
Torque X组件系统是一个聚合模型的实现。一个组件是一个能够向一个集合对象添加功能的小对象。现在使用组件系统来解决我们上面提到的例子,我们可以先创建一个BaseActor类,该类继承于TorqueObject类。这样BaseActor就拥有了包含组件的能力。接着我们定义一些组件:PhysicsComponent、CollisionComponent、PickupComponent,并且让它们继承于TorqueComponent (一切组件的基类)。我们甚至不需要为Player、Monster和NPC单独创建子类。我们只需要使用一个命名的对象或对象类型遮罩(mask)来表明这几个对象间的区别就可以了。下面是简单的示例代码:
// declare the classes that we will need
public class BaseActor : TorqueObject
{
// implementation omitted
}
public class PhysicsComponent : TorqueComponent
{
// implementation omitted
}
public class CollisionComponent : TorqueComponent
{
// implementation omitted
}
public class PickupComponent : TorqueComponent
{
// implementation omitted
}
public class Game
{
public static void Main()
{
BaseActor player = new BaseActor();
player.ObjectType = TorqueObjectDatabase.Instance.GetObjectType("player");
player.Components.AddComponent(new PhysicsComponent());
player.Components.AddComponent(new CollisionComponent());
player.Components.AddComponent(new PickupComponent());
BaseActor monster = new BaseActor();
monster.ObjectType = TorqueObjectDatabase.Instance.GetObjectType("monster");
monster.Components.AddComponent(new PhysicsComponent());
monster.Components.AddComponent(new CollisionComponent());
monster.Components.AddComponent(new PickupComponent());
BaseActor npc = new BaseActor();
npc.ObjectType = TorqueObjectDatabase.Instance.GetObjectType("npc");
npc.Components.AddComponent(new PhysicsComponent());
npc.Components.AddComponent(new CollisionComponent());
// run the game
// ...
}
}
这里组件系统解决了一下几个问题:
聪明的你可以会意识到上面给出的组件示例有一点不和谐的地方。在原始的继承模型中,我们为游戏对象定义了新类,比如Player、Monster、NPC。在组件模型中,我们没有定义任何新类,取而代之的使我们创建了玩家、怪物和游戏人物的实例。设想我们想拥有许多怪物?在继承模型之中,我们仅仅需要简单的几步就可以做到:
// Create a hoard of vicious Monsters!
for (int i = 0; i < 100; ++i)
{
Monster m = new Monster();
m.Position = FindMonsterSpawn(); // FindMonsterSpawn() will find a unoccupied spawn point for our monster.
TorqueObjectDatabase.Instance.Register(m);
m.Attack(player);
}
那么我们如何在组件模型中应对这种情况?难道你要重复5行代码(请参看上面的代码,的确是5行)在每一处你需要创建怪物的地方?这太痛苦了。幸运的是,我们不需要这样做。取而代之的是,我们可以创建自己的基于组件的对象,它叫做模板。然后我们可以新建一个模板的实例。如果你还没有阅读模板指南,下面的讨论会有点陌生。但是我建议你继续阅读下去,然后再去看模板指南。
模板系统让我们能够创建基于组件的对象,并且将它变为模板。待会我们可以克隆模板来创造新的对象,并将该对象在引擎中注册。下面是示例代码:
// Create the monster template BaseActor monsterTemplate = new BaseActor(); monsterTemplate.Name = "MonsterTemplate"; monsterTemplate.ObjectType = TorqueObjectDatabase.Instance.GetObjectType("monster"); monsterTemplate.IsTemplate = true; monsterTemplate.Components.AddComponent(new PhysicsComponent()); monsterTemplate.Components.AddComponent(new CollisionComponent()); monsterTemplate.Components.AddComponent(new PickupComponent());
注意,为了将我们的怪物变为模板,我们做出了三处调整:
现在我们拥有了怪物的配置代码,并将它置于模板中。当我们需要创建一个怪物时,我们只需要克隆这个模板,按需配置返回的对象,然后注册它。所有被模板包含的组件都会出现在克隆之后的对象中。除了IsTemplate标志量被设为false。请看示例代码:
// Create a hoard of vicious Monsters! for (int i = 0; i < 100; ++i) { BaseActor m = (BaseActor)monsterTemplate.Clone(); m.Position = FindMonsterSpawn(); // FindMonsterSpawn() will find a unoccupied spawn point for our monster. TorqueObjectDatabase.Instance.Register(m); m.Attack(player); // ex-terrrrrr-minate! }
和XNA组件的差异
XNA框架通过自己的组件模型提供了很好的可扩展性。这个系统和Torque的组件系统共享一个名字,其实概念上也是相近的。但是他们却是出于两种不同目的的不同系统!Torque系统的目的是让定义Torque游戏对象更加容易,然后XNA系统的目的是让子系统更容易地被平滑地集成到一个新游戏中去。换句话说,XNA系统是游戏的组件模型,然而Torque系统时游戏中的对象的组件模型。Torque X完全兼容XNA组件模型。事实上,大多数底层的引擎都是继承于XNA组件的,Torque X也不例外(这不废话嘛)。
另一个组件的例子
这里有一个例子,它展示了组件的逻辑的更多细节以及如何将它和引擎集成。强烈建议你先阅读模板指南然后再来看下面的代码。
为了明白Torque X组件系统是如何工作的,考虑下面的代码。它在一个反坦克飞机中创建了一个反坦克炮弹(这个例子在demo目录下大家自己先去看看,背景有一点3D的效果,强烈推荐)。其实组件往往不是通过代码创建的,而是通过xml。为了能够让大家看的更明白,我们将创建的流程提取出来,并使用C#代码表示。
// set up sprite _tankTemplate = new T2DStaticSprite(); _tankTemplate.Material = (DefaultEffect)TorqueObjectDatabase.Instance.FindObject("TanketteMaterial"); _tankTemplate.Size = new Vector2(15.0f, 14.0f); _tankTemplate.Layer = Game.Instance.Ground.Layer - 1; _tankTemplate.ObjectType = TanketteObjectType; // add tank smarts TankAIComponent ai = new TankAIComponent(); ai.ProjectileTemplate = GetProjectileTemplate(); _tankTemplate.Components.AddComponent(ai); // add some mount points _tankTemplate.Components.AddComponent(new T2DLinkPointComponent()); _tankTemplate.LinkPoints.AddLinkPoint("turret", new Vector2(-0.5f, -1.0f), 0.0f); _tankTemplate.LinkPoints.AddLinkPoint("dust", new Vector2(0.75f, 0.5f), 0.0f); // add combustible so we blow up CombustibleComponent boom = new CombustibleComponent(); boom.OnGround = true; boom.CollidesWith = BombObjectType + PlayerObjectType; boom.DestroyOnCollision = true; boom.ExplosionTemplate = GetBombExplosionTemplate(); boom.Offset = new Vector2(0, boom.ExplosionTemplate.Size.Y * 0.5f); boom.CameraShake= new Vector2(1.25f, 1000.0f); _tankTemplate.Components.AddComponent(boom); // mount turret T2DStaticSprite turret = (T2DStaticSprite)TorqueObjectDatabase.Instance.FindObject("TanketteCannon"); turret.Mount(_tankTemplate, "turret", true); turret.TrackMountRotation = false; // mount dustcloud T2DAnimatedSprite dustCloud = (T2DAnimatedSprite)TorqueObjectDatabase.Instance.FindObject("DustCloud"); dustCloud.Mount(_tankTemplate, "dust", true);
上面的代码片段创造了一个简单的精灵(不知道什么是精灵?打板子),向其添加了一些组件,现在它变成了一个相对复杂的对象了。首先,我们添加了TankAIComponent,它给予了精灵瞄准并打枪(是的!你没看错!是打枪!)的能力。然后我们创建一个易燃物组件,当我们被炮弹和其他物体击中时来制造一点爆炸效果。当然了,这个类不仅仅可以用于反坦克飞机,还可以被许多其他对象使用。现在我们要来添加一个边界检测类,来确保当我们的反坦克飞机从屏幕中消失时正确地从游戏中移除。
下面是边界检测类的代码:
class BoundsCheckerComponent : TorqueComponent, IAnimatedObject { //====================================================== #region Public methods public void UpdateAnimation(float elapsed) { T2DSceneCamera cam = T2DSceneGraph.Instance.Camera as T2DSceneCamera; if (_sceneObj.Position.X < cam.SceneMin.X - _sceneObj.Size.X) { Owner.Manager.Unregister(Owner); return; } } #endregion
上面的代码是边界检测组件的修改动画的回调函数。它通过检测对象是否位于摄像头的可视范围内来确定它是否还在屏幕内。如果对象出了屏幕,我们把它注销。我们可以通过将它的MarkedForDelete属性值为true来完成这一步。
//======================================================
#region Private, protected, internal methods protected override bool _OnRegister(TorqueObject owner) { if (!base._OnRegister(owner) || !(Owner is T2DSceneObject)) return false; _sceneObj = Owner as T2DSceneObject; ProcessList.Instance.AddAnimationCallback(Owner, this); return true; } #endregion
上面的代码当对象被注册时,负责初始化组件。我们调用base._OnRegister来确保我们的父类能够正确初始化。我们也可以检查我们的对象是否是一个T2DsceneObject对象。接着,我们为它注册一个动画回调函数,并将它放置于ProcessList中。注意,相同对象可以为不限数目的组件注册回调函数。你甚至还可以控制回调函数被调用的次序,通过为每一个回调函数传递一个排序参数。最后,_OnRegister函数返回true来表明所有东西都已经被正确初始化了。
//======================================================
#region Private, protected, internal fields T2DSceneObject _sceneObj; #endregion }
_sceneObj是这个组件中唯一的数据成员。它仅仅是便于缓存T2DsceneObject的拥有者的一个变量。
我们可以将这个组件添加给任何对象,当它离开屏幕的时候,这个对象会自动注销自己。这是一个很简单的例子,但是我们要知道一个复杂的物体其实是由很多歌简单的组件组成的。一个更复杂的组件的例子是T2DphysicsComponent,T2DCollisionComponent, T2DLinkPointComponent, and T2DforceComponent。我们以后再来讨论这几个组件。
通常一个对象的两个组件间是需要通信的。他们可以通过共享TorqueInterface来完成通信。
// T2DForceComponent defines _RegisterInterfaces: protected internal override void _RegisterInterfaces(TorqueObject owner) { base._RegisterInterfaces(owner); ... // cache force interface and match empty name only Owner.RegisterCachedInterface("force", String.Empty, this, _forceInterface); }
最后一行代码注册了被_forceInterface成员变量持有的force接口,类型是”force”,没有名字(String.Empty)。