跳转至

13-子弹发射与玩家技能

1 Player类

上节我们已经实现了不同类型的玩家角色可以发射的抛射物,即所有的三种Bullet衍生类。
下一步,我们就要完成“枪”的逻辑了,也就是完成游戏角色发出子弹的功能。
我们首先在Player基类中,添加on_attack和on_attack_ex两个虚函数

virtual void on_attack() { }
virtual void on_attack_ex() { }

具体的玩家子类中,只需要重写这两个函数即可。

1.1 攻击cd

在我们的游戏中,角色的普通攻击是由键盘按键触发的。而且角色的普通攻击一般是存在冷却时间的。在冷却时间内按下攻击键也无法触发攻击。
所以我们定义下面的私有变量

int attack_cd = 500;            // 普通攻击冷却时间(ms)
bool can_attack = true;         // 是否可以释放普通攻击
Timer timer_attack_cd;          // 普通攻击冷却定时器

这样只要按键按下时就检查bool变量,如果为true,则反转can_attack,并重置定时器的时间。当定时器时间到达后,再恢复玩家可攻击的状态。

之后我们在Player的构造函数中为定时器初始化

timer_attack_cd.set_wait_time(attack_cd);
timer_attack_cd.set_one_shot(true);
timer_attack_cd.set_callback([&]()
    { 
        can_attack = true; 
    }
);

之后就是在Player的更新方法种添加
timer_attack_cd.on_update(delta);

1.2 按键处理

下一步就是在on_input中添加对应的按键逻辑了,当按键按下时,分别为P1和P2添加对F键和.键(句号和大于号的键,而不是小键盘的的点)的处理

// P1的按键中添加
case 'F':
    if(can_attack) {
        on_attack();
        can_attack = false;
        timer_attack_cd.restart();
    }
    break;

// P2的按键中添加
case VK_OEM_PERIOD:
    if(can_attack) {
        on_attack();
        can_attack = false;
        timer_attack_cd.restart();
    }
    break;

之后,我们添加mp和hp两个变量,用来记录角色的能量与生命值

protected:
    int mp = 0;
    int hp = 100;

为玩家P1和P2分别添加G键和/键的处理

case 'G':
    if(mp >= 100) {
        on_attack_ex();
        mp = 0;
    }
    break;

case VK_OEM_2:
    if(mp >= 100) {
        on_attack_ex();
        mp = 0;
    }
    break;

现在我们已经在基类中实现了共有逻辑,下面就要到两个子类中补全它们各自的详细功能了。

2 PeashooterPlayer类

我们先来到PeashooterPlayer中。在游戏中,对于婉豆射手,无论是普通攻击还是发动技能,都是发射豌豆,只不过发射频率和豌豆速度不同。
所以我们先封装在场景中生成豌豆子弹的逻辑

private:
    void spawn_pea_bullet(float speed)
    {
        Bullet *bullet = new PeaBullet();

        Vector2 bullet_position, bullet_velocity;
        const Vector2 &bullet_size = bullet->get_size();
        bullet_position.x = is_facing_right 
            ? position.x + size.x - bullet_size.x / 2
            : position.x - bullet_size.x / 2;
        bullet_position.y = position.y;
        bullet_velocity.x = is_facing_right ? speed : -speed;
        bullet_velocity.y = 0;

        bullet->set_position(bullet_position.x, bullet_position.y);
        bullet->set_velocity(bullet_velocity.x, bullet_velocity.y);

        bullet->set_collide_target(id == PlayerID::P1 ? PlayerID::P2 : PlayerID::P1);
        bullet->set_callback([&]() { mp += 25; });

        bullet_list.push_back(bullet);
    }

这里的lambda函数中的&表示“按引用捕获”,就是说这个lambda函数能够修改函数外面的变量,比如这里的变量mp。

2.1 全局子弹列表bullet_list与普通攻击

我们将会想平台对象一样,把子弹存入到全局的vector变量bullet_list中。为此,我们要来到main.cpp文件,定义Bullet指针数组

std::vector<Bullet *> bullet_list;

在Player中引入这个外部变量
extern std::vector<Bullet *> bullet_list;

之后我们就可以编写on_attack方法了

void on_attack()
{
    spawn_pea_bullet(speed_pea);

    switch(rand() % 2) {
    case 0:
        mciSendString(_T("play pea_shoot_1 from 0"), NULL, 0, NULL);
        break;
    case 1:
        mciSendString(_T("play pea_shoot_2 from 0"), NULL, 0, NULL);
        break;
    }
}

