C++游戏开发之旅 21

问题概述

现在我们实现了玩家和敌人之间的交互,玩家可以踩踏敌人,但是目前,敌人是静止不动的,这不好,我们希望敌人可以移动、巡逻、甚至追击玩家。

解决方案

策略模式的AI系统,游戏中敌人行为有很多种,有的在地面巡逻,有的在空中,有的会跳跃。如果采用通过EnemyComponent加上复杂的if-else或是switch来处理逻辑是不现实的。

策略设计模式

  • 定义一系列算法(策略):将每种AI行为分别封装在独立的类中
  • 使它们可以互换:让这些算法类实现一个共同的接口
  • 客户端与算法分离:AI的持有者只依赖于接口,而不知道具体使用的是哪个算法

策略模式架构:

┌──────────────────┐
│   GameObject     │
│   (敌人对象)      │
└────────┬─────────┘
         │ 拥有
         ↓
┌──────────────────┐
│   AIComponent    │ ← 上下文 Context
│   (AI管理器)      │   持有策略接口
└────────┬─────────┘
         │ 持有
         ↓
┌──────────────────┐
│   AIBehavior     │ ← 策略接口 Strategy
│   (抽象基类)      │
└────────┬─────────┘
         │ 实现
    ┌────┴────┬─────────┐
    ↓         ↓         ↓
┌────────┐┌────────┐┌────────┐
│ Patrol ││ UpDown ││  Jump  │ ← 具体策略
│巡逻行为││飞行行为││跳跃行为│   Concrete Strategies
└────────┘└────────┘└────────┘

image-20260228161120823

章节目标

  • 学习策略设计模式 --- 理解策略模式的核心思想并应用于AI系统
  • 构建AI组件框架 --- 创建AIComponent和AIBehavior基类
  • 实现多种AI行为 --- 巡逻、飞行、跳跃三种行为策略
  • 动态装配AI --- 在场景中为敌人配置不同的AI行为

第一部分:架构设计与核心框架

如果按照正常传统设计,我们会给敌人一个EnemyComponent组件,然后正常更新update(),判断敌人类型,是蛤蟆、老鹰还是老鼠,然后分别判断逻辑。这会使得所有逻辑混在一起,修改一个影响全部,拓展困难,一个类承担多种职责。

策略模式:

我们的游戏对象有个AIComponent,其中缓存一个行为behavior指针,将其委托给其他的行为behavior->update(),这样我们可以为每个行为独立封装,运行时可以切换行为,添加新行为时候也比较容易。

架构三要素

角色 职责 说明
AIComonent 上下文Context 持有并调用策略对象 不包含具体逻辑
AIBehavior 策略接口 Strategy 定义行为的抽象接口 纯虚基类
具体行为类 具体策略 Concrete Strategy 实现具体的AI逻辑 PatrolBehavior

运行灵活

同一个敌人游戏对象

开始:巡逻行为 setBehavior(Patrol)

受伤:逃跑行为 setBehavior(Flee)

发现玩家:追击行为 setBehavior(Chase)

我们无需修改对象本身,只需要切换策略

1.行为接口:AIBehavior

首先,创建AIBehavior抽象基类,是具体AI行为的接口

src/game/component/ai/ai_behavior.h

#pragma once

namespace game::component {
class AIComponent;
}

namespace game::component::ai {
/**
 * @brief AI 行为策略的抽象基类
 */
class AIBehavior {
    friend class game::component::AIComponent;
public:
    AIBehavior() = default;
    virtual ~AIBehavior() = default;

    // 禁止拷贝和移动
    AIBehavior(const AIBehavior&) = delete;
    AIBehavior& operator=(const AIBehavior&) = delete;
    AIBehavior(AIBehavior&&) = delete;
    AIBehavior& operator=(AIBehavior&&) = delete;

protected:
    virtual void enter(AIComponent&) {} //enter函数可选是否实现,默认为空
    virtual void update(float, AIComponent&) = 0; // update函数必须实现, 更新AI逻辑行为(具体策略)

};

}

