0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C++で簡単なBehaviorTreeを実装してみた(後編)

Posted at

目次

成果物

・ツリーをエディターから作成している様子

CreateTree.gif

・ゲーム内でツリーを読み込んでいる様子

ReadingTree.gif

開発環境

・Visual Studio 2022
・C++17

使用させていただいたライブラリ

ImGui
https://github.com/ocornut/imgui

ImNodes
https://github.com/Nelarius/imnodes

json
https://github.com/nlohmann/json

nameof
https://github.com/Neargye/nameof

概要

前半の記事でも言及した通り、今回の記事ではビヘイビアツリーエディターを実装していきます。
エディターで作成したツリーをJSON形式で保存し、前回作成したシステムに読み込ませて挙動を確認できるまでを目標としています。

ノードの構造体

エディター上で管理されるノードの構造体は下記のように定義しています。

struct BTNode {
	int id = 0;                             // 固有ID
	NodeType type = NodeType::Leaf;         // ノードのタイプ
	NodeName name = NodeName::WaitLeaf;     // ノードの名前
	std::vector<int> children;              // 子ノードID
	int parent = -1;                        // 親ノードID

	// Branch用の変数
	int true_child = -1;
	int false_child = -1;

	// Wait用の変数
	float wait_time = -1.f;

	// CheckFarPlayer, CheckNearPlayer用の変数
	float limit_distance = -1.f;

	// エディター上での位置
	float pos_x = 0.f;
	float pos_y = 0.f;

    // 名前をstring型で取得
    std::string GetString() const {
		return std::string(NAMEOF_ENUM(name));
	}
};

ファイルの入出力

上で定義したBTNodeの各要素をJsonで書き出しています。
また、SequenceノードやSelectorノードの子ノードの実行優先順は、エディター上のy座標の高さで決定しています。

void BehaviorTreeGraph::export_json(const std::string& file_name) {
	using std::swap;

	const int cJsonIndent = 4;

	json j;

	for (const auto& pair : mNodes)
	{
		const BTNode& node = pair.second;
		json node_json;
		node_json["id"] = node.id;
		node_json["type"] = static_cast<int>(node.type);
		node_json["name"] = NAMEOF_ENUM(node.name);
		node_json["parent"] = node.parent;

		if (node.type == NodeType::Branch)
		{
			node_json["true_child"] = node.true_child;
			node_json["false_child"] = node.false_child;
		}
		else
		{
			// 書き出しする前に、y座標が小さい順に並び変える
			auto children = node.children;

			std::sort(children.begin(), children.end(),
				[&](int a, int b) {
					return mNodes[a].pos_y < mNodes[b].pos_y;
				});


			node_json["children"] = children;
		}

		// ノード固有の値
		if (node.wait_time != -1.f)
		{
			node_json["wait_time"] = node.wait_time;
		}

		if (node.limit_distance != -1.f)
		{
			node_json["limit_distance"] = node.limit_distance;
		}

		node_json["pos_x"] = node.pos_x;
		node_json["pos_y"] = node.pos_y;

		j.push_back(node_json);
	}
	std::ofstream file(file_name);
	if (file.is_open()) {
		file << j.dump(cJsonIndent);
		file.close();
	}
}

また、Jsonの読み込みについては書き出しと逆に、各要素を読み込みツリーのリストに書き出す処理を行っています。

