设计模式综合案例2:设计一个游戏角色系统(状态+策略+装饰器)

说实话,面试里我最喜欢问这类综合案例题。因为单一模式考察的是「你会不会用」,而综合案例考察的是「你什么时候该用哪个」。今天这个游戏角色系统,我当年在做一个MOBA项目时亲手写过类似的架构。嗯,踩过坑,也重构过,今天把经验分享给你。

需求场景:一个会变身的角色

假设我们要设计一个游戏角色。它有几个特点:

  • 角色有不同状态:正常、狂暴、虚弱、隐身
  • 角色可以装备不同武器:剑、弓、法杖
  • 角色可以叠加各种增益效果:火焰附魔、冰冻光环、吸血

你想想看,如果把这些逻辑全部塞到一个类里,会是什么后果?我见过一个项目,角色类有3000多行,if-else嵌套了七八层。后来加一个新状态,改了三天,改出三个bug。这就是典型的「面条代码」。

架构设计:三模式协同

我们分别用三种模式来解耦:

模式 负责什么 类比现实
状态模式 角色行为随状态变化 人开心时唱歌,难过时沉默
策略模式 可替换的攻击方式 换武器就像换策略
装饰器模式 动态叠加额外效果 给武器涂毒、附魔

核心思想:状态决定「能不能做」,策略决定「怎么做」,装饰器决定「做了之后额外发生什么」。

先画一张架构图

下面这张图展示了三个模式如何协作。我习惯先画图再写代码,这样思路清晰很多。

GameCharacter 状态模式 NormalState BerserkState WeakState 策略模式 SwordAttack BowAttack StaffAttack 装饰器模式 FireEnchant IceAura LifeSteal 状态 → 策略 → 装饰器,层层递进,各司其职

代码实现:状态模式

先看状态模式。角色在不同状态下,攻击力和移动速度都不一样。

// 状态接口
class ICharacterState {
public:
    virtual ~ICharacterState() = default;
    virtual void handleAttack(GameCharacter& character, int baseDamage) = 0;
    virtual void handleMove(GameCharacter& character, float speed) = 0;
    virtual std::string getStateName() const = 0;
};

// 正常状态
class NormalState : public ICharacterState {
public:
    void handleAttack(GameCharacter& character, int baseDamage) override {
        character.setDamage(baseDamage);
        std::cout << "正常攻击,伤害: " << baseDamage << std::endl;
    }
    void handleMove(GameCharacter& character, float speed) override {
        character.setSpeed(speed);
        std::cout << "正常移动,速度: " << speed << std::endl;
    }
    std::string getStateName() const override { return "Normal"; }
};

// 狂暴状态
class BerserkState : public ICharacterState {
public:
    void handleAttack(GameCharacter& character, int baseDamage) override {
        character.setDamage(baseDamage * 2);
        std::cout << "狂暴攻击!伤害翻倍: " << baseDamage * 2 << std::endl;
    }
    void handleMove(GameCharacter& character, float speed) override {
        character.setSpeed(speed * 1.5f);
        std::cout << "狂暴移动,速度提升50%: " << speed * 1.5f << std::endl;
    }
    std::string getStateName() const override { return "Berserk"; }
};

我的习惯:状态模式里,每个状态类只关心「自己状态下该怎么做」。这样加一个新状态,只需要新增一个类,不用改现有代码。开闭原则,说白了就是「对扩展开放,对修改关闭」。

代码实现:策略模式

策略模式负责攻击方式。角色可以随时换武器,每种武器有独立的伤害计算逻辑。

// 攻击策略接口
class IAttackStrategy {
public:
    virtual ~IAttackStrategy() = default;
    virtual int calculateDamage(int baseDamage) = 0;
    virtual std::string getWeaponName() const = 0;
};

// 剑攻击
class SwordAttack : public IAttackStrategy {
public:
    int calculateDamage(int baseDamage) override {
        return baseDamage + 10;  // 剑有额外10点伤害
    }
    std::string getWeaponName() const override { return "Sword"; }
};

// 弓攻击
class BowAttack : public IAttackStrategy {
public:
    int calculateDamage(int baseDamage) override {
        return static_cast<int>(baseDamage * 0.8f);  // 弓伤害略低
    }
    std::string getWeaponName() const override { return "Bow"; }
};

// 法杖攻击
class StaffAttack : public IAttackStrategy {
public:
    int calculateDamage(int baseDamage) override {
        return baseDamage + 20;  // 法杖伤害高但攻速慢
    }
    std::string getWeaponName() const override { return "Staff"; }
};

