跳转至

07-黄蜂女角色类实现与技能道具设计

从本节内容开始,我们就要开始着手实现由AI控制的Boss角色黄蜂女。
考虑到用较低的实现难度,我们对黄蜂女的招式进行了一些修改,在现有的设计中,黄蜂女召唤出的刺球会在空中上下符动一段时间,随后冲向玩家造成伤害。玩家可以在刺球符动或飞行的过程中使用攻击击碎它。
此外,黄蜂女扔出的剑也不会再被收回,而是持续向前飞行。

我们先来实现这些技能道具对象,之后再到对应的Enemy类中完成它们的生成逻辑。

1 Sword类

1.1 类声明

首先创建sword.h头文件

#include "vector2.h"
#include "animation.h"
#include "collision_box.h"

class Sword
{
public: 
    Sword(const Vector2& position, bool move_left);
    ~Sword();

    void on_update(float delta);
    void on_render();

    bool check_valid() const
    {
        return is_valid;
    }

private:
    const float SPEED_MOVE = 1250.0f;

private:
    Vector2 position;
    Vector2 velocity;
    Animation animation;
    bool is_valid = true;
    CollisionBox *collision_box = nullptr;
};

1.2 类实现

之后创建对应的sword.cpp文件

Sword::Sword(const Vector2& position, bool move_left)
{
    animation.set_interval(0.1f);
    animation.set_loop(true);
    animation.set_anchor_mode(Animation::AnchorMode::Centered);
    animation.add_frame(ResourcesManager::instance()->find_atlas(
        move_left ? "sword_left" : "sword_right"));

    collision_box = CollisionManager::instance()->create_collision_box();
    collision_box->set_layer_src(CollisionLayer::None);
    collision_box->set_layer_dst(CollisionLayer::Player);
    collision_box->set_size(195, 10);

    this->position = position;
    this->velocity = { move_left ? -SPEED_MOVE : SPEED_MOVE, 0};
}

Sword::~Sword()
{
    CollisionManager::instance()->destroy_collision_box(collision_box);
}

void Sword::on_update(float delta)
{
    position += velocity * delta;
    animation.set_position(position);
    collision_box->set_position(position);

    animation.on_update(delta);

    if(position.x <= -200 || position.x >= getwidth() + 200)
        is_valid = false;
}

void Sword::on_render()
{
    animation.on_render();
}

2 Barb类

对于刺球对象,我们也是定义barb.h头文件
刺球拥有四种状态,默认的上下浮动状态,向玩家冲刺前的瞄准状态,冲刺中的状态和破碎的状态。
虽然我们可以使用状态机对它进行管理,但是考虑到刺球的逻辑并不复杂,必要性不大。因此我们提供一个不适用状态机的实现思路。

2.1 类声明

timer_idle和timer_aim两个定时器分别控制闲置状态和瞄准状态的持续时间。
diff_period是一个随机值,我们使用它来保存一个随机数,控制浮动的运动周期,让场景中同时生成的刺球可以七上八下,画面效果更加自然。
total_delta_time变量用来记录刺球生成依赖度过的时间,我们对其取三角函数实现周期运动。
对于刺球的位置,我们使用两个变量来进行控制,一个是基础位置,一个是当前位置。因为刺球在浮动过程和瞄准的震动过程中,都会在原始位置上进行一定的范围偏移。所以我们需要同时记录它偏移前的原点位置,和当前帧的运动位置。
on_break方法封装了刺球破碎时的处理。无论是被玩家看重还是砸落在地板上。都需要执行相同的音效播放等逻辑。

class Barb
{
public:
    Barb();
    ~Barb();

    void on_update(float delta);
    void on_render();

    void set_position(const Vector2& position)
    {
        this->base_position = position;
        this->current_position = position;
    }

    bool check_valid() const
    {
        return is_valid;
    }

private:
    enum class Stage
    {
        Idle,
        Aim,
        Dash,
        Break
    };

private:
    const float SPEED_DASH = 1500.0f;

private:
    Timer timer_idle;
    Timer timer_aim;
    int diff_period = 0;
    bool is_valid = true;
    float total_delta_time = 0;

    Vector2 velocity;
    Vector2 base_position;
    Vector2 current_position;

    Animation animation_loose;
    Animation animation_break;
    Animation *current_animation = nullptr;

    Stage stage = Stage::Idle;
    CollisionBox *collison_box = nullptr;

private:
    void on_break();
};

2.2 类实现

在创建的barb源代码中

