跳转至

08-黄蜂女状态节点

黄蜂女的设计分为感知和决策。感知的核心在于黄蜂女可以知晓玩家武士零角色的存在,向他移动或者投掷飞行物,而不是自顾自地胡乱出招。
而决策角度,当黄蜂女的生命值低于50%时,会更倾向于使用远程攻击而不是近战冲刺。并演化出召唤刺球进行攻击的技能招式。
Pasted image 20260313110121.png
我们首先纵览一下黄蜂女的状态节点。

1 头文件声明

首先创建enemy_state_nodes.h头文件

class EnemyAimState : public StateNode
{
public:
    EnemyAimState();
    ~EnemyAimState();

    void on_enter() override;
    void on_update(float delta) override;
private:
    Timer timer;
};

class EnemyDashInAirState : public StateNode
{
public:
    EnemyDashInAirState() = default;
    ~EnemyDashInAirState() = default;

    void on_enter() override;
    void on_update(float delta) override;
    void on_exit() override;
private:
    const float SPEED_DASH = 1500.0f;
};

class EnemyDashOnFloorState : public StateNode
{
public:
    EnemyDashOnFloorState();
    ~EnemyDashOnFloorState() = default;

    void on_enter() override;
    void on_update(float delta) override;
private:
    const float SPEED_DATH = 1000.0f;
private:
    Timer timer;
};

class EnemyDeadState : public StateNode
{
public:
    EnemyDeadState() = default;
    ~EnemyDeadState() = default;

    void on_enter() override;
};

class EnemyFallState : public StateNode
{
public:
    EnemyFallState() = default;
    ~EnemyFallState() = default;

    void on_enter() override;
    void on_update(float delta) override;
};

class EnemyIdleState : public StateNode
{
public:
    EnemyIdleState() = default;
    ~EnemyIdleState() = default;

    void on_enter() override;
    void on_update(float delta) override;
    void on_exit() override;
private:
    Timer timer;
};

class EnemyJumpState : public StateNode
{
public:
    EnemyJumpState() = default;
    ~EnemyJumpState() = default;

    void on_enter() override;
    void on_update(float delta) override;
private:
    const float SPEED_JUMP = 1000.0f;
};

class EnemyRunState : public StateNode
{
public:
    EnemyRunState() = default;
    ~EnemyRunState() = default;

    void on_enter() override;
    void on_update(float delta) override;
    void on_exit() override;
private:
    const float MIN_DIS = 350.0f;
    const float SPEED_RUN = 500.0f;
};

class EnemySquatState : public StateNode
{
public:
    EnemySquatState();
    ~EnemySquatState() = default;

    void on_enter() override;
    void on_update(float delta) override;
private:
    Timer timer;
};

class EnemyThrowBarbState : public StateNode
{
public:
    EnemyThrowBarbState();
    ~EnemyThrowBarbState() = default;

    void on_enter() override;
    void on_update(float delta) override;
private:
    Timer timer;
};

class EnemyThrowSilkState : public StateNode
{
public:
    EnemyThrowSilkState();
    ~EnemyThrowSilkState() = default;

    void on_enter() override;
    void on_update(float delta) override;
public:
    Timer timer;
};

class EnemyThrowSwordState : public StateNode
{
public:
    EnemyThrowSwordState();
    ~EnemyThrowSwordState() = default;

    void on_enter() override;
    void on_update(float delta) override;
private:
    Timer timer_throw;
    Timer timer_switch;
};

2 源文件实现

2.1 瞄准状态

Pasted image 20260313112301.png
瞄准状态是空中冲刺的前置状态,黄蜂女会悬停在空中等待0.5秒,然后跳转到冲刺状态冲向玩家。
所以需要使用enemy->set_gravity_enabled(false);来设置敌人的重力。

EnemyAimState::EnemyAimState()
{
    timer.set_one_shot(true);
    timer.set_wait_time(0.5f);
    timer.set_callback([&](){
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        enemy->set_gravity_enabled(true);
        enemy->switch_state("dash_in_air");
    });
}