这里有个细节:策略模式只负责「伤害计算」,不负责「能不能攻击」。能不能攻击是状态模式管的。我曾经见过有人把状态判断也塞进策略里,结果两个模式耦合在一起,改起来很痛苦。

代码实现:装饰器模式

装饰器模式最灵活。它可以在攻击时动态叠加额外效果。注意,装饰器包装的是「攻击行为」,而不是角色本身。

// 基础攻击接口
class IAttack {
public:
    virtual ~IAttack() = default;
    virtual void execute(GameCharacter& character) = 0;
};

// 基础攻击实现
class BaseAttack : public IAttack {
public:
    void execute(GameCharacter& character) override {
        int damage = character.getAttackStrategy()->calculateDamage(
            character.getBaseDamage()
        );
        character.getCurrentState()->handleAttack(character, damage);
    }
};

// 装饰器基类
class AttackDecorator : public IAttack {
protected:
    std::unique_ptr<IAttack> wrappedAttack_;
public:
    AttackDecorator(std::unique_ptr<IAttack> attack) 
        : wrappedAttack_(std::move(attack)) {}
};

// 火焰附魔装饰器
class FireEnchant : public AttackDecorator {
public:
    using AttackDecorator::AttackDecorator;
    void execute(GameCharacter& character) override {
        wrappedAttack_->execute(character);
        std::cout << "🔥 火焰附魔额外造成5点灼烧伤害" << std::endl;
    }
};

// 冰冻光环装饰器
class IceAura : public AttackDecorator {
public:
    using AttackDecorator::AttackDecorator;
    void execute(GameCharacter& character) override {
        wrappedAttack_->execute(character);
        std::cout << "❄️ 冰冻光环减速敌人2秒" << std::endl;
    }
};

我曾经踩过的坑:装饰器模式里,如果装饰器之间有依赖关系(比如必须先有火焰才能加冰冻),那就麻烦了。我的建议是:装饰器之间保持独立,不要互相依赖。每个装饰器只做一件事,做好就收手。

组装起来:角色类

最后看角色类如何把三个模式串起来。

class GameCharacter {
private:
    std::unique_ptr<ICharacterState> state_;
    std::unique_ptr<IAttackStrategy> strategy_;
    std::unique_ptr<IAttack> attackChain_;
    int baseDamage_;
    float baseSpeed_;
    
public:
    GameCharacter()
        : state_(std::make_unique<NormalState>())
        , strategy_(std::make_unique<SwordAttack>())
        , attackChain_(std::make_unique<BaseAttack>())
        , baseDamage_(50)
        , baseSpeed_(10.0f) {}
    
    void setState(std::unique_ptr<ICharacterState> state) {
        state_ = std::move(state);
    }
    
    void setStrategy(std::unique_ptr<IAttackStrategy> strategy) {
        strategy_ = std::move(strategy);
    }
    
    void addDecorator(std::unique_ptr<IAttack> decorator) {
        // 装饰器包装现有攻击链
        attackChain_ = std::make_unique<FireEnchant>(std::move(attackChain_));
    }
    
    void performAttack() {
        attackChain_->execute(*this);
    }
    
    void move() {
        state_->handleMove(*this, baseSpeed_);
    }
    
    // getter/setter 略...
};

使用示例

int main() {
    GameCharacter hero;
    
    // 正常状态,用剑攻击
    hero.performAttack();  // 正常攻击,伤害50+10
    
    // 切换到狂暴状态
    hero.setState(std::make_unique<BerserkState>());
    hero.performAttack();  // 狂暴攻击!伤害(50+10)*2
    
    // 换法杖
    hero.setStrategy(std::make_unique<StaffAttack>());
    hero.performAttack();  // 狂暴攻击!伤害(50+20)*2
    
    // 加火焰附魔
    hero.addDecorator(std::make_unique<FireEnchant>(...));
    hero.performAttack();  // 狂暴攻击 + 火焰附魔
    
    return 0;
}

面试追问点

面试官可能会问:这三个模式能不能互相替换?我的回答是:不能。它们解决的是不同维度的问题。状态模式管「行为选择」,策略模式管「算法替换」,装饰器模式管「功能叠加」。你想想看,如果把状态切换的逻辑用装饰器实现,那代码会乱成什么样?

总结一句话:状态模式让角色「识时务」,策略模式让角色「有手段」,装饰器模式让角色「有花样」。三个模式各管一摊,互不干扰,这才是优雅的架构。

这个案例我面试时经常用来考察候选人的架构思维。能说清楚三个模式各自职责的人,代码质量一般不会差。嗯,今天就聊到这里,希望对你有所帮助。