Barb::Barb()
{
    diff_period = range_random(0, 6);

    animation_loose.set_interval(0.15f);
    animation_loose.set_loop(true);
    animation_loose.set_anchor_mode(Animation::AnchorMode::Centered);
    animation_loose.add_frame(ResourcesManager::instance()->find_atlas("barb_loose"));

    animation_break.set_interval(0.1f);
    animation_break.set_loop(false);
    animation_break.set_anchor_mode(Animation::AnchorMode::Centered);
    animation_break.add_frame(ResourcesManager::instance()->find_atlas("barb_break"));
    animation_break.set_on_finished([&]() { is_valid = false; });

    collision_box = CollisionManager::instance()->create_collision_box();
    collision_box->set_layer_src(CollisionLayer::Enemy);
    collision_box->set_layer_dst(CollisionLayer::Player);
    collision_box->set_size({ 20, 20 });
    collision_box->set_on_collide([&]() { on_break(); });

    timer_idle.set_wait_time((float)range_random(3, 10));
    timer_idle.set_one_shot(true);
    timer_idle.set_callback([&]()
        {
            if (stage == Stage::Idle) {
                stage = Stage::Aim;
                base_position = current_position;
            }
        });

    timer_aim.set_wait_time(0.75f);
    timer_aim.set_one_shot(true);
    timer_aim.set_callback([&]()
        {
            if (stage==Stage::Aim) {
                stage = Stage::Dash;
                const Vector2 &pos_player = CharacterManager::instance()->get_player()->get_position();
                velocity = (pos_player - current_position).normalized() * SPEED_DASH;
            }
        });
}

对于析构函数,我们只需要销毁碰撞箱对象就可以了

Barb::~Barb()
{
    CollisionManager::instance()->destroy_collision_box(collision_box);
}

在on_update方法中,我们将逻辑划分为三部分。更新对应阶段的定时器,更新移动逻辑和更新动画。
void Barb::on_update(float delta)
{
    //更新定时器逻辑
    if (stage == Stage::Idle) {
        timer_idle.on_update(delta);
    } 
    if (stage == Stage::Aim) {
        timer_aim.on_update(delta);
    }

    //更新移动逻辑
    total_delta_time += delta;
    switch(stage)
    {
    case Stage::Idle:
        current_position.y = base_position.y + sin(total_delta_time * 2 + diff_period) * 30;
        break;
    case Stage::Aim:
        current_position.x = base_position.x + range_random(-10, 10);
        break;
    case Stage::Dash:
        current_position += velocity * delta;
        if (current_position.y >= CharacterManager::instance()->get_player()->get_gloor_y()) {
            on_break();
        }
        if (current_position.y <= 0) {
            is_valid = false;
        }
        break;
    }
    collision_box->set_position(current_position);

    //更新动画逻辑
    current_animation = (stage == Stage::Break ? &animation_break : &animation_loose);
    current_animation->set_position(current_position);
    current_animation->on_update(delta);
}

剩下的方法非常简单

void Barb::on_render()
{
    current_animation->on_render();
}

void Barb::on_break()
{
    if (stage==Stage::Break) {
        return;
    }

    stage = Stage::Break;
    collision_box->set_enable(false);
    play_audio(_T("barb_break"), false);
}

3 Enemy类

3.1 类声明

先创建enemy.h,声明enemy类的成员变量

class Enemy : public Character
{
private:
    bool is_throwing_silk = false;
    bool is_dashing_in_air = false;
    bool is_dashing_on_floor = false;

    Animation animation_silk;
    AnimationGroup animation_dash_in_air_vfx;
    AnimationGroup animation_dash_on_floor_vfx;
    Animation *current_dash_animation = nullptr;

    std::vector<Barb *> barb_list;
    std::vector<Sword *> sword_list;
    CollisionBox* collision_box_silk = nullptr;
};

之后我们声明一些方法,包括

  • 继承自Character类的方法
  • 方便外部设置和获取Enemy状态的方法
  • 投掷barb和sword的方法
    public:
        Enemy();
        ~Enemy();
    
        void on_update(float delta) override;
        void on_render() override;
    
        void on_hurt() override;
    
        void set_facing_left(bool flag)
        {
            is_facing_left = flag;
        }
    
        bool get_facing_left() const
        {
            return is_facing_left;
        }
    
        void set_dashing_in_air(bool flag){
            is_dashing_in_air = flag;
        }
    
        bool get_dashing_in_air() const
        {
            return is_dashing_in_air;
        }
    
        void set_dashing_on_floor(bool flag){
            is_dashing_on_floor = flag;
        }
    
        bool get_dashing_on_floor() const
        {
            return is_dashing_on_floor;
        }
    
        void set_throwing_silk(bool flag){
            is_throwing_silk = flag;
        }
    
        bool get_throwing_silk() const
        {
            return is_throwing_silk;
        }
    
        void throw_barbs();
        void throw_sword();
    
        void on_dash();
        void on_throw_silk();
    

