はじめに
すみません(初手謝罪)。本当は今年OpenSiv3D v0.6.4で組み込まれたMultiplayer_Photonのサンプルでも書こうと思ってたのですが、間に合わなかったので別の記事になりました...
BehaviorTreeのソースコード
BehaviorTree.hpp
#pragma once
namespace BehaviorTree
{
// ノードの実行状態
enum class NodeState
{
Running,
Success,
Failure
};
// Treeを構成する要素
class Node : public std::enable_shared_from_this<Node>
{
public:
Node();
/// @brief リストを引数として、まとめて子ノードとしてアタッチする
/// @param children 子ノードとして登録するノード一覧
Node(const Array<std::shared_ptr<Node>>& children);
virtual ~Node() = default;
// 初期化
virtual void init();
/// @brief ノードの状態を評価する
/// @return ノードの実行状態
/// @remark アクションノードなら、行動及び実行条件を記述
virtual NodeState evaluateState();
// 親ノードを取得
std::shared_ptr<Node> getParent() const;
// 親ノードを設定
void setParent(const std::weak_ptr<Node>& parent);
protected:
// このノードの実行状態
NodeState m_state;
// 子ノード
Array<std::shared_ptr<Node>> m_children;
private:
// 現在のノードに子ノードを追加する
void attachNode(const std::shared_ptr<Node>& node);
// 親ノード
std::weak_ptr<Node> m_parent;
};
// 子ノードのうち、いずれか一つを実行する為のノード
class Selector : public Node
{
public:
Selector();
Selector(const Array<std::shared_ptr<Node>>& children);
virtual ~Selector() = default;
NodeState evaluateState() override;
};
// 子ノードを順番に実行する為のノード
class Sequence : public Node
{
public:
Sequence();
Sequence(const Array<std::shared_ptr<Node>>& children);
virtual ~Sequence() = default;
NodeState evaluateState() override;
};
/// @brief 各キャラクターAIの実態となるTree
/// @remark ここでNodeを実行する
class Tree
{
public:
Tree() = default;
/// @brief ルートからTreeを実行する
void update();
/// @brief 木構造の構築
/// @return ルートノード
/// @remark 継承先で実装される
virtual void setupTree() = 0;
protected:
std::shared_ptr<Node> m_root = nullptr;
};
}
BehaviorTree.cpp
#include "BehaviorTree.hpp"
namespace BehaviorTree
{
Node::Node()
: enable_shared_from_this()
, m_state{ NodeState::Running }
{
}
Node::Node(const Array<std::shared_ptr<Node>>& children)
: enable_shared_from_this()
, m_state{ NodeState::Running }
{
for (const auto& child : children)
{
m_children.emplace_back(child);
}
}
void Node::init()
{
for (const auto& child : m_children)
{
attachNode(child);
child->init();
}
}
NodeState Node::evaluateState()
{
return NodeState::Failure;
}
std::shared_ptr<Node> Node::getParent() const
{
return m_parent.lock();
}
void Node::setParent(const std::weak_ptr<Node>& parent)
{
m_parent = parent;
}
void Node::attachNode(const std::shared_ptr<Node>& node)
{
node->setParent(weak_from_this());
}
Selector::Selector()
: Node{}
{
}
Selector::Selector(const Array<std::shared_ptr<Node>>& children)
: Node{ children }
{
}
NodeState Selector::evaluateState()
{
// 全子ノードを評価
for (const auto& child : m_children)
{
switch (child->evaluateState())
{
// 子ノードが実行中なら、その時点で評価終了
case NodeState::Running:
m_state = NodeState::Running;
return m_state;
// 子ノードが成功なら、その時点で評価終了
case NodeState::Success:
m_state = NodeState::Success;
return m_state;
// 子ノードが失敗もしくは何も返ってこないなら、続行
default:
continue;
}
}
// 失敗
m_state = NodeState::Failure;
return m_state;
}
Sequence::Sequence()
: Node{}
{
}
Sequence::Sequence(const Array<std::shared_ptr<Node>>& children)
: Node{ children }
{
}
NodeState Sequence::evaluateState()
{
// どれか一つでも子ノードが実行中だったか
bool isRunningAnyChild = false;
// 全子ノードを評価
for (const auto& child : m_children)
{
switch (child->evaluateState())
{
// 子ノードが実行中なら、フラグをtrueにして続行
case NodeState::Running:
isRunningAnyChild = true;
continue;
// 子ノードが成功なら、評価続行
case NodeState::Success:
continue;
// 子ノードが一つでも失敗なら、これ以降実行できないので失敗
case NodeState::Failure:
m_state = NodeState::Failure;
return m_state;
// 何も返ってこないなら、子ノードがないので終了
default:
m_state = NodeState::Success;
return m_state;
}
}
// 子ノードを全て探索したら、フラグから実行中か失敗かを評価する
m_state = isRunningAnyChild ? NodeState::Running : NodeState::Failure;
return m_state;
}
void Tree::update()
{
if (m_root)
{
m_root->evaluateState();
}
}
}
サンプル
展開するとサンプルソースコードが開きます。
Test.hpp
#pragma once
#include "BehaviorTree.hpp"
class EnemyBT : public BehaviorTree::Tree, public std::enable_shared_from_this<EnemyBT>
{
public:
EnemyBT(const Vec2& pos);
void changeColor(const ColorF& color);
constexpr Vec2 getPos() const;
void setPos(const Vec2& pos);
void draw() const;
void setupTree() override;
private:
Vec2 m_pos;
ColorF m_color;
Triangle m_wayPoints;
};
class CheckEnemyInRange : public BehaviorTree::Node
{
public:
CheckEnemyInRange(const std::weak_ptr<EnemyBT>& root);
BehaviorTree::NodeState evaluateState() override;
private:
std::weak_ptr<EnemyBT> m_root;
};
class TaskGoToTarget : public BehaviorTree::Node
{
public:
TaskGoToTarget(const std::weak_ptr<EnemyBT>& root);
BehaviorTree::NodeState evaluateState() override;
private:
std::weak_ptr<EnemyBT> m_root;
static constexpr float m_smoothTime = 0.01f;
Vec2 m_velocity;
};
class TaskPatrol : public BehaviorTree::Node
{
public:
TaskPatrol(const std::weak_ptr<EnemyBT>& root, const Triangle& wayPoints);
BehaviorTree::NodeState evaluateState() override;
private:
std::weak_ptr<EnemyBT> m_root;
Triangle m_wayPoints;
Vec2 m_velocity;
int m_currentWayPointIndex;
float m_waitTimeCounter;
bool m_isWaiting;
static constexpr float m_waitTime = 1.f;
static constexpr float m_smoothTime = 0.01f;
};
Test.cpp
#pragma once
#include "BehaviorTree.hpp"
class EnemyBT : public BehaviorTree::Tree, public std::enable_shared_from_this<EnemyBT>
{
public:
EnemyBT(const Vec2& pos);
void changeColor(const ColorF& color);
constexpr Vec2 getPos() const;
void setPos(const Vec2& pos);
void draw() const;
void setupTree() override;
private:
Vec2 m_pos;
ColorF m_color;
Triangle m_wayPoints;
};
class CheckEnemyInRange : public BehaviorTree::Node
{
public:
CheckEnemyInRange(const std::weak_ptr<EnemyBT>& root);
BehaviorTree::NodeState evaluateState() override;
private:
std::weak_ptr<EnemyBT> m_root;
};
class TaskGoToTarget : public BehaviorTree::Node
{
public:
TaskGoToTarget(const std::weak_ptr<EnemyBT>& root);
BehaviorTree::NodeState evaluateState() override;
private:
std::weak_ptr<EnemyBT> m_root;
static constexpr float m_smoothTime = 0.01f;
Vec2 m_velocity;
};
class TaskPatrol : public BehaviorTree::Node
{
public:
TaskPatrol(const std::weak_ptr<EnemyBT>& root, const Triangle& wayPoints);
BehaviorTree::NodeState evaluateState() override;
private:
std::weak_ptr<EnemyBT> m_root;
Triangle m_wayPoints;
Vec2 m_velocity;
int m_currentWayPointIndex;
float m_waitTimeCounter;
bool m_isWaiting;
static constexpr float m_waitTime = 1.f;
static constexpr float m_smoothTime = 0.01f;
};
Main.cpp
# include "Test.h"
EnemyBT::EnemyBT(const Vec2& pos)
: Tree{}
, enable_shared_from_this{}
, m_pos{ pos }
, m_color{ Palette::White }
, m_wayPoints{ Triangle{ Scene::CenterF(), 200 }.rotated(90_deg) }
{
}
void EnemyBT::changeColor(const ColorF& color)
{
m_color = color;
}
constexpr Vec2 EnemyBT::getPos() const
{
return m_pos;
}
void EnemyBT::setPos(const Vec2& pos)
{
m_pos = pos;
}
void EnemyBT::draw() const
{
Print << m_pos;
m_wayPoints.drawFrame(2, Palette::Lightgreen);
Circle{ m_pos, 50 }.draw(m_color);
}
void EnemyBT::setupTree()
{
using namespace BehaviorTree;
auto weak = weak_from_this();
m_root = std::make_shared<Selector>(Array<std::shared_ptr<Node>>
{
std::make_shared<Sequence>(Array<std::shared_ptr<Node>>
{
std::make_shared<CheckEnemyInRange>(weak_from_this()),
std::make_shared<TaskGoToTarget>(weak_from_this())
}),
std::make_shared<TaskPatrol>(weak_from_this(), m_wayPoints)
});
m_root->init();
}
CheckEnemyInRange::CheckEnemyInRange(const std::weak_ptr<EnemyBT>& root)
: Node{}
, m_root{ root }
{
}
BehaviorTree::NodeState CheckEnemyInRange::evaluateState()
{
if (m_root.expired())
{
return BehaviorTree::NodeState::Failure;
}
if (const auto pos = m_root.lock()->getPos(); pos.distanceFrom(Cursor::PosF()) <= 300)
{
ClearPrint();
Print << U"CheckEnemyInRange";
return BehaviorTree::NodeState::Success;
}
return BehaviorTree::NodeState::Failure;
}
TaskGoToTarget::TaskGoToTarget(const std::weak_ptr<EnemyBT>& root)
: Node{}
, m_root{ root }
, m_velocity{ Vec2::Zero() }
{
}
BehaviorTree::NodeState TaskGoToTarget::evaluateState()
{
if (m_root.expired())
{
return BehaviorTree::NodeState::Failure;
}
Print << U"TaskGoToTarget";
auto enemyBT = m_root.lock();
if (const auto pos = enemyBT->getPos(); pos.distanceFromSq(Cursor::PosF()) > 0.01f)
{
enemyBT->setPos(Math::SmoothDamp(pos, Cursor::PosF(), m_velocity, m_smoothTime, 200, Scene::DeltaTime()));
// デバッグでプレイヤーとの線を描画
Line{ pos, Cursor::PosF() }.draw(Palette::Orange);
}
return BehaviorTree::NodeState::Running;
}
TaskPatrol::TaskPatrol(const std::weak_ptr<EnemyBT>& root, const Triangle& wayPoints)
: Node{}
, m_root{ root }
, m_wayPoints{ wayPoints }
, m_velocity{ Vec2::Zero() }
, m_currentWayPointIndex{}
, m_waitTimeCounter{}
, m_isWaiting{ false }
{
}
BehaviorTree::NodeState TaskPatrol::evaluateState()
{
if (m_root.expired())
{
return BehaviorTree::NodeState::Failure;
}
ClearPrint();
Print << U"TaskPatrol";
if (m_isWaiting)
{
m_waitTimeCounter += Scene::DeltaTime();
if (m_waitTimeCounter >= m_waitTime)
{
m_isWaiting = false;
}
}
else
{
auto enemyBT = m_root.lock();
const Vec2 wp = m_wayPoints.p(m_currentWayPointIndex);
if (const auto pos = enemyBT->getPos(); wp.distanceFromSq(pos) <= 0.01f)
{
enemyBT->setPos(wp);
m_waitTimeCounter = 0.f;
m_isWaiting = true;
constexpr int trianglePoints = 3;
m_currentWayPointIndex = (m_currentWayPointIndex + 1) % trianglePoints;
}
else
{
enemyBT->setPos(Math::SmoothDamp(pos, wp, m_velocity, m_smoothTime, 100, Scene::DeltaTime()));
}
}
return BehaviorTree::NodeState::Running;
}
void Main()
{
auto bt = std::make_shared<EnemyBT>(Scene::CenterF().moveBy(-50, 0));
bt->setupTree();
Font font(50);
while (System::Update())
{
bt->update();
bt->draw();
// Player描画
Circle(Cursor::PosF(), 50).draw(Palette::Red);
font(U"E").drawAt(bt->getPos(), Palette::Green);
font(U"P").drawAt(Cursor::PosF(), Palette::Green);
}
}
サンプル映像
おわりに
こんな感じで実装してみました。今回はSelectorノードとSequenceノードを実装してみましたが、この二つのクラスを参考にしながら様々なノードタイプを作ってみてください。私も時間がありましたらもう少し使いやすいように作り直すかもしれません。