接口设计

  • enter() --- 虚函数,行为初始化,可选实现,行为切换时调用
  • update() --- 纯虚函数,行为逻辑,必须实现,每帧调用

enter()用途:飞行的时候,关闭重力,播放飞行动画;巡逻的时候,开启重力,播放走路动画;攻击时,播放音效等等。行为切换时执行

这里我们的AIBehavior并没有保存指针,可以和原来的设计玩家时进行比对。玩家较为复杂,有很多状态,而且需要控制hanleInput(),所以我们的基类player_state中保存了PlayerComponent的指针,因为每个状态有大量的逻辑判断需要。而我们这里就没有添加,因为只需要处理enter()、update(),同一个行为实例可以用于多个AIComponent组件,并且无需管理指针有效性。

2.AI大脑:AIComponent

AIComponent是AI系统的核心管理者,持有AIBehavior策略对象,并在每帧将工作委托给策略对象。

src/game/component/ai_component.h

#pragma once
#include "../../engine/component/component.h"
#include "ai/ai_behavior.h"
#include <memory>

namespace engine::object {
class GameObject;
}

namespace engine::component {
class TransformComponent;
class PhysicsComponent;
class SpriteComponent;
class AnimationComponent;
}

namespace game::component::ai {
class AIBehavior;
}

namespace game::component {
class AIComponent final : public engine::component::Component {
    friend class engine::object::GameObject;
private:
    std::unique_ptr<ai::AIBehavior> current_behavior_ = nullptr;  // AI行为策略

    // 缓存组件指针
    engine::component::TransformComponent* transform_component_ = nullptr;
    engine::component::PhysicsComponent* physics_component_ = nullptr;
    engine::component::SpriteComponent* sprite_component_ = nullptr;
    engine::component::AnimationComponent* animation_component_ = nullptr;

public:
    AIComponent() = default;
    ~AIComponent() override = default;

    // 禁止拷贝和移动
    AIComponent(const AIComponent&) = delete;
    AIComponent& operator=(const AIComponent&) = delete;
    AIComponent(AIComponent&&) = delete;
    AIComponent& operator=(AIComponent&&) = delete;

    void setBehavior(std::unique_ptr<ai::AIBehavior> behavior);
    bool takeDamage(int damage);
    bool isAlive();

    // getters
    engine::component::TransformComponent* getTransformComponent() const { return transform_component_; }
    engine::component::PhysicsComponent* getPhysicsComponent() const { return physics_component_; }
    engine::component::SpriteComponent* getSpriteComponent() const { return sprite_component_; }
    engine::component::AnimationComponent* getAnimationComponent() const {return animation_component_;}

private:
    void init() override;
    void update(float, engine::core::Context&) override;

};

}

AIComponent持有行为策略指针,并且缓存了相关组件(transform,physics,sprite,animation),委托执行相应的行为逻辑current_behavior_->update()

src/game/component/ai_component.cpp

#include "ai_component.h"
#include "../../engine/object/game_object.h"
#include "../../engine/component/transform_component.h"
#include "../../engine/component/sprite_component.h"
#include "../../engine/component/physics_component.h"
#include "../../engine/component/animation_component.h"
#include "../../engine/component/health_component.h"

#include <spdlog/spdlog.h>

namespace game::component {
    void AIComponent::setBehavior(std::unique_ptr<ai::AIBehavior> behavior)
    {
        current_behavior_ = std::move(behavior);
        current_behavior_->enter(*this);
    }
   
    void AIComponent::init()
    {
        if(!owner_) {
            spdlog::error("AIComponent没有拥有者");
            return;
        }
        transform_component_ = owner_->getComponent<engine::component::TransformComponent>();
        physics_component_ = owner_->getComponent<engine::component::PhysicsComponent>();
        sprite_component_ = owner_->getComponent<engine::component::SpriteComponent>();
        animation_component_ = owner_->getComponent<engine::component::AnimationComponent>();

        if(!transform_component_ || !physics_component_ || !sprite_component_ || !animation_component_) {
            spdlog::error("AIComponent初始化失败,缺少必要的组件");
            return;
        }
    }

