12

警告:本题以 RPG 类比为例。

假设我正在使用 C# 制作我梦寐以求的 RPG。当玩家进入战斗时,会出现某种战场,其中包含与战斗相关的每个元素的引用,例如战场上的各种对手和可用物品。

现在所有这些元素都有一个但几个角色:例如,盟友(通过直接继承而成为战士)能够在战场上移动、发出命令或成为敌人的目标。

现在,石中的大剑在战斗中也有一些作用。显然它不能移动也不能发出命令,但它仍然可以成为目标,并且它可以(希望)被提升或使用。

在我的代码中,所有这些行为都由接口表示,因此无论实现它的对象如何,都可以使用具有相同行为的所有对象。

代码示例:

public class Ally : Fighter, IMovable, IActor, ITarget
{
    // IMovable implementation
    public void Move(...) { ... }

    // IActor implementation
    public bool HasInitiative { get { ... } }

    public ICommand IssueCommand(...) { ... }

    // ITarget implementation
    public void OnTarget(...) { ... }

    // other code ...
}

public class MightySword : ITarget, ILiftable, IUsable
{
    // ITarget implementation
    public void OnTarget(...) { ... }

    // ... other interface implementation

    // other code ...
}

现在我的问题是:我的 Battlefield 类应该如何保存对所有这些对象的引用?我提出了两种可能的解决方案,但目前尚不清楚哪一种是最好的,以及是否会找到更好的解决方案。

解决方案 A:

多次引用,直接访问:使用此解决方案,每个对象在其实现的每个接口中都被引用一次,因此我的战场类如下所示:

public class Battlefield : ...
{
   IList<IMovable> movables;
   IList<ITarget> targets;
   IList<IActor> actors;
   // ... and so on
}

Battlefield.Update 方法可能是这样的:

Update(...) 
{
  // Say every actor needs to target somebody else
  // it has nothing to do in the Update method,
  // but this is an example
  foreach (IActor actor in actors) 
  {
    if (actor.HasInitiative) 
    {
      ITarget target = targets[actor.TargetIndex];
      target.OnTarget(actor.IssueCommand(...));
    }
  }
}

该解决方案允许我们直接访问对象的各种行为,但它引入了冗余,因为必须将 Ally 引用到可移动对象、目标和参与者中。这种冗余意味着更大的内存占用,并且会使在战场上添加和删除对象变得更加复杂(现在每种类型都应该从哪些集合中添加/删除它)。

另外,添加一个新的接口意味着添加一个新的集合来保存项目并管理相应的代码。

解决方案 B:

引用一次,过滤访问:使用此解决方案,所有对象都派生自一个类或接口(类似于 IBattleElement),并存储在一个集合中。使用元素时,会根据它们的类型进行过滤:

Update(...)
{
    IList<IActor> actors = elements.OfType<Actor>();
    foreach (IActor actor in actors) 
    {
     ITarget target = elements.OfType<Target>()[actor.TargetIndex];
     target.OnTarget(actor.IssueCommand(...));
    }
}

那里。没有更多的冗余,但我不喜欢使用“typeof”结构,我通常会尽量避免它。显然,这对于运行时性能来说更糟。

我在我的玩具项目中实施了解决方案 A,但在这个项目中没有什么对性能至关重要。答案不仅应该考虑性能(内存方面和 CPU 方面),还应该考虑为我或我的设计师可能必须执行的操作提供最佳设计,例如添加新的 BattleElement 类或新行为的新接口,添加并从战场中移除对象的实例,并且更普遍地在游戏逻辑中使用它们。

我为我的示例的一般愚蠢表示歉意,我希望我的英语足够好,并且我的问题至少引起了一点兴趣,并且并不表明设计本身的整体愚蠢。

4

3 回答 3

4

解决方案 A,但如果有理由这样做,请不要立即查询整个实例列表。

一般来说(大泛化,但有重要的先例),您将通过使用尽可能少的派生类型来获得最大的灵活性,即仅使用您关心的接口。您可能会通过提前处理和分组实例而不是事后拆分它们来获得最佳性能。

性能提升是否重要是一个不同的问题。由于您可能谈论的是 10 或 100 的对象(而不是数百万),因此性能不是我首先关心的问题。

从设计的角度来看,我会回避任何需要实时类型询问的事情(但这是一个见仁见智的问题)。

与性能一样,在围绕它进行设计之前对内存进行配置。如果您使用的是 Visual Studio,那么您可以使用很好的工具来执行此操作。并且大概您没有复制这些实例,因此无论如何开销都是最小的。

混合方法也可能有意义。

  • 从单个集合中添加/删除。
  • 公开特定类型的集合过滤的只读集合。
  • 仅在需要时(重新)构建那些过滤的集合。

public class Battlefield
{
    private readonly IList<IBattleElement> _all = new List<IBattleElement>();
    private IReadOnlyList<IActor> _actors;
    private bool _isDirty;

    /// <summary>
    ///     Gets a list of all elements.
    /// </summary>
    public IReadOnlyList<IBattleElement> All
    {
        get { return _all; }
    }

    /// <summary>
    ///     Gets a filtered list of elements that are <c>IActor</c>.
    /// </summary>
    public IReadOnlyList<IActor> Actors
    {
        get
        {
            if( _isDirty || _actors == null )
            {
                _actors = All.OfType<IActor>().ToList();
            }

            return _actors;
        }
    }

    /// <summary>
    ///     The one and only way to add new elements. This could be made thread-safe if needed.
    /// </summary>
    /// <param name="element"></param>
    public void AddElement( IBattleElement element )
    {
        _all.Add( element );
        _isDirty = true;
    }
}

...而且我的问题至少引起了一点兴趣,并不表示设计本身的整体愚蠢。

一点都不傻。

于 2013-09-24T19:09:07.530 回答
2

我会选择解决方案 A;额外的内存占用实际上有多个指向同一对象实例的指针,并且代码更容易维护以备将来扩展。

顺便说一句......像这样的问题更多地基于意见,所以我认为没有正确或错误的答案。例如,我可以调整解决方案 A 以实现 IDictionary 而不是 IList,您无需担心索引维护!

于 2013-09-24T18:55:43.393 回答
1

我会创建最后一个界面。IBattleFieldObject,所有这些都是通用的。然后通过依赖注入的构造函数将它们全部放在一个列表中。然后在您的 Update 方法中,我将使用 lambda 通过 linq 提取所有 IActor。然后执行我与 IActor 相关的操作。此外,在更新中,您可以处理模拟战场所需的任何其他接口。

var IActors = (from list in battlefieldobjects where list is IActor select list).ToList();

或者你可以做一个扩展:

public static List<IActor> GetActors(this List<IBattleFieldObject> list)
{
    return list.Where(t => t is IActor).ToList();
}

用法:战场对象.GetActors();

它将消除删除各种集合中的对象的困难。当需要删除一个对象时..只需将其从您的 List 集合中删除。

此外,如果您的游戏添加任何新功能或行为..您将能够将 IBattleFieldObject 接口添加到该新“事物”..然后在您的更新方法中输入处理该新“事物”的代码。

于 2013-09-24T19:13:01.560 回答