设计模式综合案例2:设计一个游戏角色系统(状态+策略+装饰器)
说实话,面试里我最喜欢问这类综合案例题。因为单一模式考察的是「你会不会用」,而综合案例考察的是「你什么时候该用哪个」。今天这个游戏角色系统,我当年在做一个MOBA项目时亲手写过类似的架构。嗯,踩过坑,也重构过,今天把经验分享给你。
需求场景:一个会变身的角色
假设我们要设计一个游戏角色。它有几个特点:
- 角色有不同状态:正常、狂暴、虚弱、隐身
- 角色可以装备不同武器:剑、弓、法杖
- 角色可以叠加各种增益效果:火焰附魔、冰冻光环、吸血
你想想看,如果把这些逻辑全部塞到一个类里,会是什么后果?我见过一个项目,角色类有3000多行,if-else嵌套了七八层。后来加一个新状态,改了三天,改出三个bug。这就是典型的「面条代码」。
架构设计:三模式协同
我们分别用三种模式来解耦:
| 模式 | 负责什么 | 类比现实 |
|---|---|---|
| 状态模式 | 角色行为随状态变化 | 人开心时唱歌,难过时沉默 |
| 策略模式 | 可替换的攻击方式 | 换武器就像换策略 |
| 装饰器模式 | 动态叠加额外效果 | 给武器涂毒、附魔 |
核心思想:状态决定「能不能做」,策略决定「怎么做」,装饰器决定「做了之后额外发生什么」。
先画一张架构图
下面这张图展示了三个模式如何协作。我习惯先画图再写代码,这样思路清晰很多。
代码实现:状态模式
先看状态模式。角色在不同状态下,攻击力和移动速度都不一样。
// 状态接口
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;
}
面试追问点
面试官可能会问:这三个模式能不能互相替换?我的回答是:不能。它们解决的是不同维度的问题。状态模式管「行为选择」,策略模式管「算法替换」,装饰器模式管「功能叠加」。你想想看,如果把状态切换的逻辑用装饰器实现,那代码会乱成什么样?
总结一句话:状态模式让角色「识时务」,策略模式让角色「有手段」,装饰器模式让角色「有花样」。三个模式各管一摊,互不干扰,这才是优雅的架构。
这个案例我面试时经常用来考察候选人的架构思维。能说清楚三个模式各自职责的人,代码质量一般不会差。嗯,今天就聊到这里,希望对你有所帮助。