void BehaviorTreeGraph::import_json(const std::string& file_name)
{
	reset_selected();

	std::ifstream file(file_name);
	if (!file.is_open()) return;

	json j;
	file >> j;
	mNodes.clear();
	mNodeLinks.clear();
	mNextId = 1;
	for (const auto& node_json : j)
	{
		int id = node_json["id"];
		NodeType type = static_cast<NodeType>(node_json["type"]);
		NodeName name = get_matching_node_name(node_json["name"].get<std::string>());
		int parent = node_json["parent"];
		BTNode node{ id, type, name, {}, parent };
		if (type == NodeType::Branch)
		{
			node.true_child = node_json["true_child"];
			node.false_child = node_json["false_child"];
		}
		else
		{
			node.children = node_json["children"].get<std::vector<int>>();
		}
		if (node_json.contains("wait_time"))
		{
			node.wait_time = node_json["wait_time"];
		}
		if (node_json.contains("limit_distance"))
		{
			node.limit_distance = node_json["limit_distance"];
		}

		node.pos_x = node_json["pos_x"];
		node.pos_y = node_json["pos_y"];

		mNodes[id] = node;

		// リンク作成
		// childrenが空でない場合はリンク
		if (node.children.size() != 0)
		{
			for (int i = 0; i < node.children.size(); ++i)
			{
				add_link_tuple(id, node.children[i], true);
			}
		}

		// true_child, false_childが空でない場合はリンク
		if (node.true_child != -1)
		{
			add_link_tuple(id, node.true_child, true);
		}

		if (node.false_child != -1)
		{
			add_link_tuple(id, node.false_child, false);
		}
	}
	mNextId = static_cast<int>(mNodes.size()) + 1;
	file.close();
}

リアルタイム描画

今回の実装では、ビヘイビアツリーの実行中ノードを視覚的に表示するための機能が実装されています。それにより、AI動作のデバッグや挙動確認が直感的に行えるようになっています。

1. 実行中ノードの登録

実行中ノードの設定

void BehaviorTreeGraph::set_runnning_node_id(const int running_node_id)
{
    // 実行中のノード/リンクのリストを更新
    mRunningLinks.clear();
    mRunningNodes.clear();

    get_nodes_related_all_links(running_node_id, &mRunningLinks);
    get_nodes_related_all_nodes(running_node_id, &mRunningNodes);
}

このメソッドは外部からビヘイビアツリーの現在実行中のノードIDを受け取り、そのノードから親ノードへのパスに沿ったすべてのノードとリンクを特定します。

関連ノード・リンクの収集

bool BehaviorTreeGraph::get_nodes_related_all_links(const int node_id, std::vector<int>* links)
{
    int current_id = node_id;

    // ルートノードに到達するまで続ける
    while (current_id != -1)
    {
        get_nodes_related_links(current_id, links, false);

        // 親ノードへさかのぼって再度探索
        auto it = get_node(current_id);
        current_id = it.parent;
    }
    return !links->empty();
}
bool BehaviorTreeGraph::get_nodes_related_all_nodes(const int node_id, std::vector<int>* nodes)
{
    int current_id = node_id;
    nodes->push_back(current_id);

    // ルートノードに到達するまで続ける
    while (current_id != -1)
    {
        for (const auto& node : mNodes)
        {
            if (is_related_nodes(current_id, node))
            {
                nodes->push_back(node.first);
            }
        }

        // 親ノードへさかのぼって探索
        auto it = get_node(current_id);
        current_id = it.parent;
    }

    return !nodes->empty();
}

これらの関数は再帰的に親ノードをたどり、現在実行中のノードからルートノードまでのパス上にあるすべてのノードとリンクを収集します。

2. 実行中ノードの可視化

色定義

// ヘッダファイルでの定義
const ImU32 cRunningColor = IM_COL32(250, 50, 0, 255);  // 明るいオレンジ色
const ImU32 cLinkColor = IM_COL32(75, 75, 200, 200);    // 通常のリンク色

実行中のノードとリンクは、特別な色(cRunningColor)で強調表示されます。

ノードの描画

void BehaviorTreeGraph::draw_node(const BTNode& node, int node_id, bool is_selected)
{
    if (std::find(mRunningNodes.begin(), mRunningNodes.end(), node_id) != mRunningNodes.end())
    {
        ImNodes::PushColorStyle(ImNodesCol_TitleBar, cRunningColor);
    }
    else
    {
        ImNodes::PushColorStyle(ImNodesCol_TitleBar, cNodeColors.at(node.type));
    }

    ImNodes::BeginNode(node.id);
    {
        // タイトル
        draw_title(node);

        // 入力ピン
        draw_input_pin(node);

        // 出力ピン
        draw_output_pin(node, is_selected);

        // パラメータ表示
        draw_parameter(node, node_id, is_selected);
    }
    ImNodes::EndNode();

    ImNodes::PopColorStyle();
}