void EnemyAimState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("aim");

    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    enemy->set_gravity_enabled(false);
    enemy->set_velocity({0, 0});
    timer.restart();
}

void EnemyAimState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    timer.on_update(delta);
    if(enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    }
}

2.2 空中冲刺状态

在空中冲刺状态开始时,首先应该计算得到敌人到玩家运动方向的向量。
为了让黄蜂女的运动轨迹为一条直线,我们对重力进行了控制。
在更新方法中,除去死亡的检查,还有就是检查是否落到几面转化为idle状态。

void EnemyDashInAirState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("dash_in_air");

    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    const Character *player = CharacterManager::instance()->get_player();
    Vector2 pos_target = {player->get_position().x, player->get_gloor_y()};
    enemy->set_velocity((pos_target - enemy->get_position()).normalized() * SPEED_DASH);
    enemy->set_dashing_in_air(true);
    enemy->set_gravity_enabled(false);
    enemy->on_dash();

    play_audio(_T("enemy_dash"), false);
}


void EnemyDashInAirState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();

    if (enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    } else if (enemy->is_on_floor()) {
        enemy->switch_state("idle");
    }
}

void EnemyDashInAirState::on_exit()
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();

    enemy->set_dashing_in_air(false);
    enemy->set_gravity_enabled(true);
}

2.3 地面冲刺状态

地面冲刺和空中冲刺类似,将当前角色的朝向作为冲刺方向。
不过我们使用定时器来控制它的结束,而不是像空中冲刺那样通过检测是否接触地面。

EnemyDashOnFloorState::EnemyDashOnFloorState()
{
    timer.set_one_shot(true);
    timer.set_wait_time(0.5f);
    timer.set_callback([&](){
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        enemy->set_dashing_on_floor(false);
    });
}

void EnemyDashOnFloorState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("dash_on_floor");

    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    enemy->set_velocity({enemy->get_facing_left() ? -SPEED_DATH : SPEED_DATH, 0});
    enemy->set_dashing_on_floor(true);
    enemy->on_dash();
    timer.restart();

    play_audio(_T("enemy_dash"), false);
}


void EnemyDashOnFloorState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();

    timer.on_update(delta);

    if (enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    } else if (!enemy->get_dashing_on_floor()) {
        enemy->switch_state("idle");
    }
}

2.4 死亡状态

空洞骑士中并没有黄蜂女的死亡动画素材,所以我们直接跳出挑战成功的弹窗

void EnemyDeadState::on_enter()
{
    MessageBox(GetHWnd(), _T("很好,这样就行."), _T("挑战成功"), MB_OK);
    exit(0);
}

2.5 坠落状态

Pasted image 20260313120444.png
坠落状态的更新方法中,只需要检查角色是否已经落到地板上,如果是,则跳转到闲置状态即可。

void EnemyFallState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("fall");
}

void EnemyFallState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();

    if (enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    } else if (enemy->is_on_floor()) {
        enemy->switch_state("idle");
    }
}

2.6 闲置状态

