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,就可以直接在这里评论。