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