    void AIComponent::update(float dt, engine::core::Context &)
    {
        if(current_behavior_) {
            current_behavior_->update(dt,*this);
        } else {
            spdlog::error("AIComponent组件当前没有行为策略");
        }
    }

    bool AIComponent::takeDamage(int damage)
    {
        auto health_component = owner_->getComponent<engine::component::HealthComponent>();
        if(health_component) {
            return health_component->takeDamage(damage);
        }
        return false;
    }

    bool AIComponent::isAlive()
    {
        auto health_component = owner_->getComponent<engine::component::HealthComponent>();
        if(health_component) {
            return health_component->isAlive();
        }
        return true;
    }

}

第二部分:实现具体AI行为

接下来,我们创建三个具体的行为策略,对应不同的敌人类型

3.左右巡逻:PatrolBehavior

这个老鼠会在一定范围水平内进行来回移动

src/game/component/ai/patrol_behavior.h

#pragma once

#include "ai_behavior.h"

namespace game::component {
class AIComponent;
}

namespace game::component::ai {
class PatrolBehavior : public AIBehavior {
    friend class AIComponent;
private:
    float patrol_min_x_ = 0.0f; // 巡逻范围最小x
    float patrol_max_x_ = 0.0f; // 巡逻范围最大x
    float move_speed_ = 50.0f; // 移动速度
    bool moving_right_ = false; // 是否向右移动

public:
    PatrolBehavior(float min_x, float max_x, float move_speed, bool moving_right = false);
    ~PatrolBehavior() override = default;

    void enter(AIComponent& ai) override;
    void update(float dt, AIComponent& ai) override;

};

}

src/game/component/ai/patrol_behavior.cpp

#include "patrol_behavior.h"
#include "../ai_component.h"
#include "../../../engine/component/animation_component.h"
#include "../../../engine/component/physics_component.h"
#include "../../../engine/component/transform_component.h"
#include "../../../engine/component/sprite_component.h"
#include <spdlog/spdlog.h>

namespace game::component::ai {
    PatrolBehavior::PatrolBehavior(float min_x, float max_x, float move_speed, bool moving_right)
    : patrol_min_x_(min_x), patrol_max_x_(max_x), move_speed_(move_speed), moving_right_(moving_right)
    {
        if(patrol_min_x_ > patrol_max_x_){
            spdlog::error("最小巡逻x坐标不能大于最大巡逻x坐标");
            patrol_min_x_ = patrol_max_x_;
        }
    }

    void PatrolBehavior::enter(AIComponent &ai_component)
    {
        if(auto* ac = ai_component.getAnimationComponent(); ac){
            ac->playAnimation("walk");
        }
    }
    
    void PatrolBehavior::update(float, AIComponent &ai_component)
    {
        auto* pc = ai_component.getPhysicsComponent();
        auto* tc = ai_component.getTransformComponent();
        auto* sc = ai_component.getSpriteComponent();
        if(!pc || !tc || !sc){
            spdlog::error("缺少必要组件,无法执行巡逻行为");
            return;
        }
        auto current_x = tc->getPosition().x;

        if(pc->hasCollidedRight() || current_x >= patrol_max_x_){
            pc->velocity_.x = -move_speed_;
            moving_right_ = false;
        } else if(pc->hasCollidedLeft() || current_x <= patrol_min_x_){
            pc->velocity_.x = move_speed_;
            moving_right_ = true;
        } 
        sc->setFlipped(moving_right_);

    }
    
}

水平巡逻逻辑会在一定水平范围内进行,如果到达边界或者撞到墙壁,则进行转向操作。

4.上下飞行:UpDownBehavior

老鹰也是同样的逻辑,在垂直方向上进行巡逻。

// src/game/component/ai/updown_behavior.h
#pragma once

#include "ai_behavior.h"

namespace game::component {
class AIComponent;
}