3.2 初始化方法

Enemy::Enemy()
{
    is_facing_left = true;
    position = {1050, 200};
    logic_height = 150;

    hit_box->set_size({50, 80});
    hurt_box->set_size({100,180});

    hit_box->set_layer_src(CollisionLayer::None);
    hit_box->set_layer_dst(CollisionLayer::Player);

    hurt_box->set_layer_src(CollisionLayer::Enemy);
    hurt_box->set_layer_dst(CollisionLayer::None);

    hurt_box->set_on_collide([&]()
        {
            decrease_hp();
        });

    collision_box_silk = CollisionManager::instance()->create_collision_box();
    collision_box_silk->set_layer_src(CollisionLayer::None);
    collision_box_silk->set_layer_dst(CollisionLayer::Player);
    collision_box_silk->set_size({ 225, 225 });
    collision_box_silk->set_enable(false);

    {
        {
            AnimationGroup& animation_aim = animation_pool["aim"];

            Animation& animation_aim_left = animation_aim.left;
            animation_aim_left.set_interval(0.05f);
            animation_aim_left.set_loop(false);
            animation_aim_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_aim_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_aim_left"));

            Animation& animation_aim_right = animation_aim.right;
            animation_aim_right.set_interval(0.05f);
            animation_aim_right.set_loop(false);
            animation_aim_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_aim_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_aim_right"));
        }
        {
            AnimationGroup& animation_dash_in_air = animation_pool["dash_in_air"];

            Animation& animation_dash_in_air_left = animation_dash_in_air.left;
            animation_dash_in_air_left.set_interval(0.05f);
            animation_dash_in_air_left.set_loop(true);
            animation_dash_in_air_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_dash_in_air_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_dash_in_air_left"));

            Animation& animation_dash_in_air_right = animation_dash_in_air.right;
            animation_dash_in_air_right.set_interval(0.05f);
            animation_dash_in_air_right.set_loop(true);
            animation_dash_in_air_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_dash_in_air_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_dash_in_air_right"));
        }
        {
            AnimationGroup& animation_dash_on_floor = animation_pool["dash_on_floor"];

            Animation& animation_dash_on_floor_left = animation_dash_on_floor.left;
            animation_dash_on_floor_left.set_interval(0.05f);
            animation_dash_on_floor_left.set_loop(true);
            animation_dash_on_floor_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_dash_on_floor_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_dash_on_floor_left"));

            Animation& animation_dash_on_floor_right = animation_dash_on_floor.right;
            animation_dash_on_floor_right.set_interval(0.05f);
            animation_dash_on_floor_right.set_loop(true);
            animation_dash_on_floor_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_dash_on_floor_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_dash_on_floor_right"));
        }
        {
            AnimationGroup& animation_fall = animation_pool["fall"];

            Animation& animation_fall_left = animation_fall.left;
            animation_fall_left.set_interval(0.1f);
            animation_fall_left.set_loop(true);
            animation_fall_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_fall_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_fall_left"));

            Animation& animation_fall_right = animation_fall.right;
            animation_fall_right.set_interval(0.1f);
            animation_fall_right.set_loop(true);
            animation_fall_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_fall_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_fall_right"));
        }
        {
            AnimationGroup& animation_idle = animation_pool["idle"];

            Animation& animation_idle_left = animation_idle.left;
            animation_idle_left.set_interval(0.1f);
            animation_idle_left.set_loop(true);
            animation_idle_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_idle_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_idle_left"));

            Animation& animation_idle_right = animation_idle.right;
            animation_idle_right.set_interval(0.1f);
            animation_idle_right.set_loop(true);
            animation_idle_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_idle_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_idle_right"));
        }
        {
            AnimationGroup& animation_jump = animation_pool["jump"];

            Animation& animation_jump_left = animation_jump.left;
            animation_jump_left.set_interval(0.1f);
            animation_jump_left.set_loop(false);
            animation_jump_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_jump_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_jump_left"));

            Animation& animation_jump_right = animation_jump.right;
            animation_jump_right.set_interval(0.1f);
            animation_jump_right.set_loop(false);
            animation_jump_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_jump_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_jump_right"));
        }
        {
            AnimationGroup& animation_run = animation_pool["run"];

            Animation& animation_run_left = animation_run.left;
            animation_run_left.set_interval(0.05f);
            animation_run_left.set_loop(true);
            animation_run_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_run_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_run_left"));

            Animation& animation_run_right = animation_run.right;
            animation_run_right.set_interval(0.05f);
            animation_run_right.set_loop(true);
            animation_run_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_run_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_run_right"));
        }
        {
            AnimationGroup& animation_squat = animation_pool["squat"];

            Animation& animation_squat_left = animation_squat.left;
            animation_squat_left.set_interval(0.05f);
            animation_squat_left.set_loop(false);
            animation_squat_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_squat_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_squat_left"));

            Animation& animation_squat_right = animation_squat.right;
            animation_squat_right.set_interval(0.05f);
            animation_squat_right.set_loop(false);
            animation_squat_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_squat_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_squat_right"));
        }
        {
            AnimationGroup& animation_throw_barb = animation_pool["throw_barb"];

            Animation& animation_throw_barb_left = animation_throw_barb.left;
            animation_throw_barb_left.set_interval(0.1f);
            animation_throw_barb_left.set_loop(false);
            animation_throw_barb_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_throw_barb_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_throw_barb_left"));

            Animation& animation_throw_barb_right = animation_throw_barb.right;
            animation_throw_barb_right.set_interval(0.1f);
            animation_throw_barb_right.set_loop(false);
            animation_throw_barb_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_throw_barb_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_throw_barb_right"));
        }
        {
            AnimationGroup& animation_throw_silk = animation_pool["throw_silk"];

            Animation& animation_throw_silk_left = animation_throw_silk.left;
            animation_throw_silk_left.set_interval(0.1f);
            animation_throw_silk_left.set_loop(true);
            animation_throw_silk_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_throw_silk_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_throw_silk_left"));

            Animation& animation_throw_silk_right = animation_throw_silk.right;
            animation_throw_silk_right.set_interval(0.1f);
            animation_throw_silk_right.set_loop(true);
            animation_throw_silk_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_throw_silk_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_throw_silk_right"));
        }
        {
            AnimationGroup& animation_throw_sword = animation_pool["throw_sword"];

            Animation& animation_throw_sword_left = animation_throw_sword.left;
            animation_throw_sword_left.set_interval(0.05f);
            animation_throw_sword_left.set_loop(false);
            animation_throw_sword_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_throw_sword_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_throw_sword_left"));

            Animation& animation_throw_sword_right = animation_throw_sword.right;
            animation_throw_sword_right.set_interval(0.05f);
            animation_throw_sword_right.set_loop(false);
            animation_throw_sword_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
            animation_throw_sword_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_throw_sword_right"));
        }
    }

    {
        animation_silk.set_interval(0.1f);
        animation_silk.set_loop(false);
        animation_silk.set_anchor_mode(Animation::AnchorMode::Centered);
        animation_silk.add_frame(ResourcesManager::instance()->find_atlas("silk"));

        Animation& animation_dash_in_air_left = animation_dash_in_air_vfx.left;
        animation_dash_in_air_left.set_interval(0.1f);
        animation_dash_in_air_left.set_loop(false);
        animation_dash_in_air_left.set_anchor_mode(Animation::AnchorMode::Centered);
        animation_dash_in_air_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_vfx_dash_in_air_left"));

        Animation& animation_dash_in_air_right = animation_dash_in_air_vfx.right;
        animation_dash_in_air_right.set_interval(0.1f);
        animation_dash_in_air_right.set_loop(false);
        animation_dash_in_air_right.set_anchor_mode(Animation::AnchorMode::Centered);
        animation_dash_in_air_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_vfx_dash_in_air_right"));

        Animation& animation_dash_on_floor_left = animation_dash_on_floor_vfx.left;
        animation_dash_on_floor_left.set_interval(0.1f);
        animation_dash_on_floor_left.set_loop(false);
        animation_dash_on_floor_left.set_anchor_mode(Animation::AnchorMode::BottomCentered);
        animation_dash_on_floor_left.add_frame(ResourcesManager::instance()->find_atlas("enemy_vfx_dash_on_floor_left"));

        Animation& animation_dash_on_floor_right = animation_dash_on_floor_vfx.right;
        animation_dash_on_floor_right.set_interval(0.1f);
        animation_dash_on_floor_right.set_loop(false);
        animation_dash_on_floor_right.set_anchor_mode(Animation::AnchorMode::BottomCentered);
        animation_dash_on_floor_right.add_frame(ResourcesManager::instance()->find_atlas("enemy_vfx_dash_on_floor_right"));
    }
    {
        // TODO: 状态机初始化
    }
}

