设计模式综合实战四:基于C++实现游戏引擎(状态+策略+观察者+工厂)

说实话,游戏引擎这个题目,我一开始是有点犹豫的。因为游戏引擎涉及的东西太多了,渲染、物理、音频、网络……但后来我想通了——我们不是要做一个商业引擎,而是要用它来串联我们学过的设计模式。说白了,这就是一次“设计模式大阅兵”。

我个人习惯把这种综合实战叫做“模式交响曲”。每个模式都有自己的声部,合在一起才能奏出好听的曲子。今天我们就用C++来写一个迷你游戏引擎的核心骨架,把状态模式、策略模式、观察者模式和工厂模式都揉进去。

一、整体架构设计

先说说这个引擎要干什么。我们要做一个简单的2D游戏引擎,支持角色在不同状态间切换(比如站立、跑动、跳跃),支持不同的移动算法(比如普通移动、加速移动、飞行),还要有事件系统(比如血量变化、得分变化),最后用工厂来创建各种游戏对象。

嗯,这里要注意:这四个模式不是随便堆在一起的。它们各自负责一个维度:

  • 状态模式:管理角色的行为状态
  • 策略模式:封装不同的移动算法
  • 观察者模式:实现事件通知机制
  • 工厂模式:解耦对象的创建过程

核心思路:每个模式解决一个特定的变化点。状态模式应对行为变化,策略模式应对算法变化,观察者模式应对通知变化,工厂模式应对创建变化。

下面这张图展示了它们之间的关系:

游戏引擎设计模式架构图 GameEngine 核心 状态模式 角色状态管理 策略模式 移动算法封装 观察者模式 事件通知系统 工厂模式 对象创建工厂 IdleState RunState / JumpState NormalMove FastMove / FlyMove HealthObserver ScoreObserver PlayerFactory EnemyFactory 状态切换触发策略变更 状态变化通知观察者

二、状态模式:角色的行为切换

先看状态模式。游戏角色最常见的需求就是状态切换——站着、跑着、跳着。每个状态下的行为都不一样。我在项目中遇到过一个问题:如果不用状态模式,代码里全是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