namespace game::component::ai {
class UpDownBehavior : public AIBehavior {
    friend class AIComponent;
private:
    float patrol_min_y_ = 0.0f; // 巡逻范围最小y
    float patrol_max_y_ = 0.0f; // 巡逻范围最大y
    float move_speed_ = 50.0f; // 移动速度

public:
    UpDownBehavior(float min_y, float max_y, float move_speed);
    ~UpDownBehavior() override = default;

    void enter(AIComponent& ai) override;
    void update(float dt, AIComponent& ai) override;

};

}

//src/game/component/ai/updown_behavior.cpp
#include "updown_behavior.h"
#include "../ai_component.h"
#include "../../../engine/component/animation_component.h"
#include "../../../engine/component/physics_component.h"
#include "../../../engine/component/transform_component.h"
#include "../../../engine/component/sprite_component.h"
#include <spdlog/spdlog.h>

namespace game::component::ai {
    UpDownBehavior::UpDownBehavior(float min_y, float max_y, float move_speed)
    : patrol_min_y_(min_y), patrol_max_y_(max_y), move_speed_(move_speed) 
    {
        if(patrol_min_y_ > patrol_max_y_) {
            spdlog::error("巡逻最小y值必须大于最大y值");
            patrol_min_y_ = patrol_max_y_;
        }
    }

    void UpDownBehavior::enter(AIComponent &ai)
    {
        if(auto* ac = ai.getAnimationComponent(); ac){
            ac->playAnimation("fly");
        }
        // 禁用重力
        if(auto* pc = ai.getPhysicsComponent(); pc){
            pc->setUseGravity(false);
        }
    }

    void UpDownBehavior::update(float , AIComponent &ai_component)
    {
        auto* pc = ai_component.getPhysicsComponent();
        auto* tc = ai_component.getTransformComponent();
        if(!pc || !tc){
            spdlog::error("缺少必要组件,无法执行巡逻行为");
            return;
        }
        auto current_y = tc->getPosition().y;

        // 如果碰到上方或下方,则改变方向
        if(pc->hasCollidedAbove() || current_y <= patrol_min_y_){
            pc->velocity_.y = move_speed_;
        } 
        else if(pc->hasCollidedBelow() || current_y >= patrol_max_y_){
            pc->velocity_.y = -move_speed_;
        } 

    }
}

区别与前面的水平方向巡逻逻辑,我们会禁用重力。

和水分方向差不多,当老鹰y方向达到边界或者触碰到墙壁时,速度方向改变,这里的精灵翻转也是不进行的。

5.周期跳跃:JumpBehavior

这是比较复杂的行为,应用于蛤蟆,首先我们需要清楚蛤蟆的行为逻辑,它有3个动作,idle、jump、fall;首先会在地面上待机一会,然后跳跃,落地,继续待机,这样往复循环。

//src/game/component/ai/jump_behavior.h
#pragma once

#include "ai_behavior.h"
#include <glm/vec2.hpp>

namespace game::component {
class AIComponent;
}

namespace game::component::ai {
class JumpBehavior : public AIBehavior {
    friend class AIComponent;
private:
    float patrol_min_x_;     // Patrol area min x
    float patrol_max_x_;     // Patrol area max x
    glm::vec2 jump_vel_;       // Jump velocity
    float jump_interval_;      // Jump interval

    float jump_timer_ = 0.0f;      // Jump timer
    bool jumping_right_ = false;   // Is jumping right

public:
    JumpBehavior(float min_x, float max_x, glm::vec2 jump_vel = {110.f,-260.f}, float jump_interval = 2.0f);
    ~JumpBehavior() override = default;

    void enter(AIComponent& ai) override;
    void update(float dt, AIComponent& ai) override;

};

}
//src/game/component/ai/jump_behavior.cpp
#include "jump_behavior.h"
#include "../ai_component.h"
#include "../../../engine/component/animation_component.h"
#include "../../../engine/component/physics_component.h"
#include "../../../engine/component/transform_component.h"
#include "../../../engine/component/sprite_component.h"
#include <spdlog/spdlog.h>


