はじめに
この記事はCCS Advent Calendar 2019 12/09の記事です。
adventar.org
こまつなさん 2019年、本厄だったらしい[CCS Advent Calendar 2019 7日目] ←前 | 後→MeerIiokaさん
C++でステートマシンをつくる
プログラミング言語C++を用いて、ステートマシンと呼ばれる機構をつくります。
ステートマシンって何?
もともとはオートマトンとかの分野で使われている言葉で、入力と現在の状態によって次の状態が決まる論理回路のことをステートマシンと呼んだりします。状態遷移図なんかでモデル化でき、複数の状態をなんらかの起因で変化していくものに対して扱います。
本記事では、C++のクラスなどを用いて、複数の状態を遷移することで描画内容や更新内容を変化させることのできる機構をつくることにします。
やりたいこと
ステートマシンを用いてやりたいことは以下のような感じです。
- 現在の状態に適した
update()
、draw()
関数を呼びたい
- 状態変化のトリガーを外部から与えたい
- 状態変化時に呼びたい関数など毎フレーム呼ばれない関数も定義したい
これらの要望に応える機構を目指します。
つくったもの
実際に作ったものをはります。
注意点
以下のコードはOpenSiv3Dを用いて使うことを想定したコードです。とはいえ、OpenSiv3Dの機能で用いているのはHashTable
とログ出力のみなので、std::unordered_map
やcerr
などで代用すれば問題なく他でも使えると思います。
あとそれなりに新しいC++じゃないと動かないともいます。explicit
とかある・・・・・・。
コード
# pragma once
# include <Siv3D.hpp>
template<typename T>
class State {
private:
T m_id;
public:
State() = delete;
explicit State(T id) :m_id(id) {}
virtual ~State() = default;
const T& Id() { return m_id; }
virtual void setUp() {}
virtual void update() {}
virtual void draw() const {}
virtual void cleanUp() {}
};
template<typename T>
class StateMachine {
private:
HashTable<T, std::shared_ptr<State<T>>> m_stateList;
std::shared_ptr<State<T>> m_state;
public:
StateMachine() = default;
virtual ~StateMachine() = default;
Optional<const T&> getCurrentStateName() const {
if (m_state == nullptr) {
return none;
}
return m_state->Id();
}
void goToState(T nextStateId) {
if (!m_stateList.contains(nextStateId)) {
Print << U"Error: Not exist state: " << nextStateId;
return;
}
if (m_state != nullptr) {
m_state->cleanUp();
}
m_state = m_stateList[nextStateId];
m_state->setUp();
}
virtual void initializeStateMachine() = 0;
void addState(const std::shared_ptr<State<T>>& state) {
if (state == nullptr) {
Print << U"Error: This state is nullptr";
return;
}
if (m_stateList.contains(state->Id())) {
Print << U"Error: Already exist state: " << state->Id();
return;
}
m_stateList[state->Id()] = state;
}
void update() {
if (m_state == nullptr) {
return;
}
m_state->update();
}
void draw() const {
if (m_state == nullptr) {
return;
}
m_state->draw();
}
};
使い方
以下使い方です。
State クラス
状態に関するクラスです。インターフェースみたいなやつです。状態遷移図でいう丸のやつですね。
状態を表すクラスに継承します。テンプレート引数に状態名管理クラスをつけるのを忘れずに。
enum TestStates{
Idle,
Runnning
};
struct Idle : public State<TestStates> {
};
デフォルトコンストラクタは許可していません。必ず状態名を引数につけましょう。
struct Idle : public State<TestStates> {
Idle() : State<TestStates>(TestStates::Idle) {}
};
状態名を取得します。
この状態に入ったときだけ呼ばれます。繰り返しは呼ばれません。
毎フレーム呼ばれる更新用関数です。
毎フレーム呼ばれる描画用関数です。
次の状態に移る直前に呼ばれます。
setUp()
、update()
、draw()
、cleanUp()
はオーバーライドしなくても動きます。つまり何もしないということもOKです。
StateMachine クラス
ステートマシンとして管理したいクラスに継承します。継承の際はテンプレート引数に状態名を管理するものを入れてあげましょう。
class Test : public StateMachine<String> {
};
enum TestStates {
Idle,
Running
};
class Test2 : public StateMachine<TestStates> {
};
特に指定はありません。インスタンスを作ればOKです。特に理由がないのであればここでinitializeStateMachine()
を呼んであげるようにするのが丸いです。
純粋仮想関数になっていて、必ずオーバーライドする必要があります。あとは初期化したいときに呼びましょう。ここでは、作成したインスタンスに状態を追加していきます。状態の追加にはaddState()
メソッドを使います。追加する際には、安全のためshared_ptr
を使っているのでstd::make_shared
します。
addState(std::make_shared<Idle>());
addState(std::make_shared<Running>());
- addState(const std::shared_ptr<State>& state)
状態を追加する関数です。最初に呼ばずともあとから呼ぶこともできます。State
クラスを継承したオブジェクトをmake_shared
して渡してあげましょう。
現在の状態のupdate()
関数を呼びます。
現在の状態のdraw()
関数を呼びます。
指定の状態へ移行します。引数には状態名をいれてあげましょう。
if (ball.x >= 500) main.goToState(TestStates::Idle);
- getCurrentStateName() const
現在の状態名を取得できます。
使用例
以下使用例です。ステートマシンで管理するオブジェクト本体の数値を状態クラスからもいじりたいのでコンストラクタで自身の参照を渡しています。よしなにやりましょう。
ちなみにこのコードでは円の移動に座標計算を行っていますがこれは悪い例で移動速度がフレームレートに依存します。Scene::Delta()
を使うとかしましょう。
# include <Siv3D.hpp>
# include "StateMachine.hpp"
enum BallStates {
Idle,
Moving,
};
class MovingBall : public StateMachine<BallStates> {
Circle m_ball;
public:
MovingBall() {
m_ball.r = 40;
m_ball.setPos({ 100, 200 });
initializeStateMachine();
goToState(BallStates::Idle);
}
void initializeStateMachine() override {
addState(std::make_shared<Idle>(*this));
addState(std::make_shared<Moving>(*this));
}
struct Idle : public State<BallStates> {
MovingBall& main;
Idle(MovingBall &_main) :State<BallStates>(BallStates::Idle), main(_main) {}
void setUp() override {
main.m_ball.x = 100;
Print << U"Start Idle";
}
void update() override {
if (MouseL.down()) {
main.goToState(BallStates::Moving);
}
}
void draw() const override {
main.m_ball.draw(Palette::Blue);
}
void cleanUp() override {
Print << U"Finish Idle";
}
};
struct Moving : public State<BallStates> {
MovingBall& main;
Moving(MovingBall& _main) :State<BallStates>(BallStates::Moving), main(_main) {}
void setUp() override {
Print << U"Start Moving";
}
void update() override {
main.m_ball.x += 10;
if (main.m_ball.x >= 500) {
main.goToState(BallStates::Idle);
}
}
void draw() const override {
main.m_ball.draw(Palette::Red);
}
void cleanUp() override {
Print << U"Finish Moving";
}
};
};
void Main()
{
Scene::SetBackground(ColorF(0.8, 0.9, 1.0));
MovingBall b;
while (System::Update())
{
b.update();
b.draw();
}
}
最後に
以上ステートマシンをC++で作ってみた話でした。意外とこういう便利機能作ってみるのも楽しいので皆さんやってみては?
アドベントカレンダー明日は守銭奴な話か電子書籍のススメが読めるらしいです。
謝辞
作ったコードはOpenSiv3D作者のReputelessさんにレビュー頂きました、ありがとうございました!