3

我经常遇到的一个基本问题,但曾经找到一个干净的解决方案,就是你想要为公共基类或接口的不同对象之间的交互编写行为代码。为了使其更具体,我将举一个例子;

Bob 一直在编写一款支持“酷地理效应”的策略游戏。这些都是简单的限制,例如如果部队在水中行走,他们会减慢 25%。如果他们在草地上行走,他们会减慢 5%,如果他们在人行道上行走,他们会减慢 0%。

现在,管理层告诉鲍勃他们需要新的部队。会有吉普车、船和气垫船。此外,他们希望吉普车在驶入水中时受到伤害,而气垫船将忽略所有三种地形类型。也有传言说他们可能会添加另一种地形类型,其功能比减缓单位减速和受到伤害还要多。

一个非常粗略的伪代码示例如下:

public interface ITerrain
{
    void AffectUnit(IUnit unit);
}

public class Water : ITerrain
{
    public void AffectUnit(IUnit unit)
    {
        if (unit is HoverCraft)
        {
            // Don't affect it anyhow
        }
        if (unit is FootSoldier)
        {
            unit.SpeedMultiplier = 0.75f;
        }
        if (unit is Jeep)
        {
            unit.SpeedMultiplier = 0.70f;
            unit.Health -= 5.0f;
        }
        if (unit is Boat)
        {
            // Don't affect it anyhow
        }
        /*
         * List grows larger each day...
         */
    }
}
public class Grass : ITerrain
{
    public void AffectUnit(IUnit unit)
    {
        if (unit is HoverCraft)
        {
            // Don't affect it anyhow
        }
        if (unit is FootSoldier)
        {
            unit.SpeedMultiplier = 0.95f;
        }
        if (unit is Jeep)
        {
            unit.SpeedMultiplier = 0.85f;
        }
        if (unit is Boat)
        {
            unit.SpeedMultiplier = 0.0f;
            unit.Health = 0.0f;
            Boat boat = unit as Boat;
            boat.DamagePropeller();
            // Perhaps throw in an explosion aswell?
        }
        /*
         * List grows larger each day...
         */
    }
}

如您所见,如果 Bob 从一开始就有可靠的设计文档,情况会更好。随着单位数量和地形类型的增加,代码的复杂性也在增加。Bob 不仅需要担心确定哪些成员可能需要添加到单元界面,而且他还必须重复大量代码。新的地形类型很可能需要从基本 IUnit 接口获得的附加信息。

每次我们在游戏中添加另一个单位时,都必须更新每个地形以处理新单位。显然,这会导致大量重复,更不用说丑陋的运行时检查,它决定了正在处理的单元的类型。在此示例中,我选择了对特定子类型的调用,但这些调用是必需的。一个例子是,当一艘船撞到陆地时,它的螺旋桨应该被损坏。并非所有单位都有螺旋桨。

我不确定这种问题叫什么,但它是一个多对多的依赖关系,我很难解耦。我不希望 ITerrain 上的每个 IUnit 子类都有 100 个重载,因为我想通过耦合来干净利落。

任何关于这个问题的观点都备受追捧。也许我正在考虑一起脱离轨道?

4

5 回答 5

1

将交互规则与 Unit 和 Terrain 类解耦;交互规则比这更通用。例如,可以使用哈希表,其中键是一对交互类型,值是对这些类型的对象进行操作的“效应器”方法。

当两个对象必须交互时,在哈希表中找到所有交互规则并执行它们

这消除了类间依赖关系,更不用说原始示例中可怕的 switch 语句

如果性能成为问题,并且交互规则在执行期间没有更改,则在遇到类型对时缓存它们的规则集,并发出一个新的 MSIL 方法来一次运行它们

于 2008-09-16T03:18:15.503 回答
1

您在这里遇到的限制是,与其他一些 OOP 语言不同,C# 缺少多个 dispatch

换句话说,给定这些基类:

