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:
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.
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.
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
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With