Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using GetType/instanceof in C# vs. alternatives

I've come across a problem in a game I am making in C#. It's a simple tile based matching game, and the problem has come up for a power up I am trying to make:

Say we have basic tile types, circles squares and diamonds, which are all subclasses of Tile. Instead of having circles only match to circles, I tried to extract the "matches" behavior to an abstract Tile method: canMatchWith(Tile t). The Tiles also have two methods to add/remove Tiles they can match with.

So say we have a Circle tile in the middle of our game, and we have a powerup that says "Circle tiles can match with square tiles this turn". I would go through all of the Circle tiles and say circleTile.addCanMatchWith(typeof(Square)). Internally, we have a List canMatchWith.

Then later, I want to say "Circles can no longer match with squares" and simply say circleTile.removeCanMatchWith(typeOf(Square)).

This is my current solution, and it works great with no performance drawbacks that I've noticed (It's a tile based matching game, so these types are only evaluated once per 'move', not frame by frame). However, the voice in my head is telling me that this is a bad way to accomplish this behavior. So I have some alternatives:

  1. Enums... Each Tile could be composed with a Tiletype type variable. This would be initialized in the constructor and set to Type.SQUARE for squares, and so on. Then, each Tile would have a List canMatchWith, and the functionality is the same as my original implementation. Except in this case, it's a little trickier. Say I have some circle subclasses, oval and elipse. I want ovals to be able to match with ONLY squares, but elipses can match with all circles and not squares.

The problem here is redundancy, my enum would now have OVAL and ELIPSE as well, and the Elipse class would have (CIRCLE, OVAL, ELIPSE TileTypes) as types it can match with. This is completely redundant, I want to just say "Circle" which I could with the types. I suppose the Tiles could have TileType baseType and TileType actualType.

  1. Some form of behavior composition. Forget Tile subclasses, just give Tiles methods and an instance variable for List. Then, at runtime we can just say someTile.addCanMatch(new CircleMatchBehavior()). This seems silly, as I would have a bunch of classes just saying you can match with a particular shape.

In summary, what I am trying to accomplish is having multiple object types be able to interact with any number of different types. The question is, what should I be using for the Type. Is it okay to use GetType here? Enums? Or is there a better strategy someone would recommend? I'm trying to be as general as possible, these tiles should not have any hardcoded dependencies on other tiles, and must be able to change who they can interact with on the fly. Say I make a new Tile subclass, pentagon... well, Pentagons can match with Squares, Circles, and Pentagons. Easy with my implementation, but something is telling me this is a dirty OOP practice.

I feel like I have to use Types/Enums because I am not trying to say thisTile.addCanMatch(Tile someOtherObject). That is too specific, I want thisTile to be able to match with all tiles who are instances of a particular class.

like image 385
user2045279 Avatar asked Apr 27 '15 18:04

user2045279


2 Answers

If all shapes of a similar type will always share a behavior, then it makes sense to not store that behavior on a 'per instance,' level. Instead, you can have a 'CanMatchManager,' that stores a dictionary of lists, indexed by shape type. Then, when a circle tries to compare a match, it requests the types it can match from the MatchManager. Alternatively, the MatchManager can take in the two shapes, and determine if those match. This is the Mediator Pattern

like image 177
Psymunn Avatar answered Nov 15 '22 22:11

Psymunn


I know the question has already been answered and accepted, but I've did something like this once and I thought I'd just post the code here.

    public class TypeMatchManager
    {
        private Dictionary<Type, List<Type>> savedMatches = new Dictionary<Type, List<Type>>();

        public TypeMatchManager() { }

        public void AddMatch(Type firstType, Type secondType)
        {
            this.addToList(firstType, secondType);
            this.addToList(secondType, firstType);
        }

        public void DeleteMatch(Type firstType, Type secondType)
        {
            this.deleteFromList(firstType, secondType);
            this.deleteFromList(secondType, firstType);
        }

        public bool CanMatch(Type firstType, Type secondType)
        {
            List<Type> firstTypeList = this.findListForType(firstType);
            List<Type> secondTypeList = this.findListForType(secondType);
            return (firstTypeList.Contains(secondType) || secondTypeList.Contains(firstType));
        }

        private void addToList(Type firstType, Type secondType)
        {
            var matchingTypes = this.findListForType(firstType);
            if (!matchingTypes.Contains(secondType))
            {
                matchingTypes.Add(secondType);
            }
        }

        private void deleteFromList(Type firstType, Type secondType)
        {
            var matchingTypes = this.findListForType(firstType);
            if (matchingTypes.Contains(secondType))
            {
                matchingTypes.Remove(secondType);
            }
        }

        private List<Type> findListForType(Type type)
        {
            foreach (var keyValuePair in savedMatches)
            {
                if (keyValuePair.Key == type)
                {
                    return keyValuePair.Value;
                }
            }
            savedMatches.Add(type, new List<Type>());
            return findListForType(type);
        }
    }

The class was designed so that it doesn't matter at which parameter you supply what type; it checks type1.list has type2 and type2.list has type.

A simple example:

        typeManager.AddMatch(a, b);
        Console.WriteLine(typeManager.CanMatch(a, b)); // True
        typeManager.DeleteMatch(b, a);
        Console.WriteLine(typeManager.CanMatch(a, b)); // False
        Console.WriteLine(typeManager.CanMatch(a, c)); // False
        typeManager.AddMatch(a, c);
        Console.WriteLine(typeManager.CanMatch(a, c)); // True
        Console.WriteLine(typeManager.CanMatch(a, d)); // False
        typeManager.AddMatch(b, d);
        Console.WriteLine(typeManager.CanMatch(a, d)); // False
        Console.WriteLine(typeManager.CanMatch(d, b)); // True
        typeManager.DeleteMatch(d, b);
        Console.WriteLine(typeManager.CanMatch(d, b)); // False
        Console.WriteLine(typeManager.CanMatch(b, d)); // False
like image 2
Jevgeni Geurtsen Avatar answered Nov 16 '22 00:11

Jevgeni Geurtsen