2.2 特殊攻击

接下来我们实现特殊攻击
特殊攻击的条件为能量值蓄满,所以我们不需要记录其冷却时间。但是特殊攻击有一定的持续时间,在这个时间中,我们希望玩家不能随意移动。
所以来到Player中定义一个当前是否正在释放特殊攻击的布尔变量。

bool is_attacking_ex = false;   // 是否正在释放特殊攻击

然后在on_run奔跑方法中,我们可以首先判断当前是否正在释放特殊攻击。
if(is_attacking_ex) {
    return;
}

同样在on_jump方法中,原本只是根据竖直方向速度为0,判断是否可以起跳。现在,我们要加上是否正在特殊攻击的条件。

之后,我们在Player中定义两个定时器

protected:
    Timer timer_attack_ex;              // 特殊攻击状态定时器
    Timer timer_spawn_pea_ex;           // 豌豆子弹发射定时器

在婉豆射手的构造函数中初始化它的攻击状态定时器
timer_attack_ex.set_wait_time(attack_ex_duration);
timer_attack_ex.set_one_shot(true);
timer_attack_ex.set_callback([&]()
    {
        is_attacking_ex = false;
    }
);

timer_spawn_pea_ex.set_wait_time(100);
timer_spawn_pea_ex.set_callback([&]()
    {
        spawn_pea_bullet(speed_pea_ex);
    }
);

随后,我们只需要在on_attack_ex设置is_attacking_ex为true,并重启定时器

void on_attack_ex()
{
    is_attacking_ex = true;
    timer_attack_ex.restart();
}

由于角色在使用特殊攻击时有一套特殊的动画,所以我们来到Player类中添加相关Animation类变量

Animation animation_attack_ex_left;     // 朝向左的特殊攻击动画
Animation animation_attack_ex_right;    // 朝向右的特殊攻击动画

这样,我们就可以在on_attack_ex中根据角色的朝向选择充值不同的角色动画状态
void on_attack_ex()
{
    is_attacking_ex = true;
    timer_attack_ex.restart();

    is_facing_right ? animation_attack_ex_right.reset() : animation_attack_ex_left.reset();

    mciSendString(_T("play pea_shoot_ex from 0"), NULL, 0, NULL);
}

既然我们定义了新的动画,那么就要在角色的初始化中为这些动画进行配置。
首先再PeashooterPlayer头文件中引入外部变量
extern Atlas atlas_peashooter_attack_ex_left;
extern Atlas atlas_peashooter_attack_ex_right;

之后在初始化中添加
animation_attack_ex_left.set_atlas(&atlas_peashooter_attack_ex_left);
animation_attack_ex_right.set_atlas(&atlas_peashooter_attack_ex_right);
animation_sun_text.set_atlas(&atlas_sun_text);

animation_attack_ex_left.set_interval(75);
animation_attack_ex_right.set_interval(75);

除此之外,我们希望婉豆射手发动特殊技能时,画面会持续抖动。
所以在重写的on_update方法中我们根据是否处于特殊攻击状态,对摄像机进行抖动

void on_update(int delta)
{
    Player::on_update(delta);

    if (is_attacking_ex)
    {
        main_camera.shake(5, 100);
        timer_attack_ex.on_update(delta);
        timer_spawn_pea_ex.on_update(delta);
    }
}

我们还要到Player::on_updata中,把
current_animation = is_facing_right ? &animation_run_right : &animation_run_left;

改成
if(is_attacking_ex) {
    current_animation = is_facing_right ? &animation_attack_ex_right : &animation_attack_ex_left;
} else {
    current_animation = is_facing_right ? &animation_run_right : &animation_run_left;
}

3 SunflowerPlayer类

龙日葵在释放特殊技能时,会在头顶产生一个“日”字动画
所以我们额外提供了atlas_sun_text这个图集,在SunflowerPlayer中引入这个外部变量

extern Atlas atlas_sun_text;
extern Atlas atlas_sunflower_attack_ex_left;
extern Atlas atlas_sunflower_attack_ex_right;

并增加下面这些私有变量
private:
    Animation animation_sun_text;           //头顶文本动画
    bool is_sun_text_visible = false;       //是否显示头顶文本

我们还需要两个常量分别定义小型和大型日光炸弹的速度值

private:
    const float speed_sun_ex = 0.15f;       //大型日光炸弹下落速度
    const Vector2 velocity_sun = {0.25f, -0.5f};     //小型日光炸弹的抛射速度

在构造函数中添加下面的代码进行初始配置