Pasted image 20260313130831.png
在游戏中,闲置状态是一个十分关键的状态。他几乎可以是所有状态跳转的出口。在游戏中,我们也需要黄蜂女能够在闲置状态下,跳转到其他状态。
我们需要在定时器结束时,根据黄蜂女当前的剩余生命值做出不同概率的随机动作。
当角色生命值大于5的情况下

  • 有25%的概率进入到跳跃状态
  • 25%的概率进入到奔跑追击玩家的状态
  • 30%的概率进入到下蹲的地面冲刺前置状态
  • 10%的概率分别进入到扔丝线和扔剑等范围远程攻击的状态
    而当角色生命值小于5的情况下

  • 会有更大的概率选择范围伤害和远程伤害

  • 还有20%概率出现召唤刺球攻击的新招式
    EnemyIdleState::EnemyIdleState()
    {
        timer.set_one_shot(true);
        timer.set_callback([&](){
            Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    
            int rand_num = range_random(0, 100);
            if (enemy->get_hp() > 5) {
                if (rand_num <= 25) {
                    if (!enemy->is_on_floor()) {
                        enemy->switch_state("fall");
                    } else {
                        enemy->switch_state("jump");    //25%
                    }
                } else if (rand_num <= 50) {
                    if (!enemy->is_on_floor()) {
                        enemy->switch_state("fall");
                    } else {
                        enemy->switch_state("run");     //25%
                    }
                } else if (rand_num <= 80) {
                    enemy->switch_state("squat");       //30%
                } else if (rand_num <= 90) {    
                    enemy->switch_state("throw_silk");  //10%
                } else {
                    enemy->switch_state("throw_sword");  //10%
                }
            } else {
                if (rand_num <= 25) {
                    if (!enemy->is_on_floor()) {
                        enemy->switch_state("fall");
                    } else {
                        enemy->switch_state("jump");    //25%
                    }
                } else if (rand_num <= 60) {
                    enemy->switch_state("throw_sword");  //35%
                } else if (rand_num <= 70) {
                    enemy->switch_state("throw_silk");  //10%
                } else if (rand_num <= 90) {
                    enemy->switch_state("throw_barb");       //20%
                } else {
                    enemy->switch_state("squat");  //10%
                }
            }
        });
    }
    

    闲置状态的进入方法也很关键,除去设置角色速度为0外,还需要设置状态退出定时器的值为随机的值。
    当角色生命值小于5时,闲置状态持续时间的上界会更低,在宏观上,黄蜂女行动间喘息的时间会更短。
    在退出状态时,我们还需要根据玩家当前的位置,调整黄蜂女的角色朝向,来确保后续行动的正确性。
    void EnemyIdleState::on_enter()
    {
        CharacterManager::instance()->get_enemy()->set_animation("idle");
    
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        enemy->set_velocity({0, 0});
    
        float wait_time = 0;
        if (enemy->get_hp() > 5) {
            wait_time = range_random(0, 2) * 0.5f;  // 0.0s ~ 0.5s
        } else {
            wait_time = range_random(0, 1) * 0.5f;  // 0.0s ~ 0.25s
        }
    
        timer.set_wait_time(wait_time);
        timer.restart();
    }
    
    void EnemyIdleState::on_update(float delta)
    {
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        timer.on_update(delta);
    
        if (enemy->get_hp() <= 0) {
            enemy->switch_state("dead");
        } else if (enemy->get_velocity().y > 0){
            enemy->switch_state("fall");
        }
    }
    
    void EnemyIdleState::on_exit()
    {
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        enemy->set_facing_left(enemy->get_position().x > CharacterManager::instance()->get_player()->get_position().x);
    }
    

2.7 跳跃状态

Pasted image 20260313132844.png
在跳跃状态时,也是相似的道理
进入时,设置速度
更新时,根据角色生命值和随机概率切换到不同的状态。

void EnemyJumpState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("jump");

    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    enemy->set_velocity({0, -SPEED_JUMP});
}

void EnemyJumpState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();

    if (enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    } else if (enemy->get_velocity().y > 0) {
        int rand_num = range_random(0, 100);
        if (enemy->get_hp() > 5) {
            if (rand_num <= 50) {
                enemy->switch_state("aim");  //50%
            }  else if (rand_num <= 80) {    
                enemy->switch_state("fall");  //30%
            } else {
                enemy->switch_state("throw_silk");  //20%
            }
        } else {
            if (rand_num <= 50) {
                enemy->switch_state("throw_silk");  //50%
            }  else if (rand_num <= 80) {    
                enemy->switch_state("fall");  //30%
            } else {
                enemy->switch_state("aim");  //20%
            }
        }
    }
}

2.8 奔跑状态

Pasted image 20260313133520.png
奔跑状态退出的条件便不再是定时器了
而是和玩家的水平距离小于一定值时,在跳转到其他状态。

void EnemyRunState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("jump");

    play_audio(_T("enemy_run"), true);
}