各ノードの描画時に、mRunningNodesリスト内にそのノードIDが存在するか確認し、存在すればcRunningColorで、そうでなければノードタイプに応じた通常の色で描画します。

リンクの描画

void BehaviorTreeGraph::draw_links()
{
    for (auto& pair : mNodeLinks)
    {
        int link_id = pair.first;
        if (std::find(mRunningLinks.begin(), mRunningLinks.end(), link_id) != mRunningLinks.end())
        {
            ImNodes::PushColorStyle(ImNodesCol_Link, cRunningColor);
        }
        else
        {
            ImNodes::PushColorStyle(ImNodesCol_Link, cLinkColor);
        }

        int parent_id = std::get<static_cast<int>(NodeTuple::Node_Id)>(pair.second);
        int child_id = std::get<static_cast<int>(NodeTuple::Child_Id)>(pair.second);
        int pin_type = std::get<static_cast<int>(NodeTuple::PinType)>(pair.second);
        if (pin_type == cTruePinBit)
        {
            ImNodes::Link(link_id, (parent_id << cInputBit | cTruePinBit), (child_id << cInputBit));
        }
        else if (pin_type == cFalsePinBit)
        {
            ImNodes::Link(link_id, (parent_id << cInputBit | cFalsePinBit), (child_id << cInputBit));
        }

        ImNodes::PopColorStyle();
    }
}

リンクの描画でも同様に、mRunningLinksリスト内にそのリンクIDが存在するか確認し、存在すればcRunningColorで、そうでなければ通常のcLinkColorで描画します。

3. リアルタイム描画の流れのまとめ

RealTimeViewer.gif

  1. 外部からの呼び出しでset_runnning_node_idが呼ばれ、現在実行中のノードIDが設定される
  2. drawメソッドが呼ばれると、draw_editorを通じてdraw_nodesdraw_linksが実行される
  3. 各ノードとリンクの描画時に、それが実行中かどうかをチェックし、適切な色で描画する
void BehaviorTreeGraph::draw()
{
    ImGui::Begin("Behavior Tree Editor");
    {
        // ツールバーの描画
        draw_toolbar();

        // ツリー描画
        draw_editor();
    }
    ImGui::End();
}

void BehaviorTreeGraph::draw_editor()
{
    ImNodes::BeginNodeEditor();
    {
        // ミニマップを描画
        ImNodes::MiniMap(0.2f, ImNodesMiniMapLocation_TopRight);
        // ノード描画
        draw_nodes();
        // リンクの描画
        draw_links();
    }
    ImNodes::EndNodeEditor();
}

ゲーム上で動かす

エディター上で作り上げたツリーのJsonを、ゲーム中で下記のように読み込ませました。


