跳转至

14-无敌帧和玩家状态栏实现

1 玩家与子弹碰撞

现在,我们需要添加玩家与子弹的碰撞功能。
我们来到Player::move_and_collide()中,在玩家与平台进行碰撞检测后,遍历所有的子弹。

for(Bullet* bullet: bullet_list) {
    if(!bullet->get_valid() || bullet->get_collide_target() != id) {
        continue;
    }

    if(bullet->check_collision(position, size))
    {
        bullet->on_collide();
        bullet->set_valid(false);
        hp -= bullet->get_damage();
    }
}

这部分完成后,我们还需要回到GameScene中,在on_update中添加子弹的移除逻辑。
bullet_list.erase(std::remove_if(
    bullet_list.begin(), bullet_list.end(),
    [](const Bullet* bullet)
    {
        bool deletable = bullet->check_can_remove();
        if (deletable)
            delete bullet;
        return deletable;
    }
), bullet_list.end());

std::remove_if 是 C++ STL 中的一个算法(定义在 <algorithm> 头文件中),核心作用是根据指定条件 “移除” 容器中满足条件的元素,但它的工作方式有个关键特点需要重点理解:
std::remove_if 并不会真正删除容器中的元素(不会改变容器的 size()),而是做两件事:

  • 遍历容器,把不满足删除条件的元素依次向前移动,覆盖掉满足条件的元素;
  • 返回一个迭代器,指向 “有效元素” 的末尾(即最后一个不满足条件元素的下一个位置)。
    它一般与vector类的erase方法配合使用;

之后,运行程序可以看到子弹与角色碰撞的逻辑正确执行,能够正确播放子弹破碎的动画以及执行摄像机震动。

2 无敌帧

现在我们开始编写玩家受击后短暂的无敌功能。首先来到Player类中

bool is_invulnerable = false;       // 角色是否处于无敌状态
bool is_showing_sketch_frame = false;   //当前帧是否应该显示剪影

我们希望玩家处于无敌状态时通过闪烁动画来提示玩家,那么就需要不断切换玩家的动画序列帧和剪影序列帧。二者的“切换”需要用到定时器
Timer timer_invulnerable;           // 无敌状态定时器
Timer timer_invulnerable_blink;     // 无敌状态闪烁定时器

这两个定时器分别控制无敌状态的结束以及闪烁时两种序列帧的切换功能。
timer_invulnerable.set_wait_time(750);
timer_invulnerable.set_one_shot(true);
timer_invulnerable.set_callback([&]()
    {
      is_invulnerable = false;  
    });

timer_invulnerable_blink.set_wait_time(75);
timer_invulnerable_blink.set_callback([&]()
    {
        is_showing_sketch_frame = !is_showing_sketch_frame;
    });

之后我们来到on_update方法中,对两个定时器进行更新

timer_invulnerable.on_update(delta);
timer_invulnerable_blink.on_update(delta);

随后我们来到util.hpp中,添加产生剪影图片的函数

inline void sketch_image(IMAGE* src, IMAGE* dst)
{
    int w = src->getwidth();
    int h = src->getheight();
    Resize(dst, w, h);
    DWORD* src_buffer = GetImageBuffer(src);
    DWORD* dst_buffer = GetImageBuffer(dst);
    for (int y = 0; y < h; y++)
    {
        for (int x = 0; x < w; x++)
        {
            int idx = y * w + x;
            dst_buffer[idx] = BGR(RGB(255, 255, 255)) | (src_buffer[idx] & 0xFF000000);
        }
    }
}

我们在Player中添加一个IMAGE对象

IMAGE img_sketch;                   // 动画帧剪影图片

在on_update中就可以判断当前是否需要显示剪影图片

if (is_showing_sketch_frame) {
    sketch_image(current_animation->get_frame(), &img_sketch);
}

然后修改on_draw方法,选择是否渲染剪影图片
virtual void on_draw(const Camera& camera)
{
    if (hp>0 && is_invulnerable && is_showing_sketch_frame) {
        putimage_alpha(camera, (int)position.x, (int)position.y, &img_sketch);
    } else {
        current_animation->on_draw(camera, (int)position.x, (int)position.y);
    }
}

之后我们需要考虑如何触发无敌状态

void make_invulnerable()
{
    is_invulnerable = true;
    timer_invulnerable.restart();
}

然后来到move_and_collide方法的子弹碰撞部分,在外面套上一层if条件语句,并在最内部检测到碰撞时调用make_invulnerable()
if(!is_invulnerable) {
    for(Bullet* bullet: bullet_list) {
        if(!bullet->get_valid() || bullet->get_collide_target() != id) {
            continue;
        }

        if(bullet->check_collision(position, size))
        {
            make_invulnerable();
            bullet->on_collide();
            bullet->set_valid(false);
            hp -= bullet->get_damage();
        }
    }
}

3 调试模式

我们可以仿照平台的调试轮廓线,给玩家和子弹也添加相似的调试线框。来在运行时检查碰撞数据是否正常。

我们首先在Player中声明外部变量

extern bool is_debug;

在on_draw根据调试模式绘制边框。我们也可以和之前line那样封装一个带camera作为参数的绘制函数。但是由于我们游戏中的摄像机移动并不明显,所以我们就之间使用原始函数了。
if (is_debug)
{
    setlinecolor(RGB(0, 255, 255));
    rectangle((int)position.x, (int)position.y, (int)(position.x + size.x), (int)(position.y + size.y));
}

同样,在bullet.h中,也引入is_debug。重写on_draw