public class Base
{
    public virtual void Go() { Console.WriteLine("in Base"); }
}

public class Derived : Base
{
    public virtual void Go() { Console.WriteLine("in Derived"); }
}

这个功能:

public void Test()
{
    Base obj = new Derived();
    obj.Go();
}

即使引用“obj”是 Base 类型,也会正确输出“in Derived”。这是因为在运行时C# 会正确找到最衍生的 Go() 来调用。

然而,由于 C# 是一种单一的调度语言,它只对“第一个参数”执行此操作,在 OOP 语言中隐式为“this”。以下代码不像上面那样工作:

public class TestClass
{
    public void Go(Base b)
    {
        Console.WriteLine("Base arg");
    }

    public void Go(Derived d)
    {
        Console.WriteLine("Derived arg");
    }

    public void Test()
    {
        Base obj = new Derived();
        Go(obj);
    }
}

这将输出“Base arg”,因为除了“this”之外,所有其他参数都是静态分派的,这意味着它们在编译时绑定到被调用的方法。在编译时,编译器唯一知道的是传递的参数的声明类型(“Base obj”)而不是它的实际类型,因此方法调用绑定到 Go(Base b) 之一。

那么,解决您的问题的方法基本上是手动编写一个小方法调度程序:

public class Dispatcher
{
    public void Dispatch(IUnit unit, ITerrain terrain)
    {
        Type unitType = unit.GetType();
        Type terrainType = terrain.GetType();

        // go through the list and find the action that corresponds to the
        // most-derived IUnit and ITerrain types that are in the ancestor
        // chain for unitType and terrainType.
        Action<IUnit, ITerrain> action = /* left as exercise for reader ;) */

        action(unit, terrain);
    }

    // add functions to this
    public List<Action<IUnit, ITerrain>> Actions = new List<Action<IUnit, ITerrain>>();
}

您可以使用反射来检查传入的每个动作的通用参数,然后选择与给定单位和地形匹配的最派生参数,然后调用该函数。添加到 Actions 的功能可以在任何地方,甚至分布在多个程序集中。

有趣的是,我遇到过几次这个问题,但从来没有脱离游戏的背景。

于 2008-09-16T02:27:51.227 回答
1

这里肯定有三个对象在起作用:

1) 地形
2) 地形效果
3) 单位

我不建议以地形/单位作为查找动作的关键来创建地图。这将使您难以确保随着单位和地形列表的增长,您已经涵盖了所有组合。

事实上,似乎每个地形单元组合都具有独特的地形效果,因此您是否会从拥有一个通用的地形效果列表中看到好处是值得怀疑的。

相反,我会让每个单位保持自己的地形对地形效果的地图。然后,地形可以调用 Unit->AffectUnit(myTerrainType) 并且单元可以查找地形对自身的影响。

于 2008-09-16T03:51:36.877 回答
1

Terrain has-a 地形属性

地形属性是多维的。

单位有推进力。

推进与地形属性兼容。

单位通过地形访问移动,以推进作为参数。这被委托给推进。

作为访问的一部分,单位可能会受到地形的影响。

单位代码对推进一无所知。地形类型可以改变而无需改变任何东西,除了地形属性和推进力。Propuslion 的构造器保护现有单元免受新的旅行方式的影响。

于 2008-09-16T01:49:03.223 回答
0

老想法:

创建一个类 iTerrain 和另一个类 iUnit,它接受一个地形类型的参数,包括一个影响每个单位类型的方法

例子:

  boat = new
iUnit("watercraft") field = new
iTerrain("grass")
field.effects(boat)

好吧,忘记我有一个更好的主意:

使每个地形的效果成为每个单位的属性

例子:


public class hovercraft : unit {
    #You make a base class for defaults and redefine as necessary
    speed_multiplier.water = 1
}

public class boat : unit {
    speed_multiplier.land = 0
}
于 2008-09-16T01:31:29.257 回答