INode* BehaviourTreeBuilder::BuildAttackerTree(std::string file_path, BlackBoard* blackboard)
{
	// JSONファイルを読み込む
	std::ifstream file(file_path);
	if (!file.is_open())
	{
		throw std::runtime_error("Unable to open behavior_tree.json");
	}

	json j;
	file >> j;

	// IDからJSON要素へのマッピングを構築する
	std::unordered_map<int, json> nodeMap;
	for (auto& node : j)
	{
		int id = node["id"].get<int>();
		nodeMap[id] = node;
	}

	// 再帰的にノードを構築するラムダ関数
	std::function<INode* (int)> buildNode = [&](int nodeId) -> INode*
		{
			if (nodeMap.find(nodeId) == nodeMap.end())
			{
				throw std::runtime_error("Node id not found in JSON: " + std::to_string(nodeId));
			}
			auto nodeJson = nodeMap[nodeId];
			std::string name = nodeJson["name"].get<std::string>();

			INode* node = nullptr;

			// --- Compositeノード ---
			if (name == "Sequence")
			{
				Sequence* seq = new Sequence(blackboard);
				for (auto childId : nodeJson["children"])
				{
					seq->add_node(buildNode(childId.get<int>()));
				}
				node = seq;
			}
			else if (name == "Selector")
			{
				Selector* selector = new Selector(blackboard);
				for (auto childId : nodeJson["children"])
				{
					selector->add_node(buildNode(childId.get<int>()));
				}
				node = selector;
			}

			// --- Decoratorノード ---
			else if (name == "Inverter")
			{
				int childId = nodeJson["children"][0].get<int>();
				node = new Inverter(blackboard, buildNode(childId));
			}

			// --- Branchノード ---
			else if (name == "CheckNearPlayer")
			{
				int trueChildId = nodeJson["true_child"].get<int>();
				int falseChildId = nodeJson["false_child"].get<int>();
				float limitDistance = nodeJson["limit_distance"].get<float>();
				node = new CheckNearPlayer(blackboard, buildNode(trueChildId), buildNode(falseChildId), limitDistance);
			}
			else if (name == "CheckFarPlayer")
			{
				int trueChildId = nodeJson["true_child"].get<int>();
				int falseChildId = nodeJson["false_child"].get<int>();
				float limitDistance = nodeJson["limit_distance"].get<float>();
				node = new CheckFarPlayer(blackboard, buildNode(trueChildId), buildNode(falseChildId), limitDistance);
			}

			// --- Leafノード ---
			else if (name == "WaitLeaf")
			{
				float waitTime = nodeJson["wait_time"].get<float>();
				node = new WaitLeaf(blackboard, waitTime);
			}
			else if (name == "ChasePlayerLeaf")
			{
				node = new ChasePlayerLeaf(blackboard);
			}
			else if (name == "EscapeFromPlayerLeaf")
			{
				node = new EscapeFromPlayerLeaf(blackboard);
			}
			else if (name == "CircleAttackLeaf")
			{
				node = new CircleAttackLeaf(blackboard);
			}
			else if (name == "AlwaysSuccessLeaf")
			{
				node = new AlwaysSuccessLeaf(blackboard);
			}
			else if (name == "AlwaysFailLeaf")
			{
				node = new AlwaysFailLeaf(blackboard);
			}
			else if (name == "DebugDrawLeaf")
			{
				int text = nodeJson["text"].get<int>();
				node = new DebugDrawLeaf(blackboard, text);
			}

			// --- 未知のノード ---
			else
			{
				throw std::runtime_error("Unknown node type: " + name);
			}

			// すべてのノードにIDを設定
			if (node)
			{
				node->set_node_id(nodeId);
			}

			return node;
		};

	// ルートノードを探す (parentが-1のノード)
	int rootId = -1;
	for (auto& [id, nodeJson] : nodeMap)
	{
		if (nodeJson["parent"].get<int>() == -1)
		{
			rootId = id;
			break;
		}
	}
	if (rootId == -1)
	{
		throw std::runtime_error("Root node not found in JSON");
	}

	return buildNode(rootId);
}

JSONファイルはIDベースで各ノードをマッピングし、
ルートノードから再帰的に子ノードをインスタンス化することで、ツリー構造を復元しています。

ノード名やパラメータに応じて適切なC++クラス(Composite/Decorator/Branch/Leaf)を生成し、AIロジックを構成しており、もし定義されていないノードが見つかった場合は例外を投げるようにしています。

また、すべてのノードインスタンスにIDを割り当てているので、デバッグ時にどのノードがどのJsonに対応するか追跡しやすいです。

最終的に「親IDが-1」のノードをルートとし、そこからツリー全体を組み立てて返しています。

まとめ

エディターで直感的にツリーを設計し、そのままゲームで動作検証→改善が行えるため、AIの設計・運用サイクルもスピーディに回せるようになりました。

前半後半を通してビヘイビアツリーの実装をした結果として、ビヘイビアツリーがどんなアルゴリズムで動いているのか、そのツリーをどのように可視化させればよいのか、基礎的な知見を得ることができ、とても勉強になりました。

今後の開発でも今回得た知見を活かしていきたいと思います。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?