virtual void on_draw(const Camera& camera) const { 
    if (is_debug)
    {
        setfillcolor(RGB(255, 255, 255));
        setfillcolor(RGB(255, 255, 255));
        rectangle((int)position.x, (int)position.y,
                  (int)(position.x + size.x),
                  (int)(position.y + size.y));
        solidcircle((int)(position.x + size.x / 2), (int)(position.y + size.y / 2), 5);
    }
}

之后我们来到三种bullet子类的on_draw方法中调用父类的on_draw方法

Bullet::on_draw(camera);

然后运行程序,按下Q键打开调试模式。

大型日光炸弹无法显示动画,但是在调试模式下能看到边框

检查发现bug的原因是SunBulletEx::on_draw()函数与Bullet::on_draw()函数的定义不完全相同,仅相差一个const,这导致调用draw时只执行了基类Bullet中的函数,而没有执行子类中的函数

4 特殊攻击下禁用方向改变

我们在Player::on_update()中is_facing_right之前首先判断了当前的特殊攻击状态。另外,为了简化逻辑,我们把特殊攻击状态下,当前动画的设置放在if语句之外了。

if(direction != 0)
{
    if(!is_attacking_ex) {
        is_facing_right = direction > 0;
    }
    current_animation = is_facing_right ? &animation_run_right : &animation_run_left;
    float distance = direction * run_velocity * delta;
    on_run(distance);
} else {
    current_animation = is_facing_right ? &animation_idle_right : &animation_idle_left;
}

if(is_attacking_ex) {
    current_animation = is_facing_right ? &animation_attack_ex_right : &animation_attack_ex_left;
}

5 玩家状态栏

我们新建status_bar.h,然后定义StatusBar类

class StatusBar
{
public:
    StatusBar() = default;
    ~StatusBar() = default;

private:
    const int width = 275;

    int hp = 0;                  // 需要显示的生命值
    int mp = 0;                  // 需要显示的能量值
    POINT position = {0};        // 在窗口中显示的位置
    IMAGE *img_avatar = nullptr; // 角色头像图片
};

编写相关配置函数
void set_avatar(IMAGE *img)
{
    img_avatar = img;
}

void set_position(int x, int y)
{
    position.x = x, position.y = y;
}

void set_hp(int val)
{
    hp = val;
}

void set_mp(int val)
{
    mp = val;
}

再编写on_draw函数,在窗口中绘制状态栏
void on_draw()
{
    putimage_alpha(position.x, position.y, img_avatar);

    setfillcolor(RGB(5, 5, 5));
    solidroundrect(position.x + 100, position.y + 10, position.x + 100 + width + 3 * 2, position.y + 36, 8, 8);
    solidroundrect(position.x + 100, position.y + 45, position.x + 100 + width + 3 * 2, position.y + 71, 8, 8);
    setfillcolor(RGB(67, 67, 67));
    solidroundrect(position.x + 100, position.y + 10, position.x + 100 + width + 3 * 2, position.y + 33, 8, 8);
    solidroundrect(position.x + 100, position.y + 45, position.x + 100 + width + 3 * 2, position.y + 68, 8, 8);

    float hp_bar_width = width * std::max(0, hp) / 100.0f;
    float mp_bar_width = width * std::min(100, mp) / 100.0f;

    setfillcolor(RGB(197, 61, 67));
    solidroundrect(position.x + 100, position.y + 10, position.x + 100 + (int)hp_bar_width + 3, position.y + 33, 8, 8);
    setfillcolor(RGB(83, 131, 195));
    solidroundrect(position.x + 100, position.y + 45, position.x + 100 + (int)mp_bar_width + 3, position.y + 68, 8, 8);
}

在on_draw方法中对他们进行绘制
status_bar_1P.on_draw();
status_bar_2P.on_draw();

为了能够获得玩家的生命值、能量值等信息,我们在Player类中添加下面两个函数
int get_hp() const
{
    return hp;
}

int get_mp() const
{
    return mp;
}

之后在GameScene中对状态栏进行更新
status_bar_1P.set_hp(player_1->get_hp());
status_bar_1P.set_mp(player_1->get_mp());
status_bar_2P.set_hp(player_2->get_hp());
status_bar_2P.set_mp(player_2->get_mp());

我们还要把每个玩家的玩家头像进行设置
所以首先来到main.cpp中,定义两个全局图片指针

IMAGE *img_player_1_avatar = nullptr;
IMAGE *img_player_2_avatar = nullptr;

来到SelectorScene中添加它们的外部声明
extern IMAGE *img_player_1_avatar;
extern IMAGE *img_player_2_avatar;

最后在退出阶段实例化玩家后,设置它们的头像

switch (player_type_1) 
{
case PlayerType::Peashooter:
    player_1 = new PeashooterPlayer();
    img_player_1_avatar = &img_avatar_peashooter;
    break;
case PlayerType::Sunflower:
    player_1 = new SunflowerPlayer();
    img_player_1_avatar = &img_avatar_sunflower;
    break;
}
player_1->set_id(PlayerID::P1);

switch (player_type_2) 
{
case PlayerType::Peashooter:
    player_2 = new PeashooterPlayer();
    img_player_2_avatar = &img_avatar_peashooter;
    break;
case PlayerType::Sunflower:
    player_2 = new SunflowerPlayer();
    img_player_2_avatar = &img_avatar_sunflower;
    break;
}

在GameScene中,同样声明外部变量

extern IMAGE *img_player_1_avatar;
extern IMAGE *img_player_2_avatar;

使用下面这样的代码设置两位玩家的状态条对象的头像和位置。
status_bar_1P.set_avatar(img_player_1_avatar);
status_bar_2P.set_avatar(img_player_2_avatar);

status_bar_1P.set_position(235, 625);
status_bar_2P.set_position(675, 625);

评论

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