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

我们首先纵览一下黄蜂女的状态节点。
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 瞄准状态

瞄准状态是空中冲刺的前置状态,黄蜂女会悬停在空中等待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 坠落状态

坠落状态的更新方法中,只需要检查角色是否已经落到地板上,如果是,则跳转到闲置状态即可。
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 闲置状态

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

在跳跃状态时,也是相似的道理
进入时,设置速度
更新时,根据角色生命值和随机概率切换到不同的状态。
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 奔跑状态

奔跑状态退出的条件便不再是定时器了
而是和玩家的水平距离小于一定值时,在跳转到其他状态。
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 下蹲状态

下蹲状态作为地面冲刺的前置状态,也是需要定时器控制退出时机。代码与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 召唤刺球状态

在召唤此球的攻击状态中
我们只需要在动画播放结束前后,也就是状态进入后的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 缠绕丝线状态

由于黄蜂女既可以在空中也可以在地面发动缠绕丝线技能。所以和之前的空中瞄准状态相似,我们要在进入状态时禁用重力,退出时重新开启。
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 扔剑状态

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