void EnemyRunState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    const Vector2 &pos_enemy = enemy->get_position();
    const Vector2 &pos_player = CharacterManager::instance()->get_player()->get_position();
    enemy->set_velocity({pos_enemy.x < pos_player.x ? SPEED_RUN : -SPEED_RUN, 0});

    if (enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    } else if (abs(pos_enemy.x - pos_player.x) <= MIN_DIS ) {
        int rand_num = range_random(0, 100);
        if (enemy->get_hp() > 5) {
            if (rand_num <= 75) {
                enemy->switch_state("squat");  //75%
            } else {
                enemy->switch_state("throw_silk");  //25%
            }
        } else {
            if (rand_num <= 75) {
                enemy->switch_state("throw_silk");  //75%
            } else {
                enemy->switch_state("squat");  //25%
            }
        }
        stop_audio(_T("enemy_run"));
    }
}

void EnemyRunState::on_exit()
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    enemy->set_velocity({0, 0});
}

2.9 下蹲状态

Pasted image 20260313134142.png
下蹲状态作为地面冲刺的前置状态,也是需要定时器控制退出时机。代码与EnemyAimState相似.
而在状态开始时,我们需要根据玩家位置设置黄风女的朝向

EnemySquatState::EnemySquatState()
{
    timer.set_one_shot(true);
    timer.set_wait_time(0.5f);
    timer.set_callback([&](){
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        enemy->switch_state("dash_on_floor");
    });
}

void EnemySquatState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("squat");

    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    enemy->set_facing_left(enemy->get_position().x > CharacterManager::instance()->get_player()->get_position().x);
    timer.restart();
}

void EnemySquatState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    timer.on_update(delta);
    if(enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    }
}

2.10 召唤刺球状态

Pasted image 20260313134704.png
在召唤此球的攻击状态中
我们只需要在动画播放结束前后,也就是状态进入后的0.8秒时,调用先前封装好的扔刺球方法就可以了。之后把状态跳转到闲置状态。

EnemyThrowBarbState::EnemyThrowBarbState()
{
    timer.set_one_shot(true);
    timer.set_wait_time(0.8f);
    timer.set_callback([&](){
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        enemy->throw_barbs();
        enemy->switch_state("idle");
    });
}

void EnemyThrowBarbState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("throw_barb");

    timer.restart();

    play_audio(_T("enemy_throw_barbs"), false);
}

void EnemyThrowBarbState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    timer.on_update(delta);
    if(enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    }
}

2.11 缠绕丝线状态

Pasted image 20260313135133.png
由于黄蜂女既可以在空中也可以在地面发动缠绕丝线技能。所以和之前的空中瞄准状态相似,我们要在进入状态时禁用重力,退出时重新开启。

EnemyThrowSilkState::EnemyThrowSilkState()
{
    timer.set_one_shot(true);
    timer.set_wait_time(0.9f);
    timer.set_callback([&](){
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        enemy->set_gravity_enabled(true);
        enemy->set_throwing_silk(false);
        if (!enemy->is_on_floor() && enemy->get_hp() > 5 && range_random(0, 100) <= 25) {
            enemy->switch_state("aim");
        } else if (!enemy->is_on_floor()) {
            enemy->switch_state("fall");
        } else {
            enemy->switch_state("idle");
        }
    });
}

void EnemyThrowSilkState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("throw_silk");

    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    enemy->set_gravity_enabled(false);
    enemy->set_throwing_silk(true);
    enemy->set_velocity({0, 0});
    enemy->on_throw_silk();
    timer.restart();
}

void EnemyThrowSilkState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    timer.on_update(delta);
    if(enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    }
}

2.12 扔剑状态

Pasted image 20260313135722.png
扔剑的状态也是一个由定时器控制退出的状态,但是有一点小特殊。
由于“扔”是一个持续性的动作,我们需要让飞剑道具生成的时机恰到好处,也就是说飞剑应该出现在动画播放的过程中,而不是动画播放开始时或者动画播放结束时。
所以我们使用了一个额外的定时器,单独控制飞剑道具的生成。
而在状态切换的定时器中,依然是需要根据角色当前的生命值,有选择性地跳转到其他状态。

