はじめに
ステートマシンを利用して状態管理のバグを少なくすることで、よりゲームのブラッシュアップに時間をかけたいと考え実装しました。
ステートマシンに必要なクラス
まず初めにステートマシンの実装に必要なインターフェースを定義します。
ステートノード
ステートノードです。
下記のUnityのステートマシンでいう赤丸の部分です。
二次元グラフのこの部分を「ノード」といいます。
//--------------------------------------------------------------------------------------
/// ノードの基底クラス(最低限の機能を持たせる)
//--------------------------------------------------------------------------------------
class NodeBase
{
bool m_isActive; //アクティブ状態
int m_index; //インデックス
public:
NodeBase();
NodeBase(const int index);
virtual ~NodeBase() = default;
public:
//--------------------------------------------------------------------------------------
/// アクセッサ
//--------------------------------------------------------------------------------------
void SetIsActive(const bool isActive) noexcept { m_isActive = isActive; }
bool IsActive() const noexcept { return m_isActive; }
bool GetIsActive() const noexcept { return IsActive(); }
void SetIndex(const int index) noexcept { m_index = index; }
int GetIndex() const noexcept { return m_index; }
};
//--------------------------------------------------------------------------------------
/// ステートマシンのノードインターフェース
//--------------------------------------------------------------------------------------
class I_StateNode
{
public:
virtual void OnStart() = 0;
virtual bool OnUpdate() = 0;
virtual void OnExit() = 0;
virtual ~I_StateNode() = default;
};
//--------------------------------------------------------------------------------------
/// ステートマシン用のノード基底クラス
//--------------------------------------------------------------------------------------
template<class OwnerType>
class NodeBase_StateMachine : public NodeBase, public I_StateNode
{
std::weak_ptr<OwnerType> m_owner; //所有者
public:
NodeBase_StateMachine(const std::shared_ptr<OwnerType>& owner) :
NodeBase_StateMachine(0, owner)
{}
NodeBase_StateMachine(const int index, const std::shared_ptr<OwnerType>& owner):
NodeBase(index),
m_owner(owner)
{}
virtual ~NodeBase_StateMachine() = default;
public:
/// <summary>
/// 所有者の取得
/// </summary>
/// <returns>所有者</returns>
std::shared_ptr<OwnerType> GetOwner() const noexcept {
return m_owner.lock();
}
};
ステートエッジ
ステートエッジです。
下記のUnityのステートマシンでいう赤丸の部分です。
二次元グラフのこの部分を「エッジ」といいます。
ただし、エッジの使いすぎは遷移条件が蜘蛛の巣上になるため注意が必要です。
//--------------------------------------------------------------------------------------
/// エッジのインターフェース
//--------------------------------------------------------------------------------------
class I_Edge {
virtual void SetFromIndex(const int index) noexcept = 0;
virtual int GetFromIndex() const noexcept = 0;
virtual void SetToIndex(const int index) noexcept = 0;
virtual int GetToIndex() const noexcept = 0;
};
//--------------------------------------------------------------------------------------
/// エッジの基底クラス(最低限の機能を持たせる。)
//--------------------------------------------------------------------------------------
class EdgeBase : public I_Edge
{
std::weak_ptr<I_StateNode> m_fromNode;
std::weak_ptr<I_StateNode> m_toNode;
public:
EdgeBase();
EdgeBase(const std::shared_ptr<NodeBase>& fromNode, const std::shared_ptr<NodeBase>& toNode);
public:
//--------------------------------------------------------------------------------------
/// アクセッサ
//--------------------------------------------------------------------------------------
/// <summary>
/// 手前のノードの設定
/// </summary>
/// <param name="node">手前のノード</param>
void SetFromNode(const std::shared_ptr<NodeBase>& node);
std::shared_ptr<NodeBase> GetFromNode() const;
void SetFromIndex(const int index) noexcept override;
int GetFromIndex() const noexcept override;
void SetToNode(const std::shared_ptr<NodeBase>& node);
std::shared_ptr<NodeBase> GetToNode() const;
void SetToIndex(const int index) noexcept override;
int GetToIndex() const noexcept override;
};
/// <summary>
/// ステートマシン用のエッジクラス
/// </summary>
/// <typeparam name="EnumType">使用する列挙体</typeparam>
/// <typeparam name="TransitionStructMember">遷移条件用の構造体メンバー</typeparam>
template<class EnumType, class TransitionStructMember>
class Edge_StateMachine : public EdgeBase
{
public:
using IsTransitionFunction = std::function<bool(const TransitionStructMember& member)>;
private:
IsTransitionFunction m_isTransitionFunc = nullptr; //遷移する条件
int m_priority = 0; //優先度
bool m_isEndTransition = false; //終了時に遷移するかどうか
public:
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="from">手前のインデックス</param>
/// <param name="to">先のインデックス</param>
Edge_StateMachine(
const std::shared_ptr<NodeBase>& fromNode,
const std::shared_ptr<NodeBase>& toNode
) :
StateEdgeBase(fromNode, toNode, nullptr)
{}
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="from">手前のインデックス</param>
/// <param name="to">先のインデックス</param>
/// <param name="isTransitionFunc">遷移条件関数</param>
Edge_StateMachine(
const std::shared_ptr<NodeBase>& fromNode,
const std::shared_ptr<NodeBase>& toNode,
const IsTransitionFunction& isTransitionFunc
) :
StateEdgeBase(fromNode, toNode, isTransitionFunc, static_cast<int>(to))
{}
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="from">手前のインデックス</param>
/// <param name="to">先のインデックス</param>
/// <param name="isTransitionFunc">遷移条件関数</param>
/// <param name="priority">優先度</param>
Edge_StateMachine(
const std::shared_ptr<NodeBase>& fromNode,
const std::shared_ptr<NodeBase>& toNode,
const IsTransitionFunction& isTransitionFunc,
const int priority
) :
Edge_StateMachine(fromNode, toNode, isTransitionFunc, toNode->GetIndex(), false)
{}
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="from">手前のインデックス</param>
/// <param name="to">先のインデックス</param>
/// <param name="isTransitionFunc">遷移条件関数</param>
/// <param name="priority">優先度</param>
/// <param name="isEndTransition">更新処理終了時に判断するかどうか</param>
Edge_StateMachine(
const std::shared_ptr<NodeBase>& fromNode,
const std::shared_ptr<NodeBase>& toNode,
const IsTransitionFunction& isTransitionFunc,
const int priority,
const bool isEndTransition
) :
EdgeBase(fromNode, toNode),
m_isTransitionFunc(isTransitionFunc),
m_priority(priority),
m_isEndTransition(isEndTransition)
{}
virtual ~Edge_StateMachine() = default;
//--------------------------------------------------------------------------------------
/// アクセッサ
//--------------------------------------------------------------------------------------
/// <summary>
/// Toに遷移する条件を設定する。
/// </summary>
/// <param name="func">設定する条件</param>
void SetIsToTransition(const std::function<bool(const TransitionStructMember& member)>& func) {
m_isTransitionFunc = func;
}
/// <summary>
/// 遷移できるかどうか
/// </summary>
/// <param name="member">遷移条件用メンバー</param>
/// <param name="isEndNodeUpdate">更新終了時に判断するかどうか</param>
/// <returns></returns>
bool IsTransition(const TransitionStructMember& member, const bool isEndNodeUpdate = false) {
//終了時遷移なら
if (m_isEndTransition) {
//ノードが終了しているなら監視、そうでないならfalse
return isEndNodeUpdate ? m_isTransitionFunc(member) : false;
}
//終了時遷移でないなら常に監視
return m_isTransitionFunc(member);
}
/// <summary>
/// 優先度の設定
/// </summary>
/// <param name="priority">優先度</param>
void SetPriority(const int priority) noexcept {
m_priority = priority;
}
/// <summary>
/// 優先度の取得
/// </summary>
/// <returns>優先度</returns>
int GetPriority() const noexcept {
return m_priority;
}
EnumType GetFromType() const noexcept {
return static_cast<EnumType>(EdgeBase::GetFromIndex());
}
EnumType GetToType() const noexcept {
return static_cast<EnumType>(EdgeBase::GetToIndex());
}
/// <summary>
/// 更新終了時に遷移判断するかどうかを設定
/// </summary>
/// <param name="isEndTransition">更新終了時に遷移判断するかどうか</param>
void SetIsEndTransition(const bool isEndTransition) noexcept {
m_isEndTransition = true;
}
/// <summary>
/// 更新終了時に遷移判断するかどうかを取得
/// </summary>
/// <returns>更新終了時に遷移判断するならtrue</returns>
bool IsEndTransition() const noexcept {
return m_isEndTransition;
}
};
ステートマシン本体
ステートマシン本体です。
ステートの更新や切り替えなどを担います。
ステートの終了時に遷移させることも可能です。
Update関数はステートマシンを持つクラス(外部クラス)が呼びます。
StateMachineが内部で持つGraphBaseクラスはこちらです。
//--------------------------------------------------------------------------------------
/// 前方宣言
//--------------------------------------------------------------------------------------
class NodeBase;
template<class OwnerType>
class NodeBase_StateMachine;
class EdgeBase;
template<class EnumType, class TransitionMember>
class Edge_StateMachine;
//--------------------------------------------------------------------------------------
/// ステートマシン用の新規クラス(完成したら、前回の削除)
//--------------------------------------------------------------------------------------
template<class OwnerType, class EnumType, class TransitionStructMember>
class StateMachine
{
public:
using NodeType = NodeBase_StateMachine<OwnerType>;
using EdgeType = Edge_StateMachine<EnumType, TransitionStructMember>;
using GraphType = maru::GraphBase<EnumType, NodeType, EdgeType>;
//--------------------------------------------------------------------------------------
/// 遷移先候補のパラメータ
//--------------------------------------------------------------------------------------
struct TransitionCandidateParametor
{
EnumType type; //切り替えるタイプ
int priority; //優先度
/// <summary>
/// 遷移先候補パラメータ
/// </summary>
/// <param name="type">遷移タイプ</param>
TransitionCandidateParametor(const EnumType type)
:TransitionCandidateParametor(type)
{}
/// <summary>
/// 遷移先候補パラメータ
/// </summary>
/// <param name="type">遷移タイプ</param>
/// <param name="priority">優先度</param>
TransitionCandidateParametor(const EnumType type, const int priority)
:type(type), priority(priority)
{}
};
private:
std::unique_ptr<GraphType> m_graph; //グラフタイプ
EnumType m_currentNodeType = EnumType(0); //現在使用中のノードタイプ
TransitionStructMember m_transitionStruct; //遷移条件に利用する構造体
std::vector<TransitionCandidateParametor> m_transitionCandidates; //遷移先候補パラメータ群
private:
/// <summary>
/// ステートの変更
/// </summary>
/// <param name="type">変更したいステートタイプ</param>
void ChangeState(const EnumType type) {
std::shared_ptr<NodeType> nowNode = GetCurrentNode();
if (nowNode) {
nowNode->OnExit();
}
m_currentNodeType = type;
std::shared_ptr<NodeType> nextNode = GetNode(m_currentNodeType);
if (nextNode) {
nextNode->OnStart();
}
}
/// <summary>
/// ノードの追加
/// </summary>
/// <param name="type">ノードのEnumType</param>
/// <param name="node">追加するノード</param>
void AddNode(const EnumType type, const std::shared_ptr<NodeType>& node) {
if (IsEmpty()) {
m_currentNodeType = type;
if (node) {
node->OnStart();
}
}
m_graph->AddNode(type, node);
}
public:
/// <summary>
/// コンストラクタ
/// </summary>
StateMachine():
StateMachine(TransitionStructMember())
{}
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="member">遷移条件用メンバ</param>
StateMachine(const TransitionStructMember& member) :
m_graph(new GraphType()),
m_transitionStruct(member)
{}
virtual ~StateMachine() = default;
/// <summary>
/// ノードの追加
/// </summary>
/// <param name="type">ノードのタイプ</param>
template<class T, class... Ts,
std::enable_if_t<
std::is_base_of_v<const NodeType, T> && //基底クラスの制限
std::is_constructible_v<T, const int, Ts...>, //コンストラクタの監視
std::nullptr_t> = nullptr
>
void AddNode(const EnumType type, Ts&&... params) {
int typeInt = static_cast<int>(type);
auto newNode = std::make_shared<T>(typeInt, params...);
AddNode(type, newNode);
}
/// <summary>
/// ノードの追加
/// </summary>
/// <param name="type">ノードのタイプ</param>
template<class T, class... Ts,
std::enable_if_t<
std::is_base_of_v<const NodeType, T>&& //基底クラスの制限
std::is_constructible_v<T, Ts...>, //コンストラクタの監視
std::nullptr_t> = nullptr
>
void AddNode(const EnumType type, Ts&&... params) {
auto newNode = std::make_shared<T>(params...);
newNode->SetIndex(static_cast<int>(type));
AddNode(type, newNode);
}
/// <summary>
/// エッジの追加
/// </summary>
/// <param name="edge">追加したいエッジ</param>
void AddEdge(const std::shared_ptr<EdgeType>& edge) {
m_graph->AddEdge(edge);
}
/// <summary>
/// エッジの追加
/// </summary>
/// <param name="from">元のタイプ</param>
/// <param name="to">遷移先のタイプ</param>
/// <param name="isTransitionFunc">遷移条件</param>
/// <param name="isEndTransition">終了時に遷移するかどうか</param>
void AddEdge(
const EnumType from, const EnumType to,
const std::function<bool(const TransitionStructMember& transition)>& isTransitionFunc,
const bool isEndTransition = false)
{
auto fromNode = GetNode(from);
auto toNode = GetNode(to);
auto newEdge = std::make_shared<EdgeType>(fromNode, toNode, isTransitionFunc, static_cast<int>(to), isEndTransition);
AddEdge(newEdge);
}
/// <summary>
/// 現在使用中のノードEnumTypeを取得
/// </summary>
/// <returns>ノードのEnumType</returns>
EnumType GetCurrentNodeType() const noexcept {
return m_currentNodeType;
}
/// <summary>
/// 現在使用中のノードタイプが引数のタイプと合っているかどうか
/// </summary>
/// <param name="type">確認したいノードタイプ</param>
/// <returns>同じならtrue</returns>
bool IsCurrentNodeType(const EnumType& type) const {
return GetCurrentNodeType() == type;
}
/// <summary>
/// ノードの取得
/// </summary>
/// <param name="type">ノードタイプ</param>
/// <returns>ノード</returns>
std::shared_ptr<NodeType> GetNode(const EnumType type) const {
return m_graph->GetNode(type);
}
/// <summary>
/// 現在使用中のノードを取得
/// </summary>
/// <returns>ノードの取得</returns>
std::shared_ptr<NodeType> GetCurrentNode() const {
return GetNode(m_currentNodeType);
}
/// <summary>
/// 現在使用中のノードに設定しているエッジを取得
/// </summary>
/// <returns>エッジの取得</returns>
auto GetCurrentNodeEdges() const {
return m_graph->GetEdges(m_currentNodeType);
}
/// <summary>
/// 遷移に利用する構造体を取得する。
/// </summary>
/// <returns>構造体の参照を渡す</returns>
TransitionStructMember& GetTransitionStructMember() {
return m_transitionStruct;
}
/// <summary>
/// ステートの切り替え
/// </summary>
/// <param name="type">切り替えたいステート</param>
/// <param name="priority">優先度</param>
void ChangeState(const EnumType& type, const int priority) {
m_transitionCandidates.push_back(TransitionCandidateParametor(type, priority));
}
/// <summary>
/// 強制的なステートの切り替え。
/// </summary>
/// <param name="type">強制的なステートの切り替え</param>
void ForceChangeState(const EnumType& type) {
ChangeState(type);
}
/// <summary>
/// グラフが空かどうか
/// </summary>
/// <returns>空ならtrue</returns>
bool IsEmpty() const { return m_graph->IsEmpty(); }
/// <summary>
/// 更新処理
/// </summary>
void OnUpdate() {
if (IsEmpty()) {
return;
}
//ノードのUpdate
bool isEndNodeUpdate = NodeUpdate();
//エッジの切替判断
TransitionCheck(isEndNodeUpdate);
//トリガーのリセット
TriggerReset();
//遷移
Transition();
//遷移先候補のクリア
m_transitionCandidates.clear();
}
private:
/// <summary>
/// ノードのアップデート
/// </summary>
/// <returns>ノードが終了しているかどうか</returns>
bool NodeUpdate() {
auto node = GetCurrentNode();
if (node) {
return node->OnUpdate();
}
return false;
}
/// <summary>
/// 遷移チェック
/// </summary>
void TransitionCheck(const bool isEndNodeUpdate) {
auto edges = GetCurrentNodeEdges();
for (auto& edge : edges) {
if (edge->IsTransition(m_transitionStruct, isEndNodeUpdate)) {
const auto toType = edge->GetToType();
const auto priority = edge->GetPriority();
m_transitionCandidates.push_back(TransitionCandidateParametor(toType, priority));
}
}
}
/// <summary>
/// トリガーのリセット
/// </summary>
void TriggerReset() {
auto edgesMap = m_graph->GetEdgesMap();
for (auto& edgePair : edgesMap) {
for (auto& edge : edgePair.second) {
edge->IsTransition(m_transitionStruct);
}
}
}
/// <summary>
/// 遷移処理
/// </summary>
void Transition() {
if (m_transitionCandidates.size() == 0) { //遷移先候補が0なら処理を飛ばす。
return;
}
//遷移先候補のソート
std::sort(m_transitionCandidates.begin(), m_transitionCandidates.end(),
[](const TransitionCandidateParametor& left, const TransitionCandidateParametor& right)
{ return left.priority > right.priority ? true : false; }
); //優先度が高い順から遷移する。
ChangeState(m_transitionCandidates[0].type);
}
};
ステートマシンを扱う基底クラス
ステートマシンを生成して扱う基底クラスです。
このクラスでラッピングすることで、ステートマシンの内部構造を意識せずに使うことが可能になります。
基本的に使いたいオブジェクトに合わせて、このクラスを継承して実装します。
//--------------------------------------------------------------------------------------
/// 前方宣言
//--------------------------------------------------------------------------------------
namespace maru {
template<class EnumType, class OwnerType, class TransitionStructMember>
class StateMachine;
}
//--------------------------------------------------------------------------------------
/// ステーターの基底クラス
//--------------------------------------------------------------------------------------
template<class NodeType, class EnumType, class TransitionMember>
class StatorBase : public Component
{
public:
using StateMachine = maru::StateMachine<NodeType, EnumType, TransitionMember>;
protected:
std::unique_ptr<StateMachine> m_stateMachine = nullptr; //ステートマシン
public:
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="objPtr">このクラスを所有するゲームオブジェクト</param>
StatorBase(const std::shared_ptr<GameObject>& objPtr)
: StatorBase(objPtr, TransitionMember())
{}
/// <summary>
/// コンストラクタ
/// </summary>
/// <param name="objPtr">このクラスを所有するゲームオブジェクト</param>
/// <param name="member">遷移メンバー</param>
StatorBase(const std::shared_ptr<GameObject>& objPtr, const TransitionMember& member)
: Component(objPtr), m_stateMachine(new StateMachine(member))
{}
virtual ~StatorBase() = default;
protected:
/// <summary>
/// ノードの生成
/// </summary>
virtual void CreateNode() = 0;
/// <summary>
/// エッジの生成
/// </summary>
virtual void CreateEdge() = 0;
public:
void OnCreate() override final{
CreateNode();
CreateEdge();
}
void OnUpdate() override final{
m_stateMachine->OnUpdate();
}
//--------------------------------------------------------------------------------------
/// アクセッサ
//--------------------------------------------------------------------------------------
/// <summary>
/// ステートの変更
/// </summary>
/// <param name="state">変更したいステート</param>
/// <param name="priority">優先度</param>
void ChangeState(const EnumType& state, const int& priority = 0) override final{
m_stateMachine->ChangeState(state, priority);
}
/// <summary>
/// ステートの強制変更
/// </summary>
/// <param name="state">変更したいステート</param>
void ForceChangeState(const EnumType state) {
m_stateMachine->ForceChangeState(state);
}
/// <summary>
/// 現在のステートタイプを取得
/// </summary>
/// <returns>現在のステートタイプ</returns>
EnumType GetCurrentState() const override final{
return m_stateMachine->GetCurrentNodeType();
}
/// <summary>
/// 指定したステートの状態かどうかを返す。
/// </summary>
/// <param name="type">確認したいステート</param>
/// <returns>指定したステートならtrue</returns>
bool IsCurrentState(const EnumType& type) const override final {
return m_stateMachine->IsCurrentNodeType(type);
}
/// <summary>
/// 遷移条件メンバーを取得する。
/// </summary>
/// <returns>遷移条件メンバー</returns>
TransitionMember& GetTransitionMember() const override final{
return m_stateMachine->GetTransitionStructMember();
}
};
ステートマシンが扱うグラフクラス
ステートマシンで扱うグラフクラスです。
汎用的なグラフクラスがあると、ステートマシン以外でも活用することができるため、作業効率が上がります。
//--------------------------------------------------------------------------------------
/// 前方宣言
//--------------------------------------------------------------------------------------
class NodeBase;
class EdgeBase;
//--------------------------------------------------------------------------------------
/// グラフの基底クラス
//--------------------------------------------------------------------------------------
template<class EnumType, class NodeType, class EdgeType,
std::enable_if_t<
std::is_base_of_v<NodeBase, NodeType> && //NodeTypeがNodeBaseを継承していることを保証する
std::is_base_of_v<EdgeBase, EdgeType>, //EdgeTypeがEdgeBaseを継承していることを保証する
std::nullptr_t
> = nullptr>
class GraphBase
{
public:
//usingディレクティブ
using NodeMap = std::map<EnumType, std::shared_ptr<NodeType>>;
using EdgeVector = std::vector<std::shared_ptr<EdgeType>>;
using EdgeVectorMap = std::map<EnumType, EdgeVector>;
private:
NodeMap m_nodeMap; //ノードを格納する配列
EdgeVectorMap m_edgesMap; //エッジを格納する配列
bool m_isActive = true; //アクティブかどうか
public:
GraphBase() = default;
virtual ~GraphBase() = default;
/// <summary>
/// 指定したノードの取得
/// </summary>
/// <param name="type">ノードタイプ</param>
/// <returns>指定したノード</returns>
std::shared_ptr<NodeType> GetNode(const EnumType type) const {
return m_nodeMap.at(type);
}
/// <summary>
/// ノードの配列を取得
/// </summary>
/// <returns>ノード配列</returns>
NodeMap GetNodes() const {
return m_nodeMap;
}
/// <summary>
/// 指定したノードを連結するエッジの取得
/// </summary>
/// <param name="from">手前のインデックス</param>
/// <param name="to">先のインデックス</param>
/// <returns>指定したノードを連結するエッジの取得</returns>
std::shared_ptr<EdgeType> GetEdge(const EnumType from, const EnumType to) const {
//存在しなかったらnullptrを返す。
if (m_edgesMap.count[from] == 0) {
return nullptr;
}
auto edges = m_edgesMap[from];
for (std::shared_ptr<EdgeBase>& edge : edges) {
if (edge->GetToType<EnumType>() == to) {
return edge;
}
}
return nullptr;
}
/// <summary>
/// 指定したノードから伸びるエッジ配列の取得
/// </summary>
/// <param name="from">取得したいノード</param>
/// <returns>指定したノードから伸びるエッジ配列</returns>
EdgeVector GetEdges(const EnumType from) const {
if (m_edgesMap.count(from) == 0) {
return EdgeVector();
}
return m_edgesMap.at(from);
}
/// <summary>
/// エッジ連想配列を取得
/// </summary>
/// <returns>エッジ連想配列</returns>
EdgeVectorMap GetEdgesMap() const {
return m_edgesMap;
}
/// <summary>
/// ノードの追加
/// </summary>
/// <param name="type">ノードのタイプ</param>
/// <param name="node">ノードのポインタ</param>
virtual void AddNode(const EnumType type, const std::shared_ptr<NodeType>& node) {
m_nodeMap[type] = node;
}
/// <summary>
/// ノードの削除
/// </summary>
/// <param name="node">削除したいノード</param>
void RemoveNode(const std::shared_ptr<NodeType>& node) {
//maru::MyUtility::RemoveVec(m_nodeMap, node);
}
/// <summary>
/// エッジの追加
/// </summary>
/// <param name="edge">追加したいエッジ</param>
void AddEdge(const std::shared_ptr<EdgeType>& edge) {
auto type = static_cast<EnumType>(edge->GetFromIndex());
m_edgesMap[type].push_back(edge);
}
/// <summary>
/// エッジの追加
/// </summary>
/// <param name="from">元のタイプ</param>
/// <param name="to">遷移先のタイプ</param>
/// <param name="isTransitionFunc">遷移条件</param>
/// <param name="isEndTransition">終了時に遷移するかどうか</param>
template<class... Ts,
std::enable_if_t<
std::is_constructible_v<EdgeType, std::shared_ptr<NodeBase>&, std::shared_ptr<NodeBase>&, Ts...>, //コンストラクタの引数の整合性を保証する
std::nullptr_t> = nullptr
>
void AddEdge(const EnumType fromType, const EnumType toType, Ts&&... params)
{
auto newEdge = std::make_shared<EdgeType>(GetNode(fromType), GetNode(toType), params...);
AddEdge(newEdge);
}
/// <summary>
/// ノードの数の取得
/// </summary>
/// <returns>ノードの数</returns>
int GetNumNode() const {
return static_cast<int>(m_nodeMap.size());
}
/// <summary>
/// エッジの数の取得
/// </summary>
/// <param name="from">どのノードから伸びるエッジか指定</param>
/// <returns>エッジの数</returns>
int GetNumEdge(const EnumType from) const {
return static_cast<int>(m_edgesMap.count(from));
}
/// <summary>
/// エッジ連想配列の数を取得
/// </summary>
/// <returns>エッジの連想配列</returns>
int GetNumEdgeMap() const {
return static_cast<int>(m_edgesMap.size());
}
/// <summary>
/// アクティブなグラフかどうか
/// </summary>
/// <returns>アクティブなグラフならtrue</returns>
bool IsActive() {
return m_isActive;
}
/// <summary>
/// グラフのアクティブ状態を設定
/// </summary>
/// <param name="isActive">グラフのアクティブ状態</param>
void SetActive(const bool isActive) {
m_isActive = isActive;
}
/// <summary>
/// ノードの有無を判断
/// </summary>
/// <returns>ノードが一つも存在しないのならばtrue</returns>
bool IsEmpty() const {
return static_cast<int>(m_nodeMap.size()) == 0 ? true : false;
}
};
使用例
このステートマシンの使用例になります。
まず初めにStaorBaseを継承したStatorクラスを定義します。
そこでステートのノードとエッジを設定します。
//--------------------------------------------------------------------------------------
/// AIPlayerStatorのステートタイプ
//--------------------------------------------------------------------------------------
enum class AIPlayerStator_StateType {
None,
Patrol, //パトロール
Buttle, //バトル
Goal, //ゴール中
Dyning, //死亡中
Dead, //死亡
};
//--------------------------------------------------------------------------------------
/// AIPlayerStatorの遷移条件メンバー
//--------------------------------------------------------------------------------------
struct AIPlayerStator_TransitionMember {
float patrolEeyRange; //パトロールしているときの視界範囲
float buttleStartEyeRange; //バトル開始時の視界範囲
AIPlayerStator_TransitionMember();
};
//--------------------------------------------------------------------------------------
/// AIPlayerStator
//--------------------------------------------------------------------------------------
class AIPlayerStator : public StatorBase<EnemyBase, AIPlayerStator_StateType, AIPlayerStator_TransitionMember>
{
public:
using StateType = AIPlayerStator_StateType;
using TransitionMember = AIPlayerStator_TransitionMember;
public:
AIPlayerStator(const std::shared_ptr<GameObject>& objPtr);
virtual ~AIPlayerStator() = default;
//ノードの生成
void CreateNode() override {
auto enemy = GetGameObject()->GetComponent<EnemyBase>();
//None状態
m_stateMachine->AddNode<maru::StateNode::EmptyNode<Enemy::EnemyBase>>(StateType::None, enemy);
//パトロール状態
m_stateMachine->AddNode<StateNode::Patrol>(StateType::Patrol, enemy);
//バトル
m_stateMachine->AddNode<StateNode::Buttle>(StateType::Buttle, enemy);
//ゴール中
m_stateMachine->AddNode<StateNode::Goal>(StateType::Goal, enemy);
//死亡中
m_stateMachine->AddNode<StateNode::Dyning>(StateType::Dyning, enemy);
//死亡
m_stateMachine->AddNode<StateNode::Dead>(StateType::Dead, enemy);
}
//エッジの生成
void CreateEdge() override {
auto enemy = GetGameObject()->GetComponent<EnemyBase>();
//None
m_stateMachine->AddEdge(StateType::None, StateType::Patrol, &IsGameState);
//パトロールからバトルに遷移
m_stateMachine->AddEdge(
StateType::Patrol,
StateType::Buttle,
[&](const TransitionMember& member) { return IsFindButtleTarget(member); }
);
//バトルからパトロールに遷移
m_stateMachine->AddEdge(
StateType::Buttle,
StateType::Patrol,
[&](const TransitionMember& member) { return IsLostButtleTarget(member); }
);
//ゴールからパトロールへ遷移
m_stateMachine->AddEdge(
StateType::Goal,
StateType::Patrol,
[](const TransitionMember& member) { return true; },
true
);
}
private:
//--------------------------------------------------------------------------------------
/// 遷移条件系
//--------------------------------------------------------------------------------------
/// <summary>
/// バトルターゲット発見
/// </summary>
/// <param name="member">遷移条件メンバー</param>
/// <returns>バトルターゲット</returns>
bool IsFindButtleTarget(const TransitionMember& member);
/// <summary>
/// ターゲットを見失ったときの遷移
/// </summary>
/// <param name="member">遷移条件メンバー</param>
/// <returns>ターゲットを見失ったらtrue</returns>
bool IsLostButtleTarget(const TransitionMember& member){
//見失ったメッセージを受け取る
auto tupleSpace = m_tupler.lock()->GetTupleSpace();
auto patrolTransition = tupleSpace->Take<Tuple::PatrolTransition>();
if (patrolTransition) {
return true;
}
return false;
}
};
上記で生成したAIPlayerStatorをゲームオブジェクトにアタッチすれば、状態管理をすることができます。
void PlayerObject::OnCreate() {
AddComponent<AIPlayerStator>();
}
最後に
C++でのステートマシンの実装でした。
AI制作の助けに少しでもなれば幸いです。
StateMachineとStatorはfriendクラスにすることで、それ以外のクラスからUpdateの呼び出しを禁止するのも良いかもしれません。
AI制作に力を入れており、ステートマシン以外の記事も書いています。
下記にURLをまとめているので、ぜひご覧ください。