namespace game::component::ai {
    JumpBehavior::JumpBehavior(float min_x, float max_x, glm::vec2 jump_vel, float jump_interval)
    : patrol_min_x_(min_x), patrol_max_x_(max_x), jump_vel_(jump_vel), jump_interval_(jump_interval)
    {
        if(patrol_min_x_ > patrol_max_x_){
            spdlog::error("最小巡逻x坐标不能大于最大巡逻x坐标");
            patrol_min_x_ = patrol_max_x_;
        }
    }

    void JumpBehavior::enter(AIComponent &ai_component)
    {
        if(auto* ac = ai_component.getAnimationComponent(); ac){
            ac->playAnimation("idle");
        }
    }

    void JumpBehavior::update(float dt, AIComponent &ai_component)
    {
        auto* pc = ai_component.getPhysicsComponent();
        auto* tc = ai_component.getTransformComponent();
        auto* sc = ai_component.getSpriteComponent();
        auto* ac = ai_component.getAnimationComponent();
        if(!pc || !tc || !sc || !ac){
            spdlog::error("缺少必要组件,无法执行巡逻行为");
            return;
        }
        
        auto current_x = tc->getPosition().x;
        auto is_on_ground = pc->hasCollidedBelow();
        
        if(is_on_ground){
            // 跳跃计时器
            jump_timer_ += dt;
            pc->velocity_ = {0,0};
            // 跳跃逻辑
            if(jump_timer_ > jump_interval_){
                jump_timer_ = 0;
                // 检查转向逻辑
                if(!jumping_right_ && current_x < patrol_min_x_ || pc->hasCollidedLeft()){
                    jumping_right_ = true;
                } else if(jumping_right_ && current_x > patrol_max_x_ || pc->hasCollidedRight()){
                    jumping_right_ = false;
                }
                auto jump_vel_x = jumping_right_ ? jump_vel_.x : -jump_vel_.x;
                pc->velocity_ = {jump_vel_x,jump_vel_.y};
                ac->playAnimation("jump");
            } else {
                ac->playAnimation("idle");
            }
        } else { // 在空中
            if(pc->velocity_.y < 0){
                ac->playAnimation("jump");
            } else{
                ac->playAnimation("fall");
            }
        }
        // 方向设置
        sc->setFlipped(jumping_right_);
    }

}

跳跃行为

我们通过物理组件中的hasCollidedBelow()函数来判断是否在地面上还是在空中两种状态。

if 地面上:
1.跳跃计时器累加
2.速度设置为0
if 3.达到跳跃冷却(是否跳跃)
4.重置计时器
5.是否转向检查
6.设置跳跃速度
7.播放跳跃动画
else 8.播放待机动画
else 空中:
1.检查y方向速度
2.大于0 -> fall
3.小于0 -> jump

JumpBehavior中内置状态机,通过计时器+方向判断来实现行为逻辑更新

第三部分:在场景中装配AI

现在,我们需要在GameScene中为敌人装上AIComponent,并为其设置行为setBehavior

6.动态装配AI系统

我们在 GameSceneinitEnemyAndItem 函数中为每个敌人添加 AIComponent 并配置相应的行为。

// src/game/scene/game_scene.cpp
bool GameScene::initEnemyAndItem()
{
    for(const auto& obj : game_objects_){
        if(obj->getName() == "eagle"){
            float max_y = obj->getComponent<engine::component::TransformComponent>()->getPosition().y;
            auto* ai = obj->addComponent<game::component::AIComponent>();
            ai->setBehavior(std::make_unique<game::component::ai::UpDownBehavior>(
                max_y - 80.f,max_y,50.f
            ));
        }
        if(obj->getTag() == "item"){
            auto* ac = obj->getComponent<engine::component::AnimationComponent>();
            ac->playAnimation("idle");
        }
        if(obj->getName() == "frog"){
            float max_x = obj->getComponent<engine::component::TransformComponent>()->getPosition().x;
            auto* ai = obj->addComponent<game::component::AIComponent>();
            ai->setBehavior(std::make_unique<game::component::ai::JumpBehavior>(
                max_x - 100.f,max_x - 10.f
            ));
        }
        if(obj->getName() == "opossum"){
            float max_x = obj->getComponent<engine::component::TransformComponent>()->getPosition().x;
            auto* ai = obj->addComponent<game::component::AIComponent>();
            ai->setBehavior(std::make_unique<game::component::ai::PatrolBehavior>(
                max_x - 80.f,max_x, 45.f
            ));
        }
    }
    return true;
}

