Question
How to refactor this type-dependant code to avoid downcasting?
Background
I am coding in Unity, with C#8.0.
I am making a game where the player battles enemies by playing cards that have different effects (think Slay the Spire). The player selects the card, then selects the card's target, and then (if the target is valid) the card's effect is invoked.
Lots of different game objects can be the target of a card: enemies, the player character, other cards, the turn counter, the sky, etc. For each object that can be a target I have its class implement an interface:
public interface ICardTarget
{
public bool IsValidTarget(Card targetingCard);
}
Similarly, lots of different game objects can play cards: the player character, enemies, obstacle objects, etc. For each object that can use a card I have its class implement an interface:
public interface ICardUser
{
//I don't have anything in here, this is really more of just a tag to group together any card users. Am I using interfaces right?
}
So some of the objects in my scene might be:
public class Enemy : ICardTarget, ICardUser
{
public bool IsValidTarget(Card targetingCard) {/*...*/}
public string Species {get;set;}
}
public class PlayerCharacter : ICardTarget, ICardUser
{
public bool IsValidTarget(Card targetingCard) {/*...*/}
public float AttackSpeed {get;set;}
public int Health {get;set;}
}
public class Weather : ICardTarget
{
public bool IsValidTarget(Card targetingCard) {/*...*/}
public bool IsCloudy {get;set;}
}
public class Boulder: ICardUser
{
public int Mass {get;set;}
}
etc.
Each card defines which types of targets are valid. When the player (or any card user for that matter) selects an object as the card's target, the card passes itself to the IsValidTarget function and receives a bool response as to whether this is a valid target for this card.
public class Card
{
private ICardUser _user; //reference to this card's user, set somewhere else
private ICardTarget _target; //reference to this card's target, set somewhere else
public void CheckTarget()
{
if (_target.IsValidTarget(this))
{
PerformCardEffect();
}
}
private void PerformCardEffect() {/*whatever the card does*/}
}
So far so good! Now the card needs to perform its effect on the targeted object, and also may have an effect on the card's user.
The Problem
Currently, the only thing the card knows about this targeted object is that it is a valid target (that's all that the ICardTarget interface guarantees, after all). So how do I perform the card's effect that depends on the type of the target without checking the type and downcasting? The exact same problem applies to the card user: all I know is that the card came from a card user, how can I interact with it without downcasting? Or is downcasting not the boogieman I think it is?
Attempted Solution 1
I don't worry about it and downcast the target/user to one of the classes that implements it when PerformCardEffect() is run:
public class Card
{
private ICardUser _user;
private ITarget _target;
public PerformCardEffect()
{
if (_target is Weather w)
{
w.IsCloudy = true;
}
if (_target is Enemy e)
{
print(e.Species);
}
if (_target is PlayerCharacter p)
{
p.AttackSpeed *= 2;
}
if (_user is PlayerCharacter p)
{
p.Health -= 3;
}
if (_user is Enemy e)
{
e.Health -= 5;
}
//etc.
}
}
This is easiest for me to iterate on right now, but I know this is frowned upon.
Attempted Solution 2
I modify ICardTarget/ICardUser so that the responsibility of calling the correct function is moved off of Card and onto them:
public interface ICardTarget
{
//...
public void SelectCardEffectTarget(Card targetingCard);
//...
}
public interface ICardUser
{
public void SelectCardEffectUser(Card targetingCard);
}
public class Enemy : ICardTarget, ICardUser
{
//...
public void SelectCardEffectTarget(Card targetingCard)
{
targetingCard.PerformCardEffectTarget(this);
}
public void SelectCardEffectUser(Card targetingCard)
{
targetingCard.PerformCardEffectUser(this);
}
//...
}
public class PlayerCharacter : ICardTarget, ICardUser
{
//...
public void SelectCardEffectTarget(Card targetingCard)
{
targetingCard.PerformCardEffectTarget(this);
}
public void SelectCardEffectUser(Card targetingCard)
{
targetingCard.PerformCardEffectUser(this);
}
//...
}
public class Weather : ICardTarget
{
//...
public void SelectCardEffectTarget(Card targetingCard)
{
targetingCard.PerformCardEffectTarget(this);
}
//...
}
public class Card
{
private ICardUser _user;
private ITarget _target;
public void CheckTarget()
{
if (_target.IsValidTarget(this))
{
_target.SelectCardEffectTarget(this);
_user.SelectCardEffectUser(this);
}
}
public void PerformCardEffectTarget(Weather w)
{
w.IsCloudy = true;
}
public void PerformCardEffectTarget(Enemy e)
{
print(e.Species);
}
public void PerformCardEffectTarget(PlayerCharacter p)
{
p.AttackSpeed *= 2;
}
public void PerformCardEffectUser(Enemy e)
{
e.Health -= 5;
}
public void PerformCardEffectUser(PlayerCharacter p)
{
p.Health -= 3;
}
//I have to include these calls so that any objects not caught by the above overloaded parameters default to doing nothing.
public void PerformCardEffectTarget(ICardTarget t) {}
public void PerformCardEffectUser(ICardUser u) {}
}
This seems better but there's a lot of passing references back and forth, which isn't a bad thing but to me it feels a little excessive. I also have to expose a lot of methods on both Card and the interfaces.
P.S. Is there a shorter way of writing SelectCardEffectTarget()/SelectCardEffectUser() so that I don't have to repeat the same code block in each subclass? I know I could make ICardTarget/ICardUser into a class and set the function there so it's inherited, but that doesn't seem like the appropriate hierarchy for this situation.
Attempted Solution 3 (Failed)
I tried to utilize a generic class to specify the target/user types, but I couldn't figure out a way to involve it and get useful functionality from it.
public class CardEffectDetails<T,U> where T : ICardTarget where U : ICardUser
{
T Target {get;set;}
U User {get;set;}
}
public class Card
{
private ICardTarget _target;
private ICardUser _user;
private void CreateEffectDetails()
{
CardEffectDetails<_target.GetType(), _user.GetType()> effectDetails = new();
//what can I even do with this thing?
}
}