3.3 析构函数

在析构函数中,由于角色自身的攻击和受击碰撞箱都会在Character基类中进行销毁,那么我们只需要关注子类拓展的丝线攻击碰撞箱对象,让它在析构函数中销毁即可

Enemy::~Enemy()
{
    CollisionManager::instance()->destroy_collision_box(collision_box_silk);
}

3.4 更新方法

void Enemy::on_update(float delta)
{
    if (velocity.x>= 0.0001f) {
        is_facing_left = (velocity.x < 0);
    }

    Character::on_update(delta);

    hit_box->set_position(get_logic_center());

    if (is_throwing_silk) {
        collision_box_silk->set_position(get_logic_center());
        animation_silk.set_position(get_logic_center());
        animation_silk.on_update(delta);
    }

    if (is_dashing_in_air || is_dashing_on_floor) {
        current_dash_animation->set_position(is_dashing_in_air ? get_logic_center() : position);
        current_dash_animation->on_update(delta);
    }

    for (Barb* barb : barb_list) {
        barb->on_update(delta);
    }

    for(Sword* sword: sword_list) {
        sword->on_update(delta);
    }

    barb_list.erase(std::remove_if(barb_list.begin(), barb_list.end(), 
        [](Barb *barb)
         {
            bool can_remove = !barb->check_valid();
            if (can_remove)
                delete barb;
            return can_remove; 
        }),
        barb_list.end());
    sword_list.erase(std::remove_if(sword_list.begin(), sword_list.end(), 
        [](Sword *sword)
         {
            bool can_remove = !sword->check_valid();
            if (can_remove)
                delete sword;
            return can_remove; 
        }),
        sword_list.end());
}

