Unity+C#卡牌游戏:如何重构依赖类型代码以避免向下转型?
我正在使用Unity与C#8.0进行开发。
我正在制作一款类似《杀戮尖塔》的卡牌对战游戏,玩家通过使用不同效果的卡牌与敌人战斗。流程为玩家选择卡牌,接着选择卡牌目标,若目标有效则触发卡牌效果。
多种游戏对象可作为卡牌目标:敌人、玩家角色、其他卡牌、回合计数器、天空等。每个可作为目标的对象所属类需实现如下接口:
public interface ICardTarget { public bool IsValidTarget(Card targetingCard); }
同理,多种游戏对象可使用卡牌:玩家角色、敌人、障碍物等。每个可使用卡牌的对象所属类需实现如下接口:
public interface ICardUser { //此处无内容,仅作为标记接口归类卡牌使用者。我是否正确使用了接口? }
场景中的部分对象示例如下:
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;} }
等等。
每张卡牌定义了合法目标类型。当玩家(或其他卡牌使用者)选择对象作为目标时,卡牌将自身传入IsValidTarget方法,获取该对象是否为合法目标的布尔值。
public class Card { private ICardUser _user; //卡牌使用者的引用,在其他地方设置 private ICardTarget _target; //卡牌目标的引用,在其他地方设置 public void CheckTarget() { if (_target.IsValidTarget(this)) { PerformCardEffect(); } } private void PerformCardEffect() {/*卡牌执行的逻辑*/} }
目前一切正常!但现在卡牌需要对目标对象执行效果,同时可能对卡牌使用者产生影响。
当前卡牌仅知道目标对象是合法目标(这是ICardTarget接口仅能保证的内容)。那么如何在不检查类型和向下转型的情况下,执行依赖目标类型的卡牌效果?
同样的问题也存在于卡牌使用者:我仅知道它是卡牌使用者,如何在不向下转型的情况下与它交互?
或者向下转型并没有我想象的那么糟糕?
我选择忽略该问题,在执行PerformCardEffect()时将目标/使用者向下转型为对应的实现类:
public class Card { private ICardUser _user; private ICardTarget _target; public void 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; } //等等 } }
这是目前最便于我迭代的方案,但我知道这种做法并不被推荐。
我修改ICardTarget/ICardUser接口,将调用正确方法的责任从Card转移到目标/使用者:
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 ICardTarget _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; } //我必须添加这些方法,让未被上述重载参数匹配的对象默认不执行任何操作。 public void PerformCardEffectTarget(ICardTarget t) {} public void PerformCardEffectUser(ICardUser u) {} }
这个方案看起来更好,但存在大量的引用传递,虽然不算坏事,但我觉得有些冗余。同时我必须在Card和接口上暴露大量方法。
附:有没有更简洁的方式编写SelectCardEffectTarget()/SelectCardEffectUser(),避免在每个子类中重复相同的代码块?我知道可以将ICardTarget/ICardUser改为类并在其中定义方法供子类继承,但这似乎不符合当前场景的层级结构。
我尝试使用泛型类指定目标/使用者类型,但无法找到有效的使用方式:
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(); //我到底能用这个做什么? } }
内容的提问来源于stack exchange,提问作者Will

