在.Net中的接口继承/层次结构 - 是否有更好的方法?

6

我打算举一个我的代码中的案例来说明我的意思。

现在,我正在为我的组件化游戏系统开发一个行为/动作系统。游戏对象可以附加IBehaviorComponent和IActionComponent,它们只显示相关细节,如下所示:

public interface IBehaviorComponent : IBaseComponent
{
   IBehavior Behavior { get; }
}

public interface IActionComponent : IBaseComponent
{
   IAction Action { get; }
   void ExecuteAction(IGameObject target = null);
}

现在,到目前为止一切都很好(至少对我来说是这样的!)。但是,当我看到IActionComponent的实现时,问题开始酝酿。
例如,一个简单的IActionComponent实现:
public class SimpleActionComponent : IActionComponent
{
   public IAction Action { get; protected set; }

   public void ExecuteAction(IGameObject target = null)
   {
      Action.Execute(target);
   }

   public void Update(float deltaTime) { } //from IBaseComponent
}

但是,假设我想引入一个更复杂的IActionComponent实现,允许在定时计划上执行操作:

public class TimedActionComponent : IActionComponent
{
   public IAction Action { get; protected set; }

   public float IdleTime { get; set; }

   public float IdleTimeRemaining { get; protected set; }

   public void ExecuteAction(IGameObject target = null)
   {
      IdleTimeRemaining = IdleTime;
   }

   public void Update(float deltaTime)
   {
      if (IdleTimeRemaining > 0f)
      {
         IdleTimeRemaining -= deltaTime;
      }
      else
      {
         Action.Execute(target);
      }
   }
}

现在,假设我想要公开IdleTime,以便外部因素可以进行更改。一开始我的想法是创建一个新的接口:

public interface ITimedActionComponent : IActionComponent
{
   float IdleTime { get; set; }

   float IdleTimeRemaining { get; set; }
}

然而,在我的组件系统中,所有东西都存储在IBaseComponent的一级上。因此,GameObject的操作组件被检索为IActionComponent,而不是ITimedActionComponent、IRandomizedActionComponent甚至是ICrashTheProgramActionComponent。我希望这样做的原因很明显,我希望任何东西都可以查询GameObject的一个组件,而无需知道它需要什么除了基本类型的组件(IActionComponent、IRenderableComponent、IPhysicsComponent等)。

有没有更简单的处理方式,可以让我暴露子类中定义的这些属性,而不必让每个东西都将检索到的IActionComponent转换为它感兴趣的类型呢?或者说这只是完成这项任务的唯一/最佳方式?例如:

public void IWantYouToAttackSuperSlow(IGameObject target)
{
   //Let's get the action component for the gameobject and test if it's an ITimedActionComponent...
   ITimedActionComponent actionComponent = target.GetComponent<IActionComponent>() as ITimedActionComponent;

   if (actionComponent != null) //We're okay!
   {
      actionComponent.IdleTime = int.MaxValue; //Mwhaha
   }
}

我现在认为这是唯一的方法,但我想看看是否有隐藏在木工中我不知道的模式,或者是否有人能建议更好的方法来实现这个目标。

谢谢!


我的另一个想法是在内部使用另一个对象,例如IActionComponentExecutor,它将决定何时调用Action.Execute(),但我认为这会以迂回的方式使我回到原点 - 外部对象将不得不跟踪IActionComponentTimedExecutor(哦~我们现在变得像企业了!)来更改IdleTime。 - Terminal8
仅为了解 - actionComponent.IdleTime的状态是否存储在某种通用包中? 如果是,您可以在基础接口上拥有“Apply(BagData bag)”和“Store(BagData bag)”方法。 也就是说,您可能还需要考虑一些元数据类型系统,以便您可以查询属性而无需知道派生类型。TypeDescriptor是一个准备好的起点,它被像属性网格等东西使用。 - Andras Zoltan
不,目前还不是。我对这个概念也不是很熟悉——你能提供一个参考资料吗? - Terminal8
嗯,实际上,我想把它看作是类似于序列化。但如果您希望编写的代码“知道”有一个值可以通过知道派生接口而设置,那么这可能不太合适。 - Andras Zoltan
好的,我明白了。我认为这可能对这种情况来说有点太笼统了。其他代码将会知道这些派生接口,我只需要一个方便的方法来获取访问权限。 - Terminal8
2个回答

