Xcode与C++之游戏开发:人工智能算法

本文介绍了游戏开发中的人工智能算法,包括状态机的设计和使用类实现,以及寻路算法如广度优先搜索、启发式搜索(A*、Dijkstra)在游戏中的应用。通过状态机实现游戏AI的行为变化,使用寻路算法解决路径规划问题,为游戏设计更智能的决策系统。
摘要由CSDN通过智能技术生成

人工智能算法在游戏中被用于决定计算机控制的实体的行动。常用的游戏人工智能算法包括行为状态机、寻路算法、两个玩家轮流博弈中常用的游戏树。

状态机

对于非常简单的游戏,比如之前编写的 Pong 游戏,游戏 AI 都有相同的行为。在整个游戏中,行为都没有发生什么改变。

稍微复杂一点的游戏,不同时间里,不同场景地点中,游戏 AI 的行为也会发生变化。比如在经典的吃豆人 Pac-Man 中,每个幽灵就有三种行为,追赶玩家、散开(一开始的时候)、逃离玩家。行为之间的变化的经典实现就是状态机。

自动贩卖机也是一种状态机。另外,状态机的非常有名,图灵的计算机模型就是基于状态机的。状态机图灵完备,理论上可以实现一切可计算的功能。

状态机设计

定义状态、进入状态时候的操作和退出状态转换操作最终构成状态机。实现游戏 AI 最重要的问题是状态之间如何转换。例如,NPC 在预定义的路径上巡逻,发现了玩家,就会开始攻击玩家。巡逻是一个状态,发现玩家后,就转换到了攻击状态。如果玩家可以发动攻击,NPC 有可能被杀死。死亡本身也是一种状态。

我们可以设计一个 AIComponent 来实现上述的状态行为。首先定义三个状态,巡逻(Patrol)、死亡(Death)、攻击(Attack):

enum class AIState
{
   
    Patrol,
    Death,
    Attack
};

然后就可以创建一个 AICompoent 类拥有 AIState 这个成员数据,还可以分开定义每个状态的更新函数。

最简单的实现大概类似这样:

void AIComponent::Update(float deltaTime)
{
   
    switch (mState) {
   
        case AIState::Patrol
            UpdatePatrol(deltaTime);
            break;
        case AIState::Death:
            UpdateDeath(deltaTime);
            break;
        case AIState::Attack:
            UpdateDeath(deltaTime);
            break;
        default:
            break;
    }
}

为了实现状态的转换,可以实现一个函数 ChangeState,大体思路就是传入一个新状态,然后函数先退出原有状态,设置成新状态,进入新状态:

void AIComponent::ChangeState(AIState newState)
{
   
    // 退出当前状态(调用该状态的 Exit 函数)
    
    mState = newState;
    
    // 进入新的状态
}

上面的这种实现非常简单,但有问题。第一,不具备伸缩性。添加更多的状态同时降低 UpdateChangeState 的可读性。另外,为每一个状态设置 UpdateEnterExit 函数也会让代码难以追踪。

有可能多个 AI 共享同一个状态,例如巡逻 Patrol,但上面这种处理显然很难进行复用。因此,我们可能不该把数据和操作分离开,而应该使用一种面向对象的思维将状态设计成类。

使用类实现状态

为了实现状态的可扩展,将状态本身抽象成为一个基类:

class AIState
{
   
public:
    AIState(class AIComponent* owner): mOwner(owner) {
   
        
    }
    // 特定状态行为
    virtual void Update(float deltaTime) = 0;
    virtual void OnEnter() = 0;
    virtual void OnExit() = 0;
    // 获取状态的名字
    virtual const char* GetName() const = 0;
protected:
    class AIComponent* mOwner;
};

通过 mOwner 成员变量,可以将 AIState 联系到特定的 AIComponent 上。下面,定义 AIComponent

class AIComponent : public Component
{
   
public:
    AIComponent(class Actor* owner);
    
    void Update(float deltaTime) override;
    void ChangeState(const std::string& name);
    
    // 添加新状态
    void RegisterState(class AIState* state);
private:
    // 映射状态名到 AIState 实例
    std::unordered_map<std::string, class AIState*> mStateMap;
    // 当前状态
    class AIState* mCurrentState;
};

注意,AIComponent 有一个哈希表映射名字到 AIState 实例的指针,同时也保持有一个指向当前状态的指针。RegisterState 函数将 AIState 的指针添加到映射上:

void AIComponent::RegisterState(class AIState* state)
{
   
   mStateMap.emplace(state->GetName(), state);
}

AIComponent::Update 非常直截了当,调用当前状态的 Update 函数就可以了:

void AIComponent::Update(float deltaTime)
{
   
    if (mCurrentState)
    {
   
        mCurrentState->Update(deltaTime);
    }
}

ChangeState 稍微要做的事情多一些,首先要调用 OnExit 函数退出当前状态,然后在哈希表中找到新状态,并调用 OnEnter

void AIComponent::ChangeState(const std::string& name)
{
   
    // 退出当前的状态
    if (mCurrentState)
    {
   
        mCurrentState->OnExit();
    }
    
    // 在映射中找到新状态
    auto iter = mStateMap.find(name);
    if (iter != mStateMap.end())
    {
   
        mCurrentState = iter->second;
        // 进入状态
        mCurrentState->OnEnter();
    }
    else
    {
   
        SDL_Log("无法在映射中找到AIState %s", name.c_str());
        mCurrentState = nullptr;
    }
}

具体的状态,像巡逻 Patrol 可以通过继承自 AIState 实现:

class AIPatrol : public AIState
{
   
public:
    AIPatrol(class AIComponent* owner)
        :AIState(owner)
    {
    }

    // 重写行为
    void Update(float deltaTime) override;
    void OnEnter() override;
    void OnExit() override;

    const char* GetName() const override
    {
    return "Patrol"; }
};

就可以实现特定的 UpdateOnEnterOnExit

void AIPatrol::Update(float deltaTime)
{
   
    SDL_Log("更新%s状态", GetName());
    // 可以做一些操作
    bool dead = true; // 这里只是简单示例,实际当然得弄清楚是不是死了
    if (dead)
    {
   
        mOwner->ChangeState("Death");
    }
}

实际使用的时候需要把状态注册到 AIComponent 上。

寻路

寻路算法用于找出两个之间的路径,并且在路径中绕开障碍物。这个问题的复杂性在于可能两点之间存在大量的路径,但是这些路径中只有少数路径是最短的

在求解寻路问题之前,我们需要找到一个表示游戏世界里 AI 路径的方式。显然,最经典的选择就是 数据结构:图的实现。这里假定读者对基本图数据结构有所了解。

我们这里使用的是邻接表实现的图结构。不同游戏也会用不同方式表示游戏世界,例如将世界划分为正方形是最简单的方法。这种实现方式在基于回合的策略游戏中非常普遍,例如:文明和X-COM。然而,对于其他很多类型的游戏,这种实现可能是不可行的。

struct GraphNode
{
   
    // 邻接表
    std::vector<GraphNode*> mAdjacent;
};

struct Graph
{
   
    std::vector<GraphNode*> mNodes;
  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值