04-有限状态机与横版游戏角色类设计
1 状态机
1.1 概念
在本阶段项目中,尤其是再引入具备AI行为的BOSS敌对角色时,其行为和状态阶段比较复杂。如果把所有逻辑都堆在on_update中,就过于臃肿了。
所以我们不得不开始设计点更优雅的架构。来将不同阶段的岱庙划分到不同的区域中,有条理地进行处理。
这时,就要搬出游戏开发技术中的一个热度很高的经典设计了——有限状态机。
它的英文名叫FSM,Finite State Machine。

状态机再运行时,同一时刻只会有一个状态节点被激活。
当我们设计一个状态机时,本质是再设计其中各个状态节点的内容。
想要设计一个状态机节点,我们需要从下面几个方面入手。
- 条件(或称之为“事件”):当满足何种情况时,状态机才会进入到当前状态节点中。
- 动作:即当前状态下要做什么。
- 变换:即当前状态退出的时机。

1.2 StateNode类
我们新建state_node.h头文件,在其中声明StateNode类
class StateNode
{
public:
StateNode() = default;
~StateNode() = default;
virtual void on_enter() { }
virtual void on_update(float delta) { }
virtual void on_exit() { }
};
以后玩家和敌人的状态机只需要继承这个类,并重写对应的方法。
1.3 StateMachine类
对于状态节点的管理器,我们便可以创建state_machine.h头文件,并定义状态机类。
class StateMachine
{
public:
StateMachine();
~StateMachine();
void on_update(float delta);
void set_entry(const std::string &id);
void switch_to(const std::string &id);
void register_state(const std::string &id, StateNode *state_node);
private:
bool need_init = true;
StateNode *current_state = nullptr;
std::unordered_map<std::string, StateNode *> state_pool;
};
- 为了更方便地对状态节点进行管理,我们和先前一样借助哈希表实现了状态池。我们可以通过字符串形式地状态名,注册新的状态或切换状态机地当前状态。
- need_init标记了当前状态机是否已经被初始化过。也就是入口节点状态是否已经执行过on_enter逻辑。
- current_state指针记录着当前激活的状态节点。以方便在on_update的过程中调用其更新逻辑。
- set_entery和switch_to分别用来设置初始状态,和切换状态
- register_state则用来向状态机中注册新的状态
对应到state_machine.cpp源代码文件中,
StateMachine::StateMachine() = default;
StateMachine::~StateMachine() = default;
void StateMachine::on_update(float delta)
{
if(!current_state)
return;
if (need_init)
{
current_state->on_enter();
need_init = false;
}
current_state->on_update(delta);
}
void StateMachine::set_entry(const std::string& id)
{
current_state = state_pool[id];
}
void StateMachine::switch_to(const std::string& id)
{
if (current_state)
current_state->on_exit();
current_state = state_pool[id];
if (current_state)
current_state->on_enter();
}
void StateMachine::register_state(const std::string& id, StateNode* state_node)
{
state_pool[id] = state_node;
}
2 Character角色类实现
2.1 声明头文件
无论是玩家还是敌人,我们希望它们都能继承一个游戏角色基类。
所以先定义charater头文件,
class Character
{
public:
Character();
~Character();
protected:
struct AnimationGroup
{
Animation left;
Animation right;
};
protected:
const float FLOOR_Y = 620; // 地板的竖直方向坐标
const float GRAVITY = 980 * 2; // 重力大小
protected:
int hp = 10; // 角色生命值
Vector2 position; // 角色脚底位置
Vector2 velocity; // 角色速度
float logic_height = 0; // 角色的逻辑高度
bool is_facing_left = true; // 当前角色是否朝向左
StateMachine state_machine; // 角色逻辑状态机
bool enable_gravity = true; // 启用重力模拟
bool is_invulnerable = false; // 当前是否无敌
Timer timer_invulnerable_blink; // 无敌闪烁效果计时器
Timer timer_invulnerable_statue; // 无敌状态计时器
bool is_blink_invulnerable = false; // 当前是否处于闪烁的不可见帧
CollisionBox *hit_box = nullptr; // 攻击碰撞箱
CollisionBox *hurt_box = nullptr; // 受击碰撞箱
AnimationGroup *current_animation = nullptr; // 当前角色动画
std::unordered_map<std::string, AnimationGroup> animation_pool; // 角色动画池
};
之后,需要定义下面这些方法,方便在外部获取或修改Character中刚刚定义的protected字段
public:
Character();
~Character();
void decrease_hp();
int get_hp() const
{
return hp;
}
void set_position(const Vector2& position)
{
this->position = position;
}
const Vector2& get_position() const
{
return position;
}
void set_velocity(const Vector2& velocity)
{
this->velocity = velocity;
}
const Vector2& get_velocity() const
{
return velocity;
}
Vector2 get_logic_center() const
{
return Vector2(position.x, position.y - logic_height / 2);
}
void set_gravity_enabled(bool flag)
{
enable_gravity = flag;
}
CollisionBox* get_hit_box()
{
return hit_box;
}
CollisionBox* get_hurt_box()
{
return hurt_box;
}
bool is_on_floor() const
{
return position.y >= FLOOR_Y;
}
float get_gloor_y() const
{
return FLOOR_Y;
}
void make_invulnerable()
{
is_invulnerable = true;
timer_invulnerable_status.restart();
}
virtual void on_input(const ExMessage &msg);
virtual void on_update(float delta);
virtual void on_render();
virtual void on_hurt();
void Character::switch_state(const std::string &id);
void Character::set_animation(const std::string &id);
代码编写的思路始终是从游戏的实际功能需求出发,过度设计和过度封装是工程推进的大忌;在试错和学习的过程中,一定不要太多纠结细枝末节、无关紧要的细节而固步自封。
2.2 实现关键方法
下面便可以创建对应的character.cpp源文件,引入所需的头文件,开始编写先前声明的方法实现了。
首先是构造函数部分.设置闪烁状态持续1.0秒,闪烁帧每0.075秒切换一次。
Character::Character()
{
hit_box = CollisionManager::instance()->create_collision_box();
hurt_box = CollisionManager::instance()->create_collision_box();
timer_invulnerable_status.set_wait_time(1.0f);
timer_invulnerable_status.set_one_shot(true);
timer_invulnerable_status.set_callback([&]()
{ is_invulnerable = false; });
timer_invulnerable_blink.set_wait_time(0.075f);
timer_invulnerable_blink.set_one_shot(false);
timer_invulnerable_blink.set_callback([&]()
{ is_blink_invulnerable = !is_blink_invulnerable; });
}
对应在析构函数中,销毁碰撞箱
Character::~Character()
{
CollisionManager::instance()->destroy_collision_box(hit_box);
CollisionManager::instance()->destroy_collision_box(hurt_box);
}
然后是decreas_hp方法
void Character::decrease_hp()
{
if (is_invulnerable)
return;
hp -= 1;
if (hp > 0)
make_invulnerable();
on_hurt();
}
之后在on_update方法中,我们依次实现下述逻辑
- 更新状态机
- 重力逻辑(更新速度和位置)
- 对位置进行限制
- 设置受击碰撞箱的位置
- 更新闪烁计时器
- 根据朝向更新动画,并设置其位置
void Character::on_update(float delta) { state_machine.on_update(delta); if (hp<=0) { velocity.x = 0; } if (enable_gravity) { velocity.y += GRAVITY * delta; } position += velocity * delta; if(position.y >= FLOOR_Y) { position.y = FLOOR_Y; velocity.y = 0; } if (position.x <= 0) position.x=0; if (position.x >= getwidth()) position.x = (float)(getwidth()); hurt_box->set_position(get_logic_center()); timer_invulnerable_status.on_update(delta); if(is_invulnerable) timer_invulnerable_blink.on_update(delta); if (!current_animation) return; Animation &animation = (is_facing_left ? current_animation->left : current_animation->right); animation.on_update(delta); animation.set_position(position); }
在on_render方法中, 如果没有设置当前动画或者处于闪烁状态的不可见帧,则直接返回。
void Character::on_render()
{
if (!current_animation || (is_invulnerable && is_blink_invisible)) return;
(is_facing_left ? current_animation->left : current_animation->right).on_render();
}
on_hurt方法主要负责在受击播放不同效果的受击音效。因此在基类中我们留空即可。
void Character::on_hurt() { }
对于切换状态和设置动画的方法实现,只需要操作状态机和动画池即可。
void Character::switch_state(const std::string& id)
{
state_machine.switch_to(id);
}
void Character::set_animation(const std::string& id)
{
current_animation = &animation_pool[id];
current_animation->left.reset();
current_animation->right.reset();
}
评论
如果你已登录 GitHub,就可以直接在这里评论。