装配流程

AI装配流程:
    ↓
1. 遍历所有游戏对象
    for (auto& game_object : game_objects_)
    ↓
2. 根据名称识别敌人类型
    if (name == "eagle") / "frog" / "opossum"
    ↓
3. 添加 AIComponent
    addComponent<AIComponent>()
    ↓
4. 获取初始位置
    getPosition()
    ↓
5. 计算活动范围
    min, max
    ↓
6. 创建对应的行为策略
    make_unique<Behavior>(min, max)
    ↓
7. 注入到 AIComponent
    setBehavior(behavior)
    ↓
敌人开始执行AI行为

这样,后面如果有新敌人,我们只要设计新的策略就可以了。

编译运行

游戏可以正常执行相应的逻辑

章节总结

完整的AI系统架构

AI系统层次结构:
GameScene <---  场景层 配置策略
(装配AI行为)
	↓
GameObject <---  对象层 拥有组件
(持有AI组件)
	↓
AIComponent <---  组件层 持有策略
(管理AI行为)
	↓
AIBehavior <---  策略接口层 定义行为
(抽象行为接口)
	↓
Patrol	UpDown	Jump <--- 具体策略层 实现逻辑

职责划分

清晰的职责分工:
┌──────────────────┐
│ GameScene        │ ← 策略配置者
│ "为谁配什么行为"  │   装配职责
└────────┬─────────┘
         │
         ↓
┌──────────────────┐
│ AIComponent      │ ← 策略管理者
│ "持有并调用策略"  │   委托职责
└────────┬─────────┘
         │
         ↓
┌──────────────────┐
│ AIBehavior       │ ← 策略执行者
│ "实现具体逻辑"    │   实现职责
└──────────────────┘

策略模式是处理"多种可替换算法"的最佳实践。我们的AI系统现在高度解耦、易于扩展。想添加一种新的敌人?只需编写一个新的 AIBehavior 子类,然后在 GameScene 中为其装配即可,无需改动任何现有AI代码。这正是优秀软件架构的魅力所在——开放扩展,关闭修改(开闭原则)。

遇到的问题

1.在编写AIComponent的时候,里面需要缓存一个行为指针,但是当时使用的是裸指针,而不是智能指针,这个问题我时常会犯,一不留神就下意识出现了。在使用指针的时候一定要注意,先问自己个问题:1.这个对象的生命周期由当前类管理控制吗?2.当前类需要负责delete这个对象吗?得多注意,这里这个行为指针当然由AIComponent控制。

2.在进行老鹰上下行为设置的时候,发现老鹰始终不动。

最后发现问题出在patrol巡逻位置的设置上,因为刚开始,老鹰这个游戏对象是没有速度的,而且不受重力,这时候物理引擎更新显然位置上是不会变化的。这是上下巡逻的逻辑,可以看到,我们设置了上下两个边界,通过变换组件得到tc->getPosition().y位置,如果第一帧开始,其y方向位置就处在两个边界之内的话,那么速度就始终是0了,这样显然不会进行位置的更新了。

if(pc->hasCollidedAbove() || current_y <= patrol_min_y_){
    pc->velocity_.y = move_speed_;
} 
else if(pc->hasCollidedBelow() || current_y >= patrol_max_y_){
    pc->velocity_.y = -move_speed_;
} 
posted @ 2026-03-02 20:39  wenyiGamecpp  阅读(63)  评论(0)    收藏  举报