Fixed up GOAP support

This commit is contained in:
2026-02-04 04:45:32 +03:00
parent 4d47125ea9
commit 4fb7e94fed
2 changed files with 243 additions and 144 deletions

View File

@@ -164,16 +164,16 @@ public:
}
Blackboard actionPrereq({ jactionPrereq });
Blackboard actionEffect({ jactionEffect });
if (!prereq.stats.is_null())
if (!prereq.is_valid())
actionPrereq.apply(prereq);
m_actions.push_back(OGRE_NEW RunActionNode(
node, action, actionPrereq, actionEffect, cost));
if (effectName == "") {
std::cout << props.dump(4) << std::endl;
std::cout << "Prereq" << std::endl;
std::cout << actionPrereq.stats.dump(4) << std::endl;
actionPrereq.dump_bits();
std::cout << "Effect" << std::endl;
std::cout << actionEffect.stats.dump(4) << std::endl;
actionEffect.dump_bits();
OgreAssert(false, "action");
}
}
@@ -241,7 +241,7 @@ CharacterAIModule::CharacterAIModule(flecs::world &ecs)
2000 },
#endif
{ "EatMedicine",
{ { { "have_medicine", 1 }, { "healty", 0 } } },
{ { { "have_medicine", 1 }, { "healthy", 0 } } },
{ { { "healthy", 1 } } },
100 },
{ "UseToilet",
@@ -402,91 +402,90 @@ void CharacterAIModule::createAI(flecs::entity town)
town.add<TownAI>();
}
struct PlanTask {
Blackboard blackboard;
TownAI::goal_t goal;
TownAI::Plan plan;
TownAI::planner_t *planner;
bool operator()()
{
auto buildPlan = [this](Blackboard &blackboard,
const TownAI::goal_t &goal,
TownAI::Plan &plan) -> bool {
if (goal.is_reached(blackboard))
return false;
plan.goal = &goal;
std::vector<goap::BaseAction<Blackboard> *> path;
int actionCount = blackboard.getActionsCount();
auto actionsData = blackboard.getActionsData();
path.resize(actionCount * actionCount);
int path_length = planner->plan(
blackboard, goal, actionsData, actionCount,
path.data(), path.size());
if (path_length > 0) {
plan.goal = &goal;
plan.plan.insert(plan.plan.end(), path.begin(),
path.begin() + path_length);
return true;
}
return false;
};
return buildPlan(blackboard, goal, plan);
}
PlanTask(Blackboard &blackboard, const TownAI::goal_t &goal,
TownAI::planner_t *planner)
: blackboard(blackboard)
, goal(goal)
, planner(planner)
{
}
};
static std::deque<PlanTask> plan_tasks;
void CharacterAIModule::buildPlans(flecs::entity town, const TownNPCs &npcs,
TownAI &ai)
{
OgreAssert(town.is_valid(), "Bad town entity");
std::lock_guard<std::mutex> lock(*ai.mutex);
auto planner = ai.planner;
auto buildPlan = [planner](Blackboard &blackboard,
const TownAI::goal_t &goal,
TownAI::Plan &plan) -> bool {
if (goal.is_reached(blackboard))
return false;
plan.goal = &goal;
std::vector<goap::BaseAction<Blackboard> *> path;
int actionCount = blackboard.getActionsCount();
auto actionsData = blackboard.getActionsData();
path.resize(actionCount * actionCount);
int path_length = planner->plan(blackboard, goal, actionsData,
actionCount, path.data(),
path.size());
if (path_length > 0) {
plan.goal = &goal;
plan.plan.insert(plan.plan.end(), path.begin(),
path.begin() + path_length);
return true;
}
return false;
};
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
auto &bb = ai.blackboards.at(it->first);
/* if there are plans, skip until these get discarded */
if (ai.plans.find(it->first) != ai.plans.end() &&
ai.plans.at(it->first).size() > 0)
continue;
const auto &npc = npcs.npcs.at(it->first);
int index = it->first;
ai.plans[index] = {};
for (const auto &goal : ai.goals) {
struct TownAI::Plan plan;
bool created = buildPlan(bb, goal, plan);
#if 0
std::cout << "blackboard: "
<< bb.stats.dump(4)
<< std::endl;
std::cout << "goal: "
<< goal.goal.stats.dump(4)
<< std::endl;
#endif
#if 0
std::cout << "Actions: " << std::endl;
for (auto &action : actions) {
std::cout << "name: "
<< action->get_name()
<< std::endl;
std::cout
<< "\tprereq:\n"
<< action->prereq.stats
.dump(4)
<< std::endl;
std::cout
<< "\teffects:\n"
<< action->effects.stats
.dump(4)
<< std::endl;
}
#endif
#if 1
if (created) {
std::cout << bb.index << " ";
std::cout << "Goal: " << goal.get_name();
std::cout << std::endl;
std::cout << "Path: ";
for (auto &action : plan.plan) {
OgreAssert(action, "No action");
std::cout << action->get_name() + " ";
}
std::cout << " size: " << plan.plan.size()
<< std::endl;
ai.plans[it->first].push_back(plan);
OgreAssert(false, "plan");
if (plan_tasks.size() > 0) {
bool created = (plan_tasks.front())();
if (created) {
std::cout << plan_tasks.front().blackboard.index << " ";
std::cout << "Goal: "
<< plan_tasks.front().goal.get_name();
plan_tasks.front().goal.goal.dump_bits();
std::cout << std::endl;
std::cout << "Path: ";
for (auto &action : plan_tasks.front().plan.plan) {
OgreAssert(action, "No action");
std::cout << action->get_name() + " ";
}
#endif
std::cout << " size: "
<< plan_tasks.front().plan.plan.size()
<< std::endl;
ai.plans[plan_tasks.front().blackboard.index].push_back(
plan_tasks.front().plan);
// OgreAssert(false, "plan...");
}
plan_tasks.pop_front();
} else
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) ==
ai.blackboards.end())
continue;
auto &bb = ai.blackboards.at(it->first);
/* if there are plans, skip until these get discarded */
if (ai.plans.find(it->first) != ai.plans.end() &&
ai.plans.at(it->first).size() > 0)
continue;
const auto &npc = npcs.npcs.at(it->first);
int index = it->first;
ai.plans[index] = {};
for (const auto &goal : ai.goals)
plan_tasks.emplace_back(bb, goal,
ai.planner.get());
}
}
}
void CharacterAIModule::createBlackboards(flecs::entity town,
@@ -536,15 +535,7 @@ void CharacterAIModule::updateBlackboardsBits(flecs::entity town,
{
OgreAssert(town.is_valid(), "Bad town entity");
std::lock_guard<std::mutex> lock(*ai.mutex);
struct UpdateBit {
Ogre::String checkValue;
int recover;
int minValue;
int maxValue;
int threshold;
Ogre::String writeValue;
};
struct UpdateBit updateBits[] = {
std::vector<struct Blackboard::UpdateBit> updateBits = {
{ "health", 0, 0, 100, 20, "healthy" },
{ "needs_hunger", 1, 0, 10000, 2000, "hungry" },
{ "needs_thirst", 1, 0, 10000, 1000, "thirsty" },
@@ -553,39 +544,16 @@ void CharacterAIModule::updateBlackboardsBits(flecs::entity town,
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
auto &stats = ai.blackboards.at(it->first).stats;
auto &object = ai.blackboards.at(it->first).object;
auto &bb = ai.blackboards.at(it->first);
bb.commit();
}
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
ai.blackboards.at(it->first).index = it->first;
ai.blackboards.at(it->first).town = town;
for (const auto &bits : updateBits) {
int value = stats[bits.checkValue].get<int>();
int maxValue = bits.maxValue;
int minValue = bits.minValue;
int threshold = bits.threshold;
if (it->second.props.find(bits.checkValue + "_max") !=
it->second.props.end())
maxValue =
it->second
.props[bits.checkValue + "_max"]
.get<int>();
if (it->second.props.find(bits.checkValue +
"_threshold") !=
it->second.props.end())
threshold = it->second
.props[bits.checkValue +
"_threshold"]
.get<int>();
value += bits.recover;
if (value > maxValue)
value = maxValue;
if (value >= threshold)
stats[bits.writeValue] = 1;
else
stats[bits.writeValue] = 0;
if (value < bits.minValue)
value = bits.minValue;
stats[bits.checkValue] = value;
}
auto &bb = ai.blackboards.at(it->first);
bb.updateBits(updateBits, it->second.props);
}
}
@@ -598,32 +566,52 @@ void CharacterAIModule::updateBlackboards(flecs::entity town,
for (auto it = npcs.npcs.begin(); it != npcs.npcs.end(); it++) {
if (ai.blackboards.find(it->first) == ai.blackboards.end())
continue;
auto &stats = ai.blackboards.at(it->first).stats;
auto &object = ai.blackboards.at(it->first).object;
ai.blackboards.at(it->first).index = it->first;
ai.blackboards.at(it->first).town = town;
auto &bb = ai.blackboards.at(it->first);
bb.query_ai();
#if 0
OgreAssert(nodeActionCount > 0 ||
points.size() == 0,
"no node actions and no points");
if (nodeActionCount == 0) {
std::cout << "nodes:"
<< alist.nodes.size() << " "
<< alist.dynamicNodes.size()
<< std::endl;
std::cout << "points: " << points.size()
<< std::endl;
std::cout << "position: " << position
<< std::endl;
}
OgreAssert(nodeActionCount > 0,
"no node actions");
#endif
bb.fixupBooleanKeys();
}
prepareActions(ai);
}
void CharacterAIModule::prepareActions(TownAI &ai)
{
// baking actions
for (auto &action : ai.actions) {
action->effects.commit();
action->prereq.commit();
}
for (auto &naction : ai.nodeActions)
for (auto &action : naction.second) {
action->effects.commit();
action->prereq.commit();
}
#if 0
for (const auto &action : ai.actions) {
std::cout << action->get_name() << std::endl;
std::cout << "effects: " << std::endl;
action->effects.dump_bits();
std::cout << "prereq: " << std::endl;
action->prereq.dump_bits();
}
for (auto &naction : ai.nodeActions)
for (const auto &action : naction.second) {
std::cout << action->get_name() << std::endl;
std::cout << action->get_name() << std::endl;
std::cout << "effects: " << std::endl;
action->effects.dump_bits();
std::cout << "prereq: " << std::endl;
action->prereq.dump_bits();
}
std::cout << "end dump" << std::endl;
std::cout << "dump" << std::endl;
for (auto &m : Blackboard::mapping)
std::cout << m.first << ": " << m.second << std::endl;
std::cout << "end dump" << std::endl;
// OgreAssert(false, "baking actions");
#endif
}
void Blackboard::_actionRefResize(int count)
@@ -645,18 +633,58 @@ Blackboard::Blackboard()
, index(-1)
, actionRefCount(0)
, mutex(std::make_shared<std::mutex>())
, bits(0)
, mask(0)
{
}
std::unordered_map<std::string, size_t> Blackboard::mapping;
Blackboard::Blackboard(const nlohmann::json &stats)
: Blackboard()
{
this->stats = stats;
populate(stats, mapping);
}
void Blackboard::populate(const nlohmann::json &stats,
std::unordered_map<std::string, size_t> &mapping)
{
if (stats.empty())
return;
for (auto &[key, value] : stats.items()) {
if (value.is_number_integer()) {
int val = value.get<int>();
if (val == 0 || val == 1) {
// Update mapping if key is new
if (mapping.find(key) == mapping.end()) {
size_t next_bit = mapping.size();
if (next_bit < 64) {
mapping[key] = next_bit;
} else {
OgreAssert(false,
"Out of bits");
continue;
}
}
size_t bit_idx = mapping[key];
bits.set(bit_idx, val == 1);
mask.set(bit_idx);
}
}
}
}
bool Blackboard::operator==(const Blackboard &other) const
{
return is_satisfied_by(this->stats, other.stats);
OgreAssert(mask != 0 && other.mask != 0, "blackboard not prepared");
#if 0
if (mask == 0 || other.mask == 0)
return is_satisfied_by(this->stats, other.stats);
else
#endif
return (bits & other.mask) == (other.bits & other.mask);
}
bool Blackboard::operator!=(const Blackboard &other) const
@@ -668,6 +696,12 @@ void Blackboard::apply(const Blackboard &other)
{
std::lock_guard<std::mutex> lock(*mutex);
stats.update(other.stats);
// 1. Clear bits in 'values' that are about to be overwritten (where effect mask is 1)
// 2. OR the result with the effect values (filtered by the effect mask)
bits = (bits & ~other.mask) | (other.bits & other.mask);
// 3. Update our mask to include any new state definitions introduced by the effect
mask |= other.mask;
}
Ogre::String Blackboard::dumpActions()
@@ -759,6 +793,37 @@ void Blackboard::actionRefAddActions(goap::BaseAction<Blackboard> **actions,
}
}
void Blackboard::updateBits(const std::vector<UpdateBit> &updateBits,
const nlohmann::json &props)
{
for (const auto &mbits : updateBits) {
// OgreAssert(mapping.find(mbits.checkValue) != mapping.end(),
// "No key " + mbits.checkValue);
OgreAssert(mapping.find(mbits.writeValue) != mapping.end(),
"No key " + mbits.writeValue);
int value = stats[mbits.checkValue].get<int>();
int maxValue = mbits.maxValue;
int minValue = mbits.minValue;
int threshold = mbits.threshold;
if (props.find(mbits.checkValue + "_max") != props.end())
maxValue = props[mbits.checkValue + "_max"].get<int>();
if (props.find(mbits.checkValue + "_threshold") != props.end())
threshold = props[mbits.checkValue + "_threshold"]
.get<int>();
value += mbits.recover;
if (value > maxValue)
value = maxValue;
if (value >= threshold)
stats[mbits.writeValue] = 1;
else
stats[mbits.writeValue] = 0;
if (value < mbits.minValue)
value = mbits.minValue;
stats[mbits.checkValue] = value;
}
}
struct ComparePair {
const nlohmann::json &current;
const nlohmann::json &target;
@@ -802,7 +867,9 @@ bool Blackboard::is_satisfied_by(const nlohmann::json &current,
int Blackboard::distance_to(const Blackboard &goal) const
{
int distance = 0;
OgreAssert(mask != 0 && goal.mask != 0, "blackboard not prepared");
return ((bits ^ goal.bits) & goal.mask).count();
#if 0
OgreAssert(goal.stats.is_object(),
"Not an object:\n" + goal.stats.dump(4));
for (auto it = goal.stats.begin(); it != goal.stats.end(); ++it) {
@@ -829,6 +896,7 @@ int Blackboard::distance_to(const Blackboard &goal) const
}
}
return distance;
#endif
}
void Blackboard::setPosition(const Ogre::Vector3 &position)

View File

@@ -9,13 +9,23 @@ namespace ECS
{
struct Blackboard {
nlohmann::json stats;
struct UpdateBit {
Ogre::String checkValue;
int recover;
int minValue;
int maxValue;
int threshold;
Ogre::String writeValue;
};
int object;
int index;
flecs::entity town;
std::shared_ptr<std::mutex> mutex;
static std::unordered_map<std::string, size_t> mapping;
private:
nlohmann::json stats;
std::bitset<64> bits, mask;
std::vector<goap::BaseAction<Blackboard> *> actionRef;
int actionRefCount;
int actionRefPtr;
@@ -25,6 +35,7 @@ private:
void _actionRefResize(int count);
void _actionRefAddActions(
const std::vector<goap::BaseAction<Blackboard> *> &actions);
void populate(const nlohmann::json &stats, std::unordered_map<std::string, size_t>& mapping);
public:
Blackboard();
@@ -41,14 +52,32 @@ public:
const std::vector<goap::BaseAction<Blackboard> *> &actions);
void actionRefAddActions(goap::BaseAction<Blackboard> **actions,
int count);
const goap::BaseAction<Blackboard> *const *getActionsData() const
{
return actionRef.data();
}
goap::BaseAction<Blackboard> **getActionsData()
{
return actionRef.data();
}
int getActionsCount()
int getActionsCount() const
{
return actionRefCount;
}
void commit()
{
populate(stats, mapping);
}
void dump_bits() const
{
std::cout << "bits: " << bits << std::endl;
std::cout << "mask: " << mask << std::endl;
}
bool is_valid() const
{
return !stats.is_null() && mask != 0;
}
void updateBits(const std::vector<UpdateBit> &updateBits, const nlohmann::json &props);
private:
static bool is_satisfied_by(const nlohmann::json &current,
@@ -79,6 +108,7 @@ struct TownAI {
planner;
std::unordered_map<int, Blackboard> blackboards;
typedef goap::BasePlanner<Blackboard, goap::BaseAction<Blackboard> >::BaseGoal goal_t;
typedef goap::BasePlanner<Blackboard, goap::BaseAction<Blackboard> > planner_t;
struct Plan {
const goal_t *goal;
std::vector<goap::BaseAction<Blackboard> *> plan;
@@ -94,6 +124,7 @@ struct CharacterAIModule {
void createBlackboards(flecs::entity town, const TownNPCs &npcs, TownAI &ai);
void updateBlackboardsBits(flecs::entity town, ActionNodeList &alist, const TownNPCs &npcs, TownAI &ai);
void updateBlackboards(flecs::entity town, const ActionNodeList &alist, const TownNPCs &npcs, TownAI &ai);
void prepareActions(TownAI &ai);
};
}