This project demonstrates a modular behavior tree system developed for AI behavior in a custom C++ engine using an ECS-based architecture.
The goal of the system was to create a scalable and reusable way to define AI behavior, while also enabling designers to create and modify behaviors without writing code.
The behavior tree system is designed to control AI behavior through modular and reusable nodes.
Each entity has a behavior tree component, which is processed every frame by a system that evaluates the tree and applies results by adding or modifying components on the entity.
These components are then handled by other gameplay systems, allowing for separation of logic and clean interaction between systems.
Behavior is structured using nodes such as selectors and action nodes. For example, a selector node evaluates behaviors in priority order (e.x, Attack -> Chase -> Idle), allowing flexible and scalable decision-making.
Nodes are implemented in code and registered to be available in the editor. Designers can then construct behavior trees visually by combining these nodes.
class BTNodeBase
{
public:
virtual ~BTNodeBase()
{
}
virtual void Init(const BTCreationContext& aContext) = 0;
virtual std::unique_ptr<BTNodeRuntimeInstanceBase> CreateRuntimeInstanceData() const
{
return nullptr;
}
virtual bool IsRootNode() const
{
return false;
}
virtual void LoadFromJson([[maybe_unused]] const JsonData& aJsonData)
{
}
virtual void WriteToJson([[maybe_unused]] JsonData& aJsonData) const
{
}
virtual BTNodeResult Execute(BehaviourTreeUpdateContext& aContext)
{
aContext;
return BTNodeResult::Success;
}
};
class SelectorNode : public BTNodeBase
{
BTPinId myParentPin;
std::vector<BTPinId> myChildrenPins;
public:
void Init(const BTCreationContext& aContext) override
{
{
BTPin flowOutPin = {};
flowOutPin.type = BTLinkType::Link;
flowOutPin.name = "Parent"_tgaid;
flowOutPin.node = aContext.GetNodeID();
flowOutPin.role = BTPinRole::Parent;
myParentPin = aContext.FindOrCreatePin(flowOutPin);
}
for (unsigned i = 0; i < 4; ++i)
{
{
BTPin flowOutPin = {};
flowOutPin.type = BTLinkType::Link;
flowOutPin.name = StringRegistry::RegisterOrGetString(WASD::CombineStrings("Child_", std::to_string(i)));
flowOutPin.node = aContext.GetNodeID();
flowOutPin.sortingNumber = i;
flowOutPin.role = BTPinRole::Child;
myChildrenPins.emplace_back(aContext.FindOrCreatePin(flowOutPin));
}
}
}
BTNodeResult Execute(BehaviourTreeUpdateContext& aContext) override
{
for (auto& childPin : myChildrenPins)
{
BehaviourTree& tree = *aContext.tree;
std::vector<BTLinkId> links = tree.GetConnectedLinks(childPin);
if (links.size() != 1)
{
continue;
}
const BTLink& link = tree.GetLink(links.front());
const BTPin& pin = tree.GetPin(link.targetPinId);
BTNodeBase& nextNode = tree.GetNode(pin.node);
if (auto output = nextNode.Execute(aContext);
output != BTNodeResult::Failed)
{
return output;
}
}
return BTNodeResult::Failed;
}
};