EnemyThrowSwordState::EnemyThrowSwordState()
{
    timer_throw.set_one_shot(true);
    timer_throw.set_wait_time(0.65f);
    timer_throw.set_callback([&](){
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        enemy->throw_sword();
        play_audio(_T("enemy_throw_sword"), false);
    });

    timer_switch.set_one_shot(true);
    timer_switch.set_wait_time(1.0f);
    timer_switch.set_callback([&](){
        Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
        int rand_num = range_random(0, 100);
        if (enemy->get_hp() > 5) {
            if (rand_num <= 50) {
                enemy->switch_state("squat");  //50%
            } else if (rand_num <= 50) {
                enemy->switch_state("jump");  //30%
            } else {
                enemy->switch_state("idle");  //20%
            }
        } else {
            if (rand_num <= 50) {
                enemy->switch_state("jump");  //50%
            } else if (rand_num <= 50) {
                enemy->switch_state("throw_silk");  //30%
            } else {
                enemy->switch_state("idle");  //20%
            }
        }
    });
}

在扔飞剑状态的开始和更新方法中,需要对两个定时器都进行处理
void EnemyThrowSwordState::on_enter()
{
    CharacterManager::instance()->get_enemy()->set_animation("throw_sword");

    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    timer_throw.restart();
    timer_switch.restart();
}

void EnemyThrowSwordState::on_update(float delta)
{
    Enemy* enemy = (Enemy*)CharacterManager::instance()->get_enemy();
    timer_throw.on_update(delta);
    timer_switch.on_update(delta);
    if(enemy->get_hp() <= 0) {
        enemy->switch_state("dead");
    }
}

3 集成到enemy和场景中

在enemy.cpp中引入对应的头文件

#include "enemy_state_nodes.h"

最后来到角色初始化的最后,在先前预留的状态机初始化代码的位置处,填充状态节点实例的注册,并把入口节点设置为闲置状态。
state_machine.register_state("aim", new EnemyAimState());
state_machine.register_state("dash_in_air", new EnemyDashInAirState());
state_machine.register_state("dash_on_floor", new EnemyDashOnFloorState());
state_machine.register_state("dead", new EnemyDeadState());
state_machine.register_state("fall", new EnemyFallState());
state_machine.register_state("idle", new EnemyIdleState());
state_machine.register_state("jump", new EnemyJumpState());
state_machine.register_state("run", new EnemyRunState());
state_machine.register_state("squat", new EnemySquatState());
state_machine.register_state("throw_barb", new EnemyThrowBarbState());
state_machine.register_state("throw_silk", new EnemyThrowSilkState());
state_machine.register_state("throw_sword", new EnemyThrowSwordState());

state_machine.set_entry("idle");

之后来到character_manager.cpp中,引入enemy头文件。在对应方法中添加敌人角色的构造和各个阶段方法的调用

void CharacterManager::on_update(float delta)
{
    enemy->on_update(delta);
    player->on_update(delta);
}

void CharacterManager::on_render()
{
    enemy->on_render();
    BulletTimeManager::instance()->post_process();
    player->on_render();
}

CharacterManager::CharacterManager()
{
    enemy = new Enemy();
    player = new Player();
}

4 实时显示玩家生命值

在main.cpp中实现draw_remain_hp,根据玩家生命值,使用循环,把对应数量的心型绘制在窗口中即可

static void draw_remain_hp()
{
    static IMAGE *img_ui_heart = ResourcesManager::instance()->find_image("ui_heart");
    Rect rect_dst = {0, 10, img_ui_heart->getwidth(), img_ui_heart->getheight()};
    for (int i = 0; i < CharacterManager::instance()->get_player()->get_hp(); i++) {
        rect_dst.x = 10 * i * 40;
        putimage_ex(img_ui_heart, &rect_dst);
    }
}

最后来到主循环的渲染部分,添加生命值绘图数的调用

draw_background();
CharacterManager::instance()->on_render();
CollisionManager::instance()->on_debug_render();
draw_remain_hp();

评论

如果你已登录 GitHub,就可以直接在这里评论。