警告:此问题使用RPG的类比作为示例。
让我说我正在制作这个RPG,我一直在梦想,使用C#。 当玩家进入战斗时,会出现某种战场,其中包含与战斗相关的每个元素的参考,如战场上可用的各种对手和物品。
现在所有这些元素都有一个但有几个角色: 例如,Ally(通过直接继承而战斗的战士)能够在战场上移动,发出命令或被敌人作为目标。
现在石头中的强大剑也在战斗中扮演了一些角色。显然它不能移动也不能发出命令,但它仍然可以成为目标,并且可以(希望)被解除或使用。
所有这些行为都由我的代码中的接口表示,因此无论实现它的对象如何,都可以使用具有相同行为的所有对象。
代码示例:
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 ...
}
现在我的问题如下: 我的战地类应如何保留对所有这些物体的引用? 我提出了两种可能的解决方案,但目前还不清楚哪种解决方案最好,以及是否找到更好的解决方案。
解决方案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必须被引用到动作,目标和actor中 。 这种冗余意味着更大的内存占用空间,并且会使对象添加到战场变得更加复杂(每种类型现在应该从哪个集合添加/删除它)。
此外,添加新界面意味着添加新集合以保存项目并管理相应的代码。
解决方案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或新接口,添加从战场中删除对象的实例,更常见的是在游戏逻辑中使用它们。
我为我的例子的一般愚蠢道歉,我希望我的英语足够好,而且我的问题至少有一点兴趣,并不表示设计本身的整体愚蠢。
答案 0 :(得分:4)
解决方案A,但如果有理由,请不要立即查询整个实例列表。
一般而言(大概括,但具有重要的先例),您将通过使用可能性最小的类型(即您关注的接口)来实现最大的灵活性。通过提前处理和分组实例,而不是事后将它们拆分,您可能会获得最佳性能。
表现收益是否重要是一个不同的问题。既然你可能在谈论10s或100s的对象(而不是数百万),那么性能不是我的首要关注点。
我会回避任何需要从设计角度进行实时类型审讯的事情(但这是一个意见问题)。
与性能一样,在围绕它进行设计之前配置内存。如果您使用的是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;
}
}
......我的问题至少引起了一些兴趣 并不表示设计本身的整体愚蠢。
一点也不傻。
答案 1 :(得分:2)
我会选择解决方案A;额外的内存占用有效地具有指向同一对象实例的多个指针,并且代码更易于维护以便将来扩展。
顺便说一句......像这样的问题更多的基于意见,所以我认为没有正确或错误的答案。例如,我可以调整解决方案A来实现IDictionary而不是IList,你不必担心索引维护!答案 2 :(得分:1)
我会创建最后一个接口。 IBattleFieldObject,它们都是常见的。然后通过构造函数将它们全部放在一个列表中,用于依赖注入。然后在你的Update方法中,我将使用lambda通过linq提取所有IActor。然后执行与IActor相关的操作。此外,在Update中,您可以处理您需要用来模拟战场的任何其他接口。
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();
}
用法:battlefieldobjects.GetActors();
它将消除在各种集合中删除对象的难度。当需要删除对象时..只需将其从List集合中删除。
此外,如果您的游戏添加任何新功能或行为...您将能够将IBattleFieldObject接口添加到新的“东西”..然后输入代码来处理您的新“事物”更新方法。