设计模式综合实战四:基于C++实现游戏引擎(状态+策略+观察者+工厂)
说实话,游戏引擎这个题目,我一开始是有点犹豫的。因为游戏引擎涉及的东西太多了,渲染、物理、音频、网络……但后来我想通了——我们不是要做一个商业引擎,而是要用它来串联我们学过的设计模式。说白了,这就是一次“设计模式大阅兵”。
我个人习惯把这种综合实战叫做“模式交响曲”。每个模式都有自己的声部,合在一起才能奏出好听的曲子。今天我们就用C++来写一个迷你游戏引擎的核心骨架,把状态模式、策略模式、观察者模式和工厂模式都揉进去。
一、整体架构设计
先说说这个引擎要干什么。我们要做一个简单的2D游戏引擎,支持角色在不同状态间切换(比如站立、跑动、跳跃),支持不同的移动算法(比如普通移动、加速移动、飞行),还要有事件系统(比如血量变化、得分变化),最后用工厂来创建各种游戏对象。
嗯,这里要注意:这四个模式不是随便堆在一起的。它们各自负责一个维度:
- 状态模式:管理角色的行为状态
- 策略模式:封装不同的移动算法
- 观察者模式:实现事件通知机制
- 工厂模式:解耦对象的创建过程
核心思路:每个模式解决一个特定的变化点。状态模式应对行为变化,策略模式应对算法变化,观察者模式应对通知变化,工厂模式应对创建变化。
下面这张图展示了它们之间的关系:
二、状态模式:角色的行为切换
先看状态模式。游戏角色最常见的需求就是状态切换——站着、跑着、跳着。每个状态下的行为都不一样。我在项目中遇到过一个问题:如果不用状态模式,代码里全是if-else判断,改一个状态要改好几个地方,特别容易出bug。
来看看C++的实现:
// 状态基类
class CharacterState {
public:
virtual ~CharacterState() = default;
virtual void handleInput(Character* character, const std::string& input) = 0;
virtual void update(Character* character, float deltaTime) = 0;
virtual std::string getStateName() const = 0;
};
// 具体状态:站立
class IdleState : public CharacterState {
public:
void handleInput(Character* character, const std::string& input) override {
if (input == "press_up") {
character->setState(std::make_unique<JumpState>());
} else if (input == "press_right" || input == "press_left") {
character->setState(std::make_unique<RunState>());
}
}
void update(Character* character, float deltaTime) override {
// 站立时不做特殊处理
}
std::string getStateName() const override { return "Idle"; }
};
// 具体状态:跑步
class RunState : public CharacterState {
public:
void handleInput(Character* character, const std::string& input) override {
if (input == "release_right" || input == "release_left") {
character->setState(std::make_unique<IdleState>());
} else if (input == "press_up") {
character->setState(std::make_unique<JumpState>());
}
}
void update(Character* character, float deltaTime) override {
// 跑步时的移动逻辑
character->move(character->getSpeed() * deltaTime);
}
std::string getStateName() const override { return "Run"; }
};
// 具体状态:跳跃
class JumpState : public CharacterState {
private:
float jumpTime = 0.0f;
const float JUMP_DURATION = 0.5f;
public:
void handleInput(Character* character, const std::string& input) override {
// 跳跃中不能切换状态,只能等落地
}
void update(Character* character, float deltaTime) override {
jumpTime += deltaTime;
if (jumpTime >= JUMP_DURATION) {
character->setState(std::make_unique<IdleState>());
jumpTime = 0.0f;
}
// 跳跃的物理逻辑
character->setVerticalVelocity(10.0f * (1.0f - jumpTime / JUMP_DURATION));
}
std::string getStateName() const override { return "Jump"; }
};
我的经验:状态模式最怕的是状态爆炸。如果状态太多,可以考虑用状态机库或者分层状态机。我曾经在一个项目中看到过30多个状态,那维护起来真是噩梦。
三、策略模式:灵活的移动算法
策略模式在这里用来封装不同的移动算法。你想想看,角色可以普通走路、加速跑、甚至飞行。每种移动方式的算法都不一样。如果用继承,你得写一堆子类。用策略模式,直接替换算法对象就行。
// 移动策略接口
class MoveStrategy {
public:
virtual ~MoveStrategy() = default;
virtual Vector2 calculateMove(const Vector2& currentPos,
float speed,
float deltaTime) = 0;
virtual std::string getStrategyName() const = 0;
};
// 普通移动
class NormalMove : public MoveStrategy {
public:
Vector2 calculateMove(const Vector2& currentPos,
float speed,
float deltaTime) override {
return currentPos + Vector2(speed * deltaTime, 0);
}
std::string getStrategyName() const override { return "Normal"; }
};
// 加速移动
class FastMove : public MoveStrategy {
private:
const float BOOST_FACTOR = 2.5f;
public:
Vector2 calculateMove(const Vector2& currentPos,
float speed,
float deltaTime) override {
return currentPos + Vector2(speed * BOOST_FACTOR * deltaTime, 0);
}
std::string getStrategyName() const override { return "Fast"; }
};
// 飞行移动
class FlyMove : public MoveStrategy {
public:
Vector2 calculateMove(const Vector2& currentPos,
float speed,
float deltaTime) override {
// 飞行可以上下左右
return currentPos + Vector2(speed * deltaTime, speed * 0.5f * deltaTime);
}
std::string getStrategyName() const override { return "Fly"; }
};
这里有个细节:策略模式和状态模式可以配合使用。比如角色在RunState下,可以动态切换移动策略。状态决定“做什么”,策略决定“怎么做”。
四、观察者模式:事件通知系统
游戏里到处都是事件——血量变了、得分了、敌人死了。观察者模式就是干这个的。我记得有一次做联机游戏,事件系统没设计好,导致玩家A的操作要等500ms才能同步到玩家B。后来用观察者模式重构了事件总线,延迟降到了50ms以下。
// 观察者接口
class Observer {
public:
virtual ~Observer() = default;
virtual void onEvent(const std::string& eventType, void* data) = 0;
};
// 主题(被观察者)
class Subject {
private:
std::vector<Observer*> observers;
public:
void attach(Observer* obs) {
observers.push_back(obs);
}
void detach(Observer* obs) {
observers.erase(std::remove(observers.begin(), observers.end(), obs),
observers.end());
}
void notify(const std::string& eventType, void* data) {
for (auto* obs : observers) {
obs->onEvent(eventType, data);
}
}
};
// 具体观察者:血量显示
class HealthDisplay : public Observer {
public:
void onEvent(const std::string& eventType, void* data) override {
if (eventType == "health_changed") {
int* health = static_cast<int*>(data);
std::cout << "血量更新: " << *health << std::endl;
}
}
};
// 具体观察者:得分显示
class ScoreDisplay : public Observer {
public:
void onEvent(const std::string& eventType, void* data) override {
if (eventType == "score_changed") {
int* score = static_cast<int*>(data);
std::cout << "得分更新: " << *score << std::endl;
}
}
};
避坑指南:我曾经在观察者模式里犯过一个低级错误——在通知过程中删除观察者,导致迭代器失效。后来我改用“标记删除”的方式,先把要删除的观察者标记出来,通知完再统一清理。
五、工厂模式:对象的统一创建
最后是工厂模式。游戏里要创建各种对象——玩家、敌人、道具。如果直接new,代码耦合度太高。用工厂模式,把创建逻辑集中管理,换一套工厂就能换一套对象族。
// 游戏对象基类
class GameObject {
public:
virtual ~GameObject() = default;
virtual void update(float deltaTime) = 0;
virtual void render() = 0;
};
// 玩家
class Player : public GameObject {
public:
void update(float deltaTime) override {
std::cout << "玩家更新" << std::endl;
}
void render() override {
std::cout << "渲染玩家" << std::endl;
}
};
// 敌人
class Enemy : public GameObject {
public:
void update(float deltaTime) override {
std::cout << "敌人更新" << std::endl;
}
void render() override {
std::cout << "渲染敌人" << std::endl;
}
};
// 抽象工厂
class GameObjectFactory {
public:
virtual ~GameObjectFactory() = default;
virtual std::unique_ptr<GameObject> createPlayer() = 0;
virtual std::unique_ptr<GameObject> createEnemy() = 0;
};
// 具体工厂:普通模式
class NormalGameFactory : public GameObjectFactory {
public:
std::unique_ptr<GameObject> createPlayer() override {
return std::make_unique<Player>();
}
std::unique_ptr<GameObject> createEnemy() override {
return std::make_unique<Enemy>();
}
};
// 具体工厂:困难模式(可以创建更强的敌人)
class HardGameFactory : public GameObjectFactory {
public:
std::unique_ptr<GameObject> createPlayer() override {
return std::make_unique<Player>();
}
std::unique_ptr<GameObject> createEnemy() override {
// 可以返回一个更强的敌人子类
return std::make_unique<HardEnemy>();
}
};
六、整合:游戏引擎核心
好了,现在把四个模式整合到一起。游戏引擎核心类负责协调它们:
class GameEngine {
private:
std::unique_ptr<Character> player;
std::unique_ptr<GameObjectFactory> factory;
std::vector<std::unique_ptr<Observer>> observers;
public:
GameEngine() {
// 使用工厂创建角色
factory = std::make_unique<NormalGameFactory>();
player = std::make_unique<Character>(std::make_unique<IdleState>(),
std::make_unique<NormalMove>());
// 注册观察者
auto healthDisp = std::make_unique<HealthDisplay>();
auto scoreDisp = std::make_unique<ScoreDisplay>();
player->attach(healthDisp.get());
player->attach(scoreDisp.get());
observers.push_back(std::move(healthDisp));
observers.push_back(std::move(scoreDisp));
}
void handleInput(const std::string& input) {
player->handleInput(input);
}
void update(float deltaTime) {
player->update(deltaTime);
}
void setMoveStrategy(std::unique_ptr<MoveStrategy> strategy) {
player->setMoveStrategy(std::move(strategy));
}
};
总结一下:这个迷你引擎虽然简单,但四个设计模式的配合已经体现出来了。状态模式管行为切换,策略模式管算法替换,观察者模式管事件通知,工厂模式管对象创建。它们各司其职,互不干扰。
说实话,设计模式用得好不好,就看你能不能把变化点找出来。每个模式都是在封装一个变化。你找到的变化点越多,代码的扩展性就越好。我在实际项目中见过太多“过度设计”的例子——明明只有一个变化点,硬塞了三个模式进去。嗯,那反而适得其反。
最后说一句:代码是写给人看的,不是写给机器看的。设计模式是为了让代码更容易理解、更容易修改。如果用了设计模式反而让代码变复杂了,那就要反思一下是不是用错了地方。
公众号:蓝海资料掘金营,微信deep3321