3.5 渲染方法

void Enemy::on_render()
{
    for (Barb* barb : barb_list) {
        barb->on_render();
    }

    for(Sword* sword: sword_list) {
        sword->on_render();
    }

    Character::on_render();

    if (is_throwing_silk) {
        animation_silk.on_render();
    }

    if (is_dashing_in_air || is_dashing_on_floor) {
        current_dash_animation->on_render();
    }
}

3.6 其他方法

受击方法中随机播放一种受击音效

void Enemy::on_hurt()
{
    switch(range_random(1, 3))
    {
    case 1:
        play_audio(_T("enemy_hurt_1"), false);
        break;
    case 2:
        play_audio(_T("enemy_hurt_2"), false);
        break;
    case 3:
        play_audio(_T("enemy_hurt_3"), false);
        break;
    }
}

为了让刺球的间隔尽可能均匀,我们先计算均匀网格的宽度,据此在每个网格内部利用随机数设置刺球的位置

void Enemy::throw_barbs()
{
    int num_new_barb = range_random(3, 6);
    if (barb_list.size() >= 10)
        num_new_barb = 1;
    int width_grid = getwidth() / num_new_barb;

    for (int i = 0; i < num_new_barb; i++) {
        Barb *new_barb = new Barb();
        int rand_x = range_random(width_grid * i, width_grid * (i + 1));
        int rand_y = range_random(250, 500);
        new_barb->set_position({(float)rand_x, (float)rand_y});
        barb_list.emplace_back(new_barb);
    }
}

而扔剑的逻辑就简单多了

void Enemy::throw_sword()
{
    Sword *new_sword = new Sword(get_logic_center(), is_facing_left);
    sword_list.emplace_back(new_sword);
}

冲刺方法中,主要是更新黄蜂女的特效动画

void Enemy::on_dash()
{
    if (is_dashing_in_air) {
        current_dash_animation = velocity.x < 0 ? &animation_dash_in_air_vfx.left : &animation_dash_in_air_vfx.right;
    } else {
        current_dash_animation = velocity.x < 0 ? &animation_dash_on_floor_vfx.left : &animation_dash_on_floor_vfx.right;
    }

    current_dash_animation->reset();
}

对于黄蜂女缠绕丝线的逻辑,我们只需要启用对应的阿碰撞箱并播放动画.碰撞箱的启用和关闭在对应的状态机中设置,所以我们这里只需要重置她的特效动画就可以了

void Enemy::on_throw_silk()
{
    animation_silk.reset();
}

评论

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