面向对象设计,交互式对象

3
这个问题让我想起了迷你游戏Doodle God。有几个物体,其中一些可以相互作用并形成新的物体。每个物体都是自己的类:水、火、空气等,它们都继承自同一个基类。例如,水和火可以结合形成灰烬对象,这些对象可以用于新的组合。
问题在于找出一种优雅的方式来处理所有可能的组合。最明显但可怕难以维护的解决方案是创建一个函数,它以任何两个对象作为参数,并使用一个巨大的switch块来比较类型名称并确定这两个对象相互作用时应返回哪种对象(如果有的话)。同时,combine(a, b) 应始终等于 combine(b, a)。
对于这种情况,什么样的设计既易于维护又高效呢?

最好的方法是使用设计模式。看一下创建型和行为型类型http://en.wikipedia.org/wiki/Design_Patterns - Thiago Custodio
对于创建,您可以使用查找表,将两个objectId(例如“fire”,“water”)作为键传递给它,并使用重写的equals作为对象。这样,您可以执行以下操作:var key = new IdSet(doodle1.Name, doodle2.Name); String result = lookupTable[key]; 有了给定的结果,您可以创建结果对象。对于每个对象都有一个类似乎对我来说太多了,我宁愿有一个通用类,因为我怀疑涂鸦除了设置一些参数之外并没有做任何专业化。 - Fabio Marcolini
4个回答

2

我们在游戏中需要编写代码来实现物品碰撞。最终我们采用了一个二维结构,其中存储了一系列委托方法。

      | air            |  wind            | fire
air   |combine(air,air)|combine(air,wind) |combine(air,fire)
wind  |                |combine(wind,wind)|combine(wind,fire)
fire  |                |                  |combine(fire,fire)

稍加思考,您只需要填充一半以上的组合矩阵。

例如,您可以:

lookup = 
     new Dictionary<
            Tuple<Type, Type>,
            Func<ICombinable, ICombinable, ICombinable>();
lookup.Add(
   Tuple.Create(typeof(Air), typeof(Fire)),
   (air,fire) => return new Explosion());

现在我们需要一个单一的方法:

ICombinable Combine(ICombinable a,ICombinable b)
{
    var typeA = a.GetType();
    var typeB = b.GetType();
    var typeCombo1 = Tuple.Create(typeA,typeB);
    Func<ICombinable,ICombinable,ICombinable> combineFunc;
    if(lookup.TryGetValue(typeCombo1, out combineFunc))
    {
        return combineFunc(a,b);
    }
    var typeCombo2 = Tuple.Create(typeB,typeA);
    if(lookup.TryGetValue(typeCombo2, out combineFunc))
    {
        return combineFunc(b,a);
    }
     //throw?
}

这应该可以解决问题,谢谢。由于某些对象没有可组合性,因此最好返回null而不是抛出异常,这是预期的行为。 - Ryan

1
所有游戏对象都已经以某种方式进行了设计。它们是硬编码的,或者在运行时从资源中读取。
这个数据结构可以很容易地存储在一个Dictionary<Element, Dictionary<Element, Element>>中。
var fire = new FireElement();
var water = new WaterElement();
var steam = new SteamElement();

_allElements = Dictionary<Element, Dictionary<Element,Element>>
{
    new KeyValuePair<Element, Dictionary<Element, Element>>
    {
        Key = fire,
        Value = new KeyValuePair<Element, Element>
        {
            Key = water,
            Value = steam
        }
    },
    new KeyValuePair<Element, Dictionary<Element, Element>>
    {
        Key = water,
        Value = new KeyValuePair<Element, Element>
        {
            Key = fire,
            Value = steam
        }
    }

}

当加载或定义元素时,您可以只复制它们,因为最多只有几百个。在我看来,为了编程的便利性,这种开销是可以忽略不计的。 _allElements 的键包含所有现有的可组合元素。 _allElements [SomeElement] 的值产生另一个字典,您可以在其中访问要与其组合的元素。
这意味着您可以使用以下代码找到组合的结果元素:
public Element Combine(Element element1, Element element2)
{
    return _allElements[element1][element2];
}

Which, when called as such:

var resultingElement = Combine(fire, water);

产生了蒸汽,和调用Combine(water, fire)得到的结果相同。
未经测试,但我希望这个原则适用。

0

这正是接口的正确使用场景。通过它们,您可以避免使用大型开关语句,并且每个元素类都可以实现其自己与另一个元素类交互的行为。


那是相当多的接口。那么关系的另一端呢?如果你将(例如)空气和火结合起来,谁负责结果,还是它们都实现一个“组合器”(导致冗余)? - spender

-1
我建议使用抽象工厂返回特定类型的接口,比如InteractionOutcome。你无法避免使用switch-case,但是使用不同的工厂来构建每个对象,你最终会得到更易于维护的代码。
希望我的建议能够帮到你!

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