1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-20 20:23:03 +02:00

Merge pull request #3664 from vcmi/object-graph

NKAI: object graph improved and optimized
This commit is contained in:
Andrii Danylchenko 2024-03-21 20:08:45 +02:00 committed by GitHub
commit 6381a6f601
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 362 additions and 47 deletions

View File

@ -73,13 +73,25 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
}
if(objToVisit && !shouldVisit(ai->nullkiller.get(), path.targetHero, objToVisit))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Hero %s should not visit obj %s", path.targetHero->getNameTranslated(), objToVisit->getObjectName());
#endif
continue;
}
auto hero = path.targetHero;
auto danger = path.getTotalDanger();
if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT && path.exchangeCount > 1)
if(ai->nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
&& (path.getTotalDanger() == 0 || path.turn() > 0)
&& path.exchangeCount > 1)
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Hero %s is SCOUT, chain used and no danger", path.targetHero->getNameTranslated());
#endif
continue;
}
auto firstBlockedAction = path.getFirstBlockedAction();
if(firstBlockedAction)

View File

@ -27,8 +27,8 @@ namespace NKAI
{
namespace AIPathfinding
{
const int BUCKET_COUNT = 5;
const int BUCKET_SIZE = 3;
const int BUCKET_COUNT = 3;
const int BUCKET_SIZE = 5;
const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
const int CHAIN_MAX_DEPTH = 4;
}

View File

@ -19,14 +19,19 @@ namespace NKAI
namespace AIPathfinding
{
bool QuestAction::canAct(const AIPathNode * node) const
{
return canAct(node->actor->hero);
}
bool QuestAction::canAct(const CGHeroInstance * hero) const
{
if(questInfo.obj->ID == Obj::BORDER_GATE || questInfo.obj->ID == Obj::BORDERGUARD)
{
return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(node->actor->hero);
return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(hero);
}
return questInfo.quest->activeForPlayers.count(node->actor->hero->getOwner())
|| questInfo.quest->checkQuest(node->actor->hero);
return questInfo.quest->activeForPlayers.count(hero->getOwner())
|| questInfo.quest->checkQuest(hero);
}
Goals::TSubgoal QuestAction::decompose(const CGHeroInstance * hero) const

View File

@ -30,6 +30,8 @@ namespace AIPathfinding
bool canAct(const AIPathNode * node) const override;
bool canAct(const CGHeroInstance * hero) const;
Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
void execute(const CGHeroInstance * hero) const override;

View File

@ -15,10 +15,18 @@
#include "../../../lib/mapping/CMap.h"
#include "../Engine/Nullkiller.h"
#include "../../../lib/logging/VisualLogger.h"
#include "Actions/QuestAction.h"
namespace NKAI
{
struct ConnectionCostInfo
{
float totalCost = 0;
float avg = 0;
int connectionsCount = 0;
};
class ObjectGraphCalculator
{
private:
@ -34,10 +42,14 @@ private:
public:
ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai)
:ai(ai), target(target)
{
}
void setGraphObjects()
{
for(auto obj : ai->memory->visitableObjs)
{
if(obj && obj->isVisitable() && obj->ID != Obj::HERO)
if(obj && obj->isVisitable() && obj->ID != Obj::HERO && obj->ID != Obj::EVENT)
{
addObjectActor(obj);
}
@ -47,7 +59,70 @@ public:
{
addObjectActor(town);
}
}
void calculateConnections()
{
updatePaths();
foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
{
calculateConnections(pos);
});
removeExtraConnections();
}
void addMinimalDistanceJunctions()
{
foreach_tile_pos(ai->cb.get(), [this](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
{
if(target->hasNodeAt(pos))
return;
if(ai->cb->getGuardingCreaturePosition(pos).valid())
return;
ConnectionCostInfo currentCost = getConnectionsCost(pos);
if(currentCost.connectionsCount <= 2)
return;
float neighborCost = currentCost.avg + 0.001f;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Checking junction %s", pos.toString());
}
foreach_neighbour(
ai->cb.get(),
pos,
[this, &neighborCost](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
auto costTotal = this->getConnectionsCost(neighbor);
if(costTotal.avg < neighborCost)
{
neighborCost = costTotal.avg;
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Better node found at %s", neighbor.toString());
}
}
});
if(currentCost.avg < neighborCost)
{
addJunctionActor(pos);
}
});
}
private:
void updatePaths()
{
PathfinderSettings ps;
ps.mainTurnDistanceLimit = 5;
@ -59,11 +134,31 @@ public:
void calculateConnections(const int3 & pos)
{
auto guarded = ai->cb->getGuardingCreaturePosition(pos).valid();
if(target->hasNodeAt(pos))
{
foreach_neighbour(
ai->cb.get(),
pos,
[this, &pos](const CPlayerSpecificInfoCallback * cb, const int3 & neighbor)
{
if(target->hasNodeAt(neighbor))
{
auto paths = ai->pathfinder->getPathInfo(neighbor);
for(auto & path : paths)
{
if(pos == path.targetHero->visitablePos())
{
target->tryAddConnection(pos, neighbor, path.movementCost(), path.getTotalDanger());
}
}
}
});
if(guarded)
return;
}
auto guardPos = ai->cb->getGuardingCreaturePosition(pos);
auto paths = ai->pathfinder->getPathInfo(pos);
for(AIPath & path1 : paths)
@ -73,25 +168,32 @@ public:
if(path1.targetHero == path2.targetHero)
continue;
auto pos1 = path1.targetHero->visitablePos();
auto pos2 = path2.targetHero->visitablePos();
if(guardPos.valid() && guardPos != pos1 && guardPos != pos2)
continue;
auto obj1 = actorObjectMap[path1.targetHero];
auto obj2 = actorObjectMap[path2.targetHero];
auto tile1 = cb->getTile(obj1->visitablePos());
auto tile2 = cb->getTile(obj2->visitablePos());
auto tile1 = cb->getTile(pos1);
auto tile2 = cb->getTile(pos2);
if(tile2->isWater() && !tile1->isWater())
{
auto linkTile = cb->getTile(pos);
if(!cb->getTile(pos)->isWater())
continue;
if(!linkTile->isWater() || obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD)
if(obj1 && (obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD))
continue;
}
auto danger = ai->pathfinder->getStorage()->evaluateDanger(obj2->visitablePos(), path1.targetHero, true);
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
auto updated = target->tryAddConnection(
obj1->visitablePos(),
obj2->visitablePos(),
pos1,
pos2,
path1.movementCost() + path2.movementCost(),
danger);
@ -99,8 +201,8 @@ public:
{
logAi->trace(
"Connected %s[%s] -> %s[%s] through [%s], cost %2f",
obj1->getObjectName(), obj1->visitablePos().toString(),
obj2->getObjectName(), obj2->visitablePos().toString(),
obj1 ? obj1->getObjectName() : "J", pos1.toString(),
obj2 ? obj2->getObjectName() : "J", pos2.toString(),
pos.toString(),
path1.movementCost() + path2.movementCost());
}
@ -108,7 +210,49 @@ public:
}
}
private:
bool isExtraConnection(float direct, float side1, float side2) const
{
float sideRatio = (side1 + side2) / direct;
return sideRatio < 1.25f && direct > side1 && direct > side2;
}
void removeExtraConnections()
{
std::vector<std::pair<int3, int3>> connectionsToRemove;
for(auto & actor : temporaryActorHeroes)
{
auto pos = actor->visitablePos();
auto & currentNode = target->getNode(pos);
target->iterateConnections(pos, [this, &pos, &connectionsToRemove, &currentNode](int3 n1, ObjectLink o1)
{
target->iterateConnections(n1, [&pos, &o1, &currentNode, &connectionsToRemove, this](int3 n2, ObjectLink o2)
{
auto direct = currentNode.connections.find(n2);
if(direct != currentNode.connections.end() && isExtraConnection(direct->second.cost, o1.cost, o2.cost))
{
connectionsToRemove.push_back({pos, n2});
}
});
});
}
vstd::removeDuplicates(connectionsToRemove);
for(auto & c : connectionsToRemove)
{
target->removeConnection(c.first, c.second);
if(NKAI_GRAPH_TRACE_LEVEL >= 2)
{
logAi->trace("Remove ineffective connection %s->%s", c.first.toString(), c.second.toString());
}
}
}
void addObjectActor(const CGObjectInstance * obj)
{
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(obj->cb)).get();
@ -126,11 +270,77 @@ private:
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = obj;
actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD ? HeroRole::MAIN : HeroRole::SCOUT;
target->addObject(obj);
};
}
void addJunctionActor(const int3 & visitablePos)
{
auto internalCb = temporaryActorHeroes.front()->cb;
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();
CRandomGenerator rng;
objectActor->setOwner(ai->playerID); // lets avoid having multiple colors
objectActor->initHero(rng, static_cast<HeroTypeID>(0));
objectActor->pos = objectActor->convertFromVisitablePos(visitablePos);
objectActor->initObj(rng);
if(cb->getTile(visitablePos)->isWater())
{
objectActor->boat = temporaryBoats.emplace_back(std::make_unique<CGBoat>(objectActor->cb)).get();
}
assert(objectActor->visitablePos() == visitablePos);
actorObjectMap[objectActor] = nullptr;
actors[objectActor] = HeroRole::SCOUT;
target->registerJunction(visitablePos);
}
ConnectionCostInfo getConnectionsCost(const int3 & pos) const
{
auto paths = ai->pathfinder->getPathInfo(pos);
std::map<int3, float> costs;
for(auto & path : paths)
{
auto fromPos = path.targetHero->visitablePos();
auto cost = costs.find(fromPos);
if(cost == costs.end())
{
costs.emplace(fromPos, path.movementCost());
}
else
{
if(path.movementCost() < cost->second)
{
costs[fromPos] = path.movementCost();
}
}
}
ConnectionCostInfo result;
for(auto & cost : costs)
{
result.totalCost += cost.second;
result.connectionsCount++;
}
if(result.connectionsCount)
{
result.avg = result.totalCost / result.connectionsCount;
}
return result;
}
};
bool ObjectGraph::tryAddConnection(
@ -142,23 +352,24 @@ bool ObjectGraph::tryAddConnection(
return nodes[from].connections[to].update(cost, danger);
}
void ObjectGraph::removeConnection(const int3 & from, const int3 & to)
{
nodes[from].connections.erase(to);
}
void ObjectGraph::updateGraph(const Nullkiller * ai)
{
auto cb = ai->cb;
ObjectGraphCalculator calculator(this, ai);
foreach_tile_pos(cb.get(), [this, &calculator](const CPlayerSpecificInfoCallback * cb, const int3 & pos)
{
if(nodes.find(pos) != nodes.end())
return;
calculator.calculateConnections(pos);
});
calculator.setGraphObjects();
calculator.calculateConnections();
calculator.addMinimalDistanceJunctions();
calculator.calculateConnections();
if(NKAI_GRAPH_TRACE_LEVEL >= 1)
dumpToLog("graph");
}
void ObjectGraph::addObject(const CGObjectInstance * obj)
@ -166,6 +377,11 @@ void ObjectGraph::addObject(const CGObjectInstance * obj)
nodes[obj->visitablePos()].init(obj);
}
void ObjectGraph::registerJunction(const int3 & pos)
{
nodes[pos].initJunction();
}
void ObjectGraph::removeObject(const CGObjectInstance * obj)
{
nodes[obj->visitablePos()].objectExists = false;
@ -198,6 +414,9 @@ void ObjectGraph::connectHeroes(const Nullkiller * ai)
for(AIPath & path : paths)
{
if(path.getFirstBlockedAction())
continue;
auto heroPos = path.targetHero->visitablePos();
nodes[pos].connections[heroPos].update(
@ -259,20 +478,55 @@ void GraphPaths::calculatePaths(const CGHeroInstance * targetHero, const Nullkil
GraphPathNodePointer pos = pq.top();
pq.pop();
auto & node = getNode(pos);
auto & node = getOrCreateNode(pos);
std::shared_ptr<SpecialAction> transitionAction;
if(node.obj)
{
if(node.obj->ID == Obj::QUEST_GUARD
|| node.obj->ID == Obj::BORDERGUARD
|| node.obj->ID == Obj::BORDER_GATE)
{
auto questObj = dynamic_cast<const IQuestObject *>(node.obj);
auto questInfo = QuestInfo(questObj->quest, node.obj, pos.coord);
if(node.obj->ID == Obj::QUEST_GUARD
&& questObj->quest->mission == Rewardable::Limiter{}
&& questObj->quest->killTarget == ObjectInstanceID::NONE)
{
continue;
}
auto questAction = std::make_shared<AIPathfinding::QuestAction>(questInfo);
if(!questAction->canAct(targetHero))
{
transitionAction = questAction;
}
}
}
node.isInQueue = false;
graph.iterateConnections(pos.coord, [&](int3 target, ObjectLink o)
graph.iterateConnections(pos.coord, [this, ai, &pos, &node, &transitionAction, &pq](int3 target, ObjectLink o)
{
auto targetNodeType = o.danger ? GrapthPathNodeType::BATTLE : pos.nodeType;
auto targetNodeType = o.danger || transitionAction ? GrapthPathNodeType::BATTLE : pos.nodeType;
auto targetPointer = GraphPathNodePointer(target, targetNodeType);
auto & targetNode = getNode(targetPointer);
auto & targetNode = getOrCreateNode(targetPointer);
if(targetNode.tryUpdate(pos, node, o))
{
if(graph.getNode(target).objTypeID == Obj::HERO)
return;
targetNode.specialAction = transitionAction;
auto targetGraphNode = graph.getNode(target);
if(targetGraphNode.objID.hasValue())
{
targetNode.obj = ai->cb->getObj(targetGraphNode.objID, false);
if(targetNode.obj && targetNode.obj->ID == Obj::HERO)
return;
}
if(targetNode.isInQueue)
{
@ -321,11 +575,6 @@ bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathN
if(newCost < cost)
{
if(nodeType < pos.nodeType)
{
logAi->error("Linking error");
}
previous = pos;
danger = prev.danger + link.danger;
cost = newCost;
@ -348,7 +597,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
if(!node.reachable())
continue;
std::vector<int3> tilesToPass;
std::vector<GraphPathNodePointer> tilesToPass;
uint64_t danger = node.danger;
float cost = node.cost;
@ -372,7 +621,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
vstd::amax(danger, currentNode.danger);
vstd::amax(cost, currentNode.cost);
tilesToPass.push_back(current.coord);
tilesToPass.push_back(current);
if(currentNode.cost < 2.0f)
break;
@ -383,7 +632,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
if(tilesToPass.empty())
continue;
auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back());
auto entryPaths = ai->pathfinder->getPathInfo(tilesToPass.back().coord);
for(auto & path : entryPaths)
{
@ -394,12 +643,13 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
{
AIPathNodeInfo n;
n.coord = *graphTile;
n.coord = graphTile->coord;
n.cost = cost;
n.turns = static_cast<ui8>(cost) + 1; // just in case lets select worst scenario
n.danger = danger;
n.targetHero = hero;
n.parentIndex = 0;
n.parentIndex = -1;
n.specialAction = getNode(*graphTile).specialAction;
for(auto & node : path.nodes)
{

View File

@ -50,6 +50,13 @@ struct ObjectNode
objID = obj->id;
objTypeID = obj->ID;
}
void initJunction()
{
objectExists = false;
objID = ObjectInstanceID();
objTypeID = Obj();
}
};
class ObjectGraph
@ -59,8 +66,11 @@ class ObjectGraph
public:
void updateGraph(const Nullkiller * ai);
void addObject(const CGObjectInstance * obj);
void registerJunction(const int3 & pos);
void connectHeroes(const Nullkiller * ai);
void removeObject(const CGObjectInstance * obj);
bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
void removeConnection(const int3 & from, const int3 & to);
void dumpToLog(std::string visualKey) const;
template<typename Func>
@ -77,7 +87,10 @@ public:
return nodes.at(tile);
}
bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger);
bool hasNodeAt(const int3 & tile) const
{
return vstd::contains(nodes, tile);
}
};
struct GraphPathNode;
@ -131,6 +144,8 @@ struct GraphPathNode
GraphPathNodePointer previous;
float cost = BAD_COST;
uint64_t danger = 0;
const CGObjectInstance * obj = nullptr;
std::shared_ptr<SpecialAction> specialAction;
using TFibHeap = boost::heap::fibonacci_heap<GraphPathNodePointer, boost::heap::compare<GraphNodeComparer>>;
@ -157,7 +172,7 @@ public:
void dumpToLog() const;
private:
GraphPathNode & getNode(const GraphPathNodePointer & pos)
GraphPathNode & getOrCreateNode(const GraphPathNodePointer & pos)
{
auto & node = pathNodes[pos.coord][pos.nodeType];
@ -165,6 +180,13 @@ private:
return node;
}
const GraphPathNode & getNode(const GraphPathNodePointer & pos) const
{
auto & node = pathNodes.at(pos.coord)[pos.nodeType];
return node;
}
};
}

View File

@ -42,15 +42,34 @@ namespace AIPathfinding
}
auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper);
if(blocker == BlockingReason::NONE)
{
destination.blocked = nodeStorage->isDistanceLimitReached(source, destination);
if(destination.nodeObject
&& !destination.blocked
&& !allowBypassObjects
&& !dynamic_cast<const CGTeleport *>(destination.nodeObject)
&& destination.nodeObject->ID != Obj::EVENT)
{
destination.blocked = true;
destination.node->locked = true;
}
return;
}
if(!allowBypassObjects)
{
if(destination.nodeObject)
{
destination.blocked = true;
destination.node->locked = true;
}
return;
}
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(

View File

@ -49,7 +49,7 @@ namespace AIPathfinding
return;
// when actor represents moster graph node, we need to let him escape monster
if(!destination.guarded && cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition)
if(cb->getGuardingCreaturePosition(source.coord) == actor->initialPosition)
return;
}

View File

@ -71,6 +71,8 @@ public:
virtual void drawLine(int3 start, int3 end) override
{
const Point offset = Point(30, 30);
auto level = model->getLevel();
if(start.z != level || end.z != level)
@ -83,6 +85,9 @@ public:
pStart.x += 3;
pEnd.x -= 3;
pStart += offset;
pEnd += offset;
if(viewPort.isInside(pStart) && viewPort.isInside(pEnd))
{
target.drawLine(pStart, pEnd, ColorRGBA(255, 255, 0), ColorRGBA(255, 0, 0));