animation_attack_ex_left.set_atlas(&atlas_peashooter_attack_ex_left);
animation_attack_ex_right.set_atlas(&atlas_peashooter_attack_ex_right);

animation_attack_ex_left.set_interval(100);
animation_attack_ex_right.set_interval(100);

animation_attack_ex_left.set_loop(false);
animation_attack_ex_right.set_loop(false);
animation_sun_text.set_loop(false);

animation_attack_ex_left.set_callback([&]()
{
    is_attacking_ex = false;
    is_sun_text_visible = false;  
});
animation_attack_ex_right.set_callback([&]()
{
    is_attacking_ex = false;
    is_sun_text_visible = false;
});

attack_cd = 250;

最后在on_update中添加下述逻辑
void on_update(int delta)
{
    Player::on_update(delta);

    if (is_sun_text_visible)
    {
        animation_sun_text.on_update(delta);
    }
}

3.1 特殊攻击的文字动画

因为我们在绘制龙日葵本体之外,还需要绘制“日”字动画,所以不得不重写on_draw方法

void on_draw(const Camera& camera)
{
    Player::on_draw(camera);

    if (is_sun_text_visible) {
        Vector2 text_position;
        IMAGE *frame = animation_sun_text.get_frame();
        text_position.x = position.x - (size.x - frame->getwidth()) / 2;
        text_position.y = position.y - frame->getheight();
        animation_sun_text.on_draw(camera, (int)text_position.x, (int)text_position.y);
    }
}

接下来便是具体的on_attack()实现了

void on_attack()
{
    Bullet* bullet = new SunBullet();

    Vector2 bullet_position;
    const Vector2 &bullet_size = bullet->get_size();
    bullet_position.x = position.x + (size.x - bullet_size.x) / 2;
    bullet_position.y = position.y;

    bullet->set_position(bullet_position.x, bullet_position.y);
    bullet->set_velocity(is_facing_right ? velocity_sun.x : -velocity_sun.x, velocity_sun.y);

    bullet->set_collide_target(id == PlayerID::P1 ? PlayerID::P2 : PlayerID::P1);

    bullet->set_callback([&]() { mp+=35; });
    bullet_list.push_back(bullet);
}

在对手玩家的头顶生成大型日光炸弹需要获取另一个玩家的数据,所以我们引入外部变量

extern Player *player_1;
extern Player *player_2;

之后编写on_attack_ex()
void on_attack_ex()
{
    is_attacking_ex = true;
    is_sun_text_visible = true;

    animation_sun_text.reset();
    is_facing_right ? animation_attack_ex_right.reset() : animation_attack_ex_left.reset();

    Vector2 bullet_position, bullet_velocity;
    Bullet *bullet = new SunBulletEx();
    Player *target_player = (id == PlayerID::P1 ? player_2 : player_1);
    const Vector2 &bullet_size = bullet->get_size();
    const Vector2 &target_size = target_player->get_size();
    const Vector2 &target_position = target_player->get_position();
    bullet_position.x = target_position.x + (target_size.x - bullet_size.x) / 2;
    bullet_position.y = -size.y;
    bullet_velocity.x = 0;
    bullet_velocity.y = speed_sun_ex;

    bullet->set_position(bullet_position.x, bullet_position.y);
    bullet->set_velocity(bullet_velocity.x, bullet_velocity.y);

    bullet->set_collide_target(id == PlayerID::P1 ? PlayerID::P2 : PlayerID::P1);
    bullet->set_callback([&]() { mp += 50; } );

    bullet_list.push_back(bullet);

    mciSendString(_T("play sun_text from 0"), NULL, 0, NULL);
}

4 渲染和更新子弹列表

需要注意的是,现在我们只实现了普通攻击和特殊工具的部分逻辑,还没有编写渲染子弹的函数,因此现在运行程序还不能看到子弹。
所以我们在GameScene的on_draw方法中遍历所有子弹并渲染

for (const Bullet* bullet: bullet_list) {
    bullet->on_draw(camera);
}

还需要在GameScene::on_update方法中对所有子弹进行更新
void on_update(int delta) {
    player_1->on_update(delta);
    player_2->on_update(delta);

    main_camera.on_update(delta);

    for (Bullet* bullet: bullet_list) {
        bullet->on_update(delta);
    }
}

运行程序,我们可以通过F和句号大于号键来分别让婉豆射手和龙日葵进行普通攻击。

【从零开始的C++游戏开发】玩家子弹发射和角色技能实现 | EasyX制作植物明星大乱斗_哔哩哔哩_bilibili

评论

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