3

I'm writing a pathfinding algorithm for a game, but trying to keep it generic so it can be used in future applications.

I have a Node class which holds X, Y and "PassableType".

The NodeGrid class stores an array of Nodes, containing the graph information of how they connect, and then has a FindAStarPath() function, which takes as its parameters StartNode, EndNode, and params for "PassableTypes".

My problem is determining what type "PassableType" should have.

Ideally what I want is to be able to use a generic enum - i.e. a restricted list which each game defines. The Node will hold a single element of that list, to say what path type it is (the current game may use Path, Grass, Wall, etc)

Thus, when an entity tries to path, it provides the pathfinding function which types to treat as "passable". So a man may use

FindAStarPath(CurrentNode, DestinationNode, "Path", "Floor", "Door");

but a car may just use

FindAStarPath(StartNode, EndNode, "Road");

My problem is I can't work out how to get the NodeGrid to take a Generic enum or equivalent logic.

At the moment I have it taking strings, but this means I have to write
MyEnum.Road.ToString() every time I use it.

Ideally I'd like to do something like

NodeGrid<MyEnum> CurrentNodeGrid = new NodeGrid<MyEnum>()

And then Nodes will take a MyEnum for their passableType, as will the pathfinding functions, thus allowing each game to have a different set of tile types for pathing.

But I can't define NodeGrid as:

public class NodeGrid<T> where T:enum

For clarity, the only part of the pathfinding function which uses this enum is this (contained within Node):

    public bool IsPassable(string[] passableTypes)
    {
        for (var i = 0; i < passableTypes.Count(); i++)
        {
            if (this.PassableType == passableTypes[i]) return true;
        }
        return false;
    }

Thanks Haighstrom

Haighstrom
  • 577
  • 5
  • 16
  • What's your problem with `NodeGrid CurrentNodeGrid = new NodeGrid()`? – Daniel Hilgarth Jul 11 '13 at 12:07
  • I can't constrain MyEnum with "where MyEnum: enum". Updated main question. – Haighstrom Jul 11 '13 at 12:13
  • See: http://stackoverflow.com/questions/79126/create-generic-method-constraining-t-to-an-enum – Moo-Juice Jul 11 '13 at 12:14
  • Do I really have to use Enum.Parse for this? It seems unreasonable, given all I want to do is say "here is a list of acceptable values for pathing types: please constrain Node.PassableType and NodeGrid.AStarPathing(params string[] PassableTypes) to only use those values (defined at class definition)". – Haighstrom Jul 11 '13 at 12:18
  • 2
    I agree with Haigstrom: who cares what they use? Just have `IEnumerable` (or `params T[]`) and be done with it. If the caller uses an `enum`, great. If they want to use a `string` set (as you currently have it), ok. If they have a set of custom class instances to check against, that's fine too. EDIT: Maybe at that point you have to update the `==` check since I don't think you'll be able to use it for generic type `T`. Try `EqualityComparer.Default.Equals()` instead. – Chris Sinclair Jul 11 '13 at 12:19
  • Thanks Chris, this sounds like the way to go. I haven't used IEnumerable before. Could you write that as a full answer with an example of how to declare my NodeGrid / Node class? Presumably something like NodeGrid / Node where T:IEnumerable? – Haighstrom Jul 11 '13 at 12:29
  • @ChrisSinclair also can't I just use .Equals() in the check since that's defined at Object level? – Haighstrom Jul 11 '13 at 12:35
  • @Haighstrom: See my answer. You _can_, but `EqualityComparer.Default` does a bit of extra bits for you to make it more efficient and perform null checking; it's pretty useful for comparing generics. – Chris Sinclair Jul 11 '13 at 12:58

1 Answers1

5

Unless you're using some specific functionality of enums (like Enum.Parse), then I don't see any reason to constrain it to them. By freeing constraints, callers can use whatever types they see fit, beit enum, or a set of string values (as you currently have it), or a set of custom class instances to check against.

public class NodeGrid<T>
{
    public T PassableType { get; private set; }

    public bool IsPassable(params T[] passableTypes)
    {
        return IsPassable((IEnumerable<T>)passableTypes);
    }

    public bool IsPassable(IEnumerable<T> passableTypes)
    {
        foreach(T passType in passableTypes)
        {
            if (EqualityComparer<T>.Default.Equals(this.PassableType, passType))
                return true;
        }

        return false;
    }
}

But since we're now using generics, you can't use the == comparison anymore. The simplest is to leverage the EqualityComparer.Default utility. The main reason to use this over directly calling this.PassableType.Equals(passType) is it will perform null checks and leverage generics properly where applicable and if the types implement IEquatable<T>, then use those generic versions. Probably some other minor things. It will usually eventually call the Object.Equals overload.


Some examples based on your question:

//using a custom enum, calls the params T[] overload
NodeGrid<MyCarEnum> carNode = ...
carNode.IsPassable(MyCarEnum.Road, MyCarEnum.Tunnel);

//demonstrates receiving a set of pass types strings from an external source
List<string> passTypes = new List<string>("Path", "Floor", "Door");
NodeGrid<string> personNode = ...
personNode.IsPassable(passTypes) //calls the IEnumerable<T> overload

//feel free to declare enums wherever you want, 
//it can avoid potential mixups like this:
NodeGrid<string> airplaneNode = ...
NodeGrid<string> personNode = ...
NodeGrid<MyCarEnum> carNode = ...
airplaneNode.IsPassable("Floor"); //makes no sense, but will compile
personNode.IsPassable("Clouds"); //makes no sense, but will compile
carNode.IsPassable("Sky"); //compile error: was expected a MyCarEnum value
Chris Sinclair
  • 22,858
  • 3
  • 52
  • 93