跳转至

12-子弹派生类

这节课,我们要实现子弹的派生类

1 PeaBullet类

首先实现PeaBullet类的基本定义

class PeaBullet : public::Bullet
{
public:
    PeaBullet() = default;
    ~PeaBullet() = default;

private:
    Animation animation_break;      //豌豆子弹破碎动画
};

我们准备了三种不同的豌豆破碎的声音,用于在豌豆子弹发生碰撞时随机播放。这种设计非常常见,比如脚步声、枪声等。
Pasted image 20260304113633.png
因此,我们就需要重写on_collide()等函数的逻辑了
void on_colilde()
{
    Bullet::on_collide();

    switch (rand() % 3) 
    {
    case 0:
        mciSendString(_T("play pea_break_1 from 0"), NULL, 0, NULL);
        break;
    case 1:
        mciSendString(_T("play pea_break_2 from 0"), NULL, 0, NULL);
        break;
    case 2:
        mciSendString(_T("play pea_break_3 from 0"), NULL, 0, NULL);
        break;
    }
}

void on_update(int delta)
{
    position += velocity * (float)delta;

    if(!valid)
    {
        animation_break.on_update(delta);
    }

    if (check_if_exceeds_screen()) {
        can_remove = true;
    }
}

void on_draw(const Camera& camera) const
{
    if(valid) {
        putimage_alpha(camera, (int)position.x, (int)position.y, &img_pea);
    } else {
        animation_break.on_draw(camera, (int)position.x, (int)position.y);
    }
}

我们还需要引入外部变量,和编写构造函数
extern IMAGE img_pea;                   // 豌豆图片
extern Atlas atlas_pea_break;           // 豌豆破碎动画图集

PeaBullet() {
    size.x = 64, size.y = 64;

    damage = 10;

    animation_break.set_atlas(&atlas_pea_break);
    animation_break.set_interval(100);
    animation_break.set_loop(false);
    animation_break.set_callback([&]() { can_remove = false; });
}

2 SunBullet类

class SunBullet : public::Bullet
{
public:
    SunBullet() = default;
    ~SunBullet() = default;

private:
    Animation animation_idle;       //日光炸弹默认动画
    Animation animation_explode;    //日光炸弹爆炸动画
    Vector2 explode_render_offset;  //爆炸动画渲染偏移
};

与豌豆子弹不同的是,日光炸弹在默认状态下就有不断闪烁的动画效果。所以,我们需要两个动画对象。
除此之外,我们还需要一个二维向量来描述爆炸动画渲染时的位置偏移。


Pasted image 20260304120749.png

这时因为爆炸和默认状态时图片的尺寸不同,所以我们需要这个偏移量来让它们在渲染时中心是对齐的。

SunBullet() {
    size.x = 96, size.y = 96;
    damage = 20;

    animation_idle.set_atlas(&atlas_sun);
    animation_explode.set_atlas(&atlas_sun_explode);
    animation_idle.set_interval(50);
    animation_explode.set_interval(50);
    animation_explode.set_loop(false);

    IMAGE *frame_idle = animation_idle.get_frame();
    IMAGE *frame_explode = animation_explode.get_frame();

    explode_render_offset.x = (frame_idle->getwidth() - frame_explode->getwidth() ) / 2;
    explode_render_offset.y = (frame_idle->getheight() - frame_explode->getwidth()) / 2;
}

当发生碰撞时,摄像机会发生抖动,并且要播放爆炸音效。

void on_collide()
{
    Bullet::on_collide();

    main_camera.shake(5, 250);

    mciSendString(_T("paly sun_explode from 0"), NULL, 0, NULL);
}

随后便是实现on_updata更新方法了
我们先回顾一下PeaBullet类的更新方法

void on_update(int delta)
{
    position += velocity * (float)delta;

    if(!valid) {
        animation_break.on_update(delta);
    }

    if(check_if_exceeds_screen()) {
        can_remove = true;
    }
}

注意,在这里无论豌豆是否已经发生了碰撞,都会一直向前飞行,配合爆炸动画,形成飞溅效果。

但是日光炸弹不能够如此简单粗暴地实现更新,炸弹的爆炸动画应该留在原地,而不会继续受到重力影响运动。我们先声明重力常量

private:
    const float gravity = 1e-3f;    //日光炸弹重力

然后实现on_update以及on_draw方法
void on_update(int delta)
{
    if (valid)
    {
        velocity.y += gravity * delta;
        position += velocity * (float)delta;
    }

    if(!valid) {
        animation_explode.on_update(delta);
    } else {
        animation_idle.on_update(delta);
    }

    if(check_if_exceeds_screen())
    {
        can_remove = true;
    }
}

void on_draw(const Camera& camera) const
{
    if(valid) {
        animation_idle.on_draw(camera, (int)position.x, (int)position.y);
    } else {
        animation_explode.on_draw(camera,
            (int)(position.x+explode_render_offset.x),
            (int)(position.y+explode_render_offset.y));
    }
}

3 SunBulletEx类

class SunBulletEx: public Bullet
{
public:
    SunBulletEx() = default;
    ~SunBulletEx() = default;
private:
    Animation animation_idle;       // 巨大日光炸弹默认动画
    Animation animation_explode;    // 巨大日光炸弹爆炸动画
    Vector2 explode_render_offset;  // 爆炸动画渲染偏移
};

之后重写构造函数,在其中对于私有变量进行初始化。
SunBulletEx() 
{
    size.x = 288, size.y = 288;

    damage = 20;

    animation_idle.set_atlas(&atlas_sun_ex);
    animation_idle.set_interval(50);

    animation_explode.set_atlas(&atlas_sun_ex_explode);
    animation_explode.set_interval(50);
    animation_explode.set_loop(false);
    animation_explode.set_callback([&](){ can_remove = true; });

    IMAGE *frame_idle = animation_idle.get_frame();
    IMAGE *frame_explode = animation_idle.get_frame();
    explode_render_offset.x = (frame_explode->getwidth() - frame_idle->getwidth()) / 2;
    explode_render_offset.y = (frame_explode->getheight() - frame_idle->getheight()) / 2;
}

之后我们实现on_collide方法,进行更大范围的摄像机抖动,并且播放与之对应的音效

void on_collide()
{
    Bullet::on_collide();

    main_camera.shake(20, 350);

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

由于大型日光炸弹的尺寸更大,所以原有的碰撞检测算法已经不太合适了,我们应该重写check_collision方法.
这里的检测方法就是之前检测两个矩形是否重合的方法

bool check_collision(const Vector2& position, const Vector2& size)
{
    bool is_collide_x = (std::max(this->position.x + this->size.x, position.x + size.x)
        - std::min(this->position.x, position.x) ) <= (this->size.x + size.x);
    bool is_collide_y = (std::max(this->position.y + this->size.y, position.y + size.y)
        - std::min(this->position.y, position.y) ) <= (this->size.y + size.y);

    return is_collide_x && is_collide_y;
}

接下来on_update和on_draw方法的实现和小型日光炸弹相似
void on_update(int delta)
{
    if(valid) {
        position += velocity * (float)delta;
    }

    if(!valid) {
        animation_explode.on_update(delta);
    } else {
        animation_idle.on_update(delta);
    }

    if(check_if_exceeds_screen()) {
        can_remove = true;
    }
}

void on_draw(const Camera& camera) const
{
    if(valid) {
        animation_idle.on_draw(camera, (int)position.x, (int)position.y);
    } else {
        animation_explode.on_draw(camera,
            (int)(position.x + explode_render_offset.x),
            (int)(position.y - explode_render_offset.y));
    }
}

评论

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