1

你展示的代码将一个 IActionComponent 转换为一个 ITimedActionComponent,据我所见是不可避免的 - 你必须意识到 IdleTime 属性才能使用它们,对吗?

我认为这里的诀窍是隐藏那些不太好看的代码,让使用你的 IActionComponent 的类不必处理它。
我第一个想到的解决方法是使用一个 工厂

  public void IWantYouToAttackSuperSlow(IGameObject target)
{
   //Let's get the action component for the gameobject 
   IActionComponent actionComponent = ActionComponentsFactory.GetTimedActionComponentIfAvailable(int.MaxValue); 
}

还有你们工厂的方法:

public IActionComponent GetTimedActionComponentIfAvailable(IGameObject target, int idleTime)
{
var actionComponent = target.GetComponent<IActionComponent>() as ITimedActionComponent;

   if (actionComponent != null) //We're okay!
   {
      actionComponent.IdleTime = int.MaxValue; //Mwhaha
   }
return (actionComponent != null)? actionComponent : target.GetComponent<IActionComponent>();
}

关于你的第一句话,我同意,并且我看不到任何真正的解决办法。我想我是在寻求一些神奇的架构来解决所有问题。但我猜我们还没有完全到达那里 ;) 至于工厂方法,我喜欢它,它至少会帮助清理一些东西。我将看看它在我现在拥有的东西中能否很好地发挥作用。 - Terminal8

0

你说得对,通过将对象强制转换为子类型来测试特定实现并不是一个好主意。从你提供的例子来看,似乎你有一个IGameObject想要执行某些操作。与其让你的IActionComponent公开一个以IGameObject为参数的方法,你可以反过来做,即让IGameObject执行一个或多个操作,然后你可以执行这些操作。

interface IActionComponent
{
    ExecuteAction();
}

interface IGameObject
{
    IActionComponent ActionComponent { get; }
}

然后在执行方法中进行

public void IWantYouToAttackSuperSlow(IGameObject target)
{
    target.ActionComponent.ExecuteAction();
}

根据您如何实现IActionComponent,多态性将确保执行正确的代码。

有一种称为命令模式的模式似乎符合您的需求。


啊,我在这里提供的案例只是我遇到的接口继承问题的一个实例。由于我正在开发一个组件化系统,IGameObject 只是一个极其简单的组件容器,绝对没有关于任何“真正”的组件的逻辑或知识。本质上,它只是一个允许将覆盖给定“实体”的组件绑定到该“实体”的GUID(但我已将该概念封装在接口/类中,以提供方便的组件检索)。 - Terminal8
@Icelight:我不能完全理解你的架构。但是,你的句子“IGameObject只是一个极其简单的组件容器,没有任何逻辑...”听起来很奇怪。每当我听到一个“应该只是一个属性袋,没有任何逻辑”的类时,我就会想起我学习面向对象编程的第一件事:它是关于将行为放在数据所在的地方。因此,在我看来,没有逻辑的类有点像代码异味。 - J. Ed
@sJhonny:我理解你的意思。然而,在这种情况下,一个GameObject是为了我的方便而放置的,而不是必要的。在一个“真正”的组件化游戏系统中,所有的组件都会漂浮在巨大的组件云中(或者更可能地,坐在专门管理它们的组件类型的管理器中),只有像GUID这样的东西将属于同一实体的组件联系在一起。与采用这种方法不同,我使用一个GameObject来保存同一实体的所有组件——这是为了方便,因为这种方法更容易让我想象。 - Terminal8

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接