views:

210

answers:

5

A basic problem I run into quite often, but ever found a clean solution to, is one where you want to code behaviour for interaction between different objects of a common base class or interface. To make it a bit concrete, I'll throw in an example;

Bob has been coding on a strategy game which supports "cool geographical effects". These round up to simple constraints such as if troops are walking in water, they are slowed 25%. If they are walking on grass, they are slowed 5%, and if they are walking on pavement they are slowed by 0%.

Now, management told Bob that they needed new sorts of troops. There would be jeeps, boats and also hovercrafts. Also, they wanted jeeps to take damage if they went drove into water, and hovercrafts would ignore all three of the terrain types. Rumor has it also that they might add another terrain type with even more features than slowing units down and taking damage.

A very rough pseudo code example follows:

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...
         */
    }
}

As you can see, things would have been better if Bob had a solid design document from the beginning. As the number of units and terrain types grow, so does code complexity. Not only does Bob have to worry about figuring out which members might need to be added to the unit interface, but he also has to repeat alot of code. It's very likely that new terrain types require additional information from what can be obtained from the basic IUnit interface.

Each time we add another unit into the game, each terrain must be updated to handle the new unit. Clearly, this makes for a lot of repetition, not to mention the ugly runtime check which determines the type of unit being dealt with. I've opted out calls to the specific subtypes in this example, but those kinds of calls are neccessary to make. An example would be that when a boat hits land, its propeller should be damaged. Not all units have propellers.

I am unsure what this kind of problem is called, but it is a many-to-many dependence which I have a hard time decoupling. I don't fancy having 100's of overloads for each IUnit subclass on ITerrain as I would want to come clean with coupling.

Any light on this problem is highly sought after. Perhaps I'm thinking way out of orbit all together?

A: 

Old idea:

Make a class iTerrain and another class iUnit which accepts an argument which is the terrain type including a method for affecting each unit type

example:

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

ok forget all that I have a better idea:

Make the effects of each terrain a property of each unit

Example:


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
}
Jiaaro
+1  A: 

Terrain has-a Terrain Attribute

Terrain Attributes are multidimensional.

Units has-a Propulsion.

Propulsion is compatible able with Terrain Attributes.

Units move by a Terrain visit with Propulsion as an argument. That gets delegated to the Propulsion.

Units may get affected by terrain as part of the visit.

Unit code knows nothing about propulsion. Terrain types can change w/o changing anything except Terrain Attributes and Propulsion. Propuslion's constructors protect existing units from new methods of travel.

Tim Williscroft
+1  A: 

The limitation you're running into here is that C#, unlike some other OOP languages, lacks multiple dispatch.

In other words, given these base classes:

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

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

This function:

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

will correctly output "in Derived" even though the reference "obj" is of type Base. This is because at runtime C# will correctly find the most-derived Go() to call.

However, since C# is a single dispatch language, it only does this for the "first parameter" which is implicitly "this" in an OOP language. The following code does not work like the above:

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);
    }
}

This will output "Base arg" because aside from "this" all other parameters are statically dispatched, which means they are bound to the called method at compile time. At compile time, the only thing the compiler knows is the declared type of the argument being passed ("Base obj") and not its actual type, so the method call is bound to the Go(Base b) one.

A solution to your problem then, is to basically hand-author a little method dispatcher:

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>>();
}

You can use reflection to inspect the generic parameters of each Action passed in and then choose the most-derived one that matches the unit and terrain given, then call that function. The functions added to Actions can be anywhere, even distributed across multiple assemblies.

Interestingly, I've run into this problem a few times, but never outside of the context of games.

munificent
+1  A: 

decouple the interaction rules from the Unit and Terrain classes; interaction rules are more general than that. For example a hash table might be used with the key being a pair of interacting types and the value being an 'effector' method operating on objects of those types.

when two objects must interact, find ALL of the interaction rules in the hash table and execute them

this eliminates the inter-class dependencies, not to mention the hideous switch statements in your original example

if performance becomes an issue, and the interaction rules do not change during execution, cache the rule-sets for type pairs as they are encountered and emit a new MSIL method to run them all at once

Steven A. Lowe
+1  A: 

There's definitely three objects in play here:

1) Terrain
2) Terrain Effects
3) Units

I would not suggest creating a map with the pair of terrain/unit as a key to look up the action. That is going to make it difficult for you to make sure you've got every combination covered as the lists of units and terrains grow.

In fact, it appears that every terrain-unit combination has a unique terrain effect so it's doubtful that you'd see a benefit from having a common list of terrain effects at all.

Instead, I would have each unit maintain its own map of terrain to terrain effect. Then, the terrain can just call Unit->AffectUnit(myTerrainType) and the unit can look up the effect that the terrain will have on itself.

17 of 26