You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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接口仅能保证的内容)。那么如何在不检查类型和向下转型的情况下,执行依赖目标类型的卡牌效果?
同样的问题也存在于卡牌使用者:我仅知道它是卡牌使用者,如何在不向下转型的情况下与它交互?
或者向下转型并没有我想象的那么糟糕?

尝试方案1

我选择忽略该问题,在执行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;
        }

        //等等
    }
}

这是目前最便于我迭代的方案,但我知道这种做法并不被推荐。

尝试方案2

我修改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改为类并在其中定义方法供子类继承,但这似乎不符合当前场景的层级结构。

尝试方案3(失败)

我尝试使用泛型类指定目标/使用者类型,但无法找到有效的使用方式:

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 16:15:54