More progress.

- AMBUSH flag on nodes makes them blind and untargetable when getting `startnode` and `goalnode` for `FindPath()`. This is useful for indicating a node should be skipped when tele/portaling, so the current path can be preserved.
- Setup is simple: place nodes behind lines that tele/portal entities and mark them as AMBUSH.

Other changes:

- Restored global array since blockmap is not a viable option here.
- Added MAPINFO `pathing` flag which enables pathing by default.
- Added NOPATHING flag to disable pathing entirely, useful for maps that have pathing enabled.
- Added `ReachedNode(Actor mo)` virtual, responsible for handling node traversal.
- Nodes now make use of MeleeRange to limit their sight checking functions.
This commit is contained in:
Major Cooke 2024-02-16 09:22:20 -06:00 committed by Rachael Alexanderson
parent ad52e2cc1e
commit 81ebd8c8c4
14 changed files with 210 additions and 82 deletions

View file

@ -2506,26 +2506,25 @@ static void ReconstructPath(TMap<AActor*, AActor*> &cameFrom, AActor* current, T
while(tmp = cameFrom.CheckKey(*tmp));
}
static AActor* FindClosestNode(AActor* from, double maxSearch)
static AActor* FindClosestNode(AActor* from)
{
static PClass * nodeCls = PClass::FindClass(NAME_PathNode);
FPortalGroupArray check(FPortalGroupArray::PGA_Full3d);
FMultiBlockThingsIterator it(check, from->Level, from->Pos().X, from->Pos().Y, from->Pos().Z - ((from->Height + maxSearch) / 2), from->Height + maxSearch, from->radius + maxSearch, false, from->Sector);
FMultiBlockThingsIterator::CheckResult res;
AActor * closest = nullptr;
double closestDist = DBL_MAX;
while(it.Next(&res))
for (int i = 0; i < from->Level->PathNodes.Size(); i++)
{
if(nodeCls->IsAncestorOf(res.thing->GetClass()))
AActor* node = from->Level->PathNodes[i];
if(node && !(node->flags & MF_AMBUSH) && nodeCls->IsAncestorOf(node->GetClass()))
{
double dst = res.thing->Distance3D(from);
if(dst < closestDist && P_CheckSight(res.thing, from))
double dst = node->Distance3DSquared(from);
bool mrange = (dst < closestDist && (node->meleerange <= 0.0 || dst < (node->meleerange * node->meleerange)));
if(mrange && P_CheckSight(node, from))
{
closestDist = dst;
closest = res.thing;
closest = node;
}
}
}
@ -2540,7 +2539,7 @@ static V GetOr(TMap<K, V> map, const K &key, V alt)
return k ? *k : alt;
}
static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>> &path)
static bool FindPathAStar(AActor* startnode, AActor* goalnode, TArray<TObjPtr<AActor*>> &path)
{
TArray<AActor*> openSet;
@ -2548,9 +2547,9 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
TMap<AActor*, double> gScore;
TMap<AActor*, double> fScore;
openSet.Push(start);
gScore.Insert(start, 0);
fScore.Insert(start, start->Distance3D(goal));
openSet.Push(startnode);
gScore.Insert(startnode, 0);
fScore.Insert(startnode, startnode->Distance3DSquared(goalnode));
auto lt_fScore = [&fScore](AActor* lhs, AActor* rhs)
{
@ -2561,7 +2560,7 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
{
AActor * current = openSet[0];
openSet.Delete(0);
if(current == goal)
if(current == goalnode)
{
ReconstructPath(cameFrom, current, path);
return true;
@ -2572,7 +2571,7 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
for(AActor * neighbor : GetPathNodeNeighbors(current))
{
double tentative_gScore = current_gScore + current->Distance3D(neighbor);
double tentative_gScore = current_gScore + current->Distance3DSquared(neighbor);
double neighbor_gScore = GetOr(gScore, neighbor, DBL_MAX);
@ -2581,7 +2580,7 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
openSet.SortedDelete(neighbor, lt_fScore);
cameFrom.Insert(neighbor, current);
gScore.Insert(neighbor, tentative_gScore);
fScore.Insert(neighbor, tentative_gScore + neighbor->Distance3D(goal));
fScore.Insert(neighbor, tentative_gScore + neighbor->Distance3DSquared(goalnode));
openSet.SortedInsert(neighbor, lt_fScore);
}
}
@ -2589,20 +2588,19 @@ static bool FindPathAStar(AActor* start, AActor* goal, TArray<TObjPtr<AActor*>>
return false;
}
bool FLevelLocals::FindPath(AActor* start, AActor* goal, AActor* startNode, AActor* goalNode, double maxSearch)
bool FLevelLocals::FindPath(AActor* chaser, AActor* target, AActor* startNode, AActor* goalNode)
{
static PClass * nodeCls = PClass::FindClass(NAME_PathNode);
if (!start || !goal)
if (!chaser || !target)
{
return false;
}
static PClass* nodeCls = PClass::FindClass(NAME_PathNode);
assert(startNode == nullptr || nodeCls->IsAncestorOf(startNode->GetClass()));
assert(goalNode == nullptr || nodeCls->IsAncestorOf(goalNode->GetClass()));
if(startNode == nullptr) startNode = FindClosestNode(start, maxSearch);
if(goalNode == nullptr) goalNode = FindClosestNode(goal, maxSearch);
if(startNode == nullptr) startNode = FindClosestNode(chaser);
if(goalNode == nullptr) goalNode = FindClosestNode(target);
// Incomplete graph.
if (!startNode || !goalNode)
@ -2612,16 +2610,16 @@ bool FLevelLocals::FindPath(AActor* start, AActor* goal, AActor* startNode, AAct
if (startNode == goalNode)
{
start->ClearPath();
start->Path.Push(MakeObjPtr<AActor*>(startNode));
chaser->ClearPath();
chaser->Path.Push(MakeObjPtr<AActor*>(startNode));
return true;
}
if(FindPathAStar(startNode, goalNode, start->Path))
if (FindPathAStar(startNode, goalNode, chaser->Path))
{
if (start->goal && nodeCls->IsAncestorOf(start->goal->GetClass()))
if (chaser->goal && nodeCls->IsAncestorOf(chaser->goal->GetClass()))
{
start->goal = nullptr;
chaser->goal = nullptr;
}
return true;
}
@ -2636,6 +2634,22 @@ DEFINE_ACTION_FUNCTION(FLevelLocals, FindPath)
PARAM_OBJECT(target, AActor);
PARAM_OBJECT(startnode, AActor);
PARAM_OBJECT(goalnode, AActor);
PARAM_FLOAT(maxSearch);
return self->FindPath(chaser, target, startnode, goalnode, maxSearch);
return self->FindPath(chaser, target, startnode, goalnode);
}
DEFINE_ACTION_FUNCTION(FLevelLocals, HandlePathNode)
{
PARAM_SELF_STRUCT_PROLOGUE(FLevelLocals);
PARAM_OBJECT(node, AActor);
PARAM_BOOL(add);
if (node)
{
if (add)
{
if (self->PathNodes.Find(node) >= self->PathNodes.Size())
self->PathNodes.Push(node);
}
else self->PathNodes.Delete(self->PathNodes.Find(node));
}
return 0;
}

View file

@ -444,8 +444,9 @@ public:
void SetMusic();
bool FindPath(AActor *chaser, AActor *target, AActor *startnode = nullptr, AActor *goalnode = nullptr, double maxSearch = 256.0);
bool FindPath(AActor *chaser, AActor *target, AActor *startnode = nullptr, AActor *goalnode = nullptr);
TArray<AActor *> PathNodes;
TArray<vertex_t> vertexes;
TArray<sector_t> sectors;
TArray<extsector_t> extsectors; // container for non-trivial sector information. sector_t must be trivially copyable for *_fakeflat to work as intended.

View file

@ -1819,6 +1819,7 @@ MapFlagHandlers[] =
{ "disableskyboxao", MITYPE_CLRFLAG3, LEVEL3_SKYBOXAO, 0 },
{ "avoidmelee", MITYPE_SETFLAG3, LEVEL3_AVOIDMELEE, 0 },
{ "attenuatelights", MITYPE_SETFLAG3, LEVEL3_ATTENUATE, 0 },
{ "pathing", MITYPE_SETFLAG3, LEVEL3_PATHING, 0 },
{ "nobotnodes", MITYPE_IGNORE, 0, 0 }, // Skulltag option: nobotnodes
{ "nopassover", MITYPE_COMPATFLAG, COMPATF_NO_PASSMOBJ, 0 },
{ "passover", MITYPE_CLRCOMPATFLAG, COMPATF_NO_PASSMOBJ, 0 },

View file

@ -270,6 +270,7 @@ enum ELevelFlags : unsigned int
LEVEL3_AVOIDMELEE = 0x00020000, // global flag needed for proper MBF support.
LEVEL3_NOJUMPDOWN = 0x00040000, // only for MBF21. Inverse of MBF's dog_jumping flag.
LEVEL3_LIGHTCREATED = 0x00080000, // a light had been created in the last frame
LEVEL3_PATHING = 0x00100000, // enable pathfinding by default
};

View file

@ -326,6 +326,7 @@ void FLevelLocals::ClearLevelData(bool fullgc)
}
ClearPortals();
PathNodes.Clear();
tagManager.Clear();
ClearTIDHashes();
if (SpotState) SpotState->Destroy();

View file

@ -445,6 +445,7 @@ enum ActorFlag9
MF9_DECOUPLEDANIMATIONS = 0x00000010, // [RL0] Decouple model animations from states
MF9_PATHING = 0x00000020, // [MC] Enables monsters to do pathfinding, such as A*.
MF9_KEEPPATH = 0x00000040, // [MC] Forces monsters to keep to the path when target's in sight.
MF9_NOPATHING = 0x00000080, // [MC] override the mapinfo "pathfinding"
};
// --- mobj.renderflags ---
@ -1104,6 +1105,8 @@ public:
void SetDynamicLights();
void ClearPath();
bool CanPathfind();
void CallReachedNode(AActor *node);
// info for drawing
// NOTE: The first member variable *must* be snext.

View file

@ -2208,8 +2208,12 @@ DEFINE_ACTION_FUNCTION(AActor, A_ClearLastHeard)
void AActor::ClearPath()
{
Path.Clear();
if (goal && goal->IsKindOf(NAME_PathNode))
goal = nullptr;
if (goal)
{
static PClass* nodeCls = PClass::FindClass(NAME_PathNode);
if (nodeCls->IsAncestorOf(goal->GetClass()))
goal = nullptr;
}
}
DEFINE_ACTION_FUNCTION(AActor, ClearPath)
@ -2219,6 +2223,42 @@ DEFINE_ACTION_FUNCTION(AActor, ClearPath)
return 0;
}
bool AActor::CanPathfind()
{
if ((!(flags9 & MF9_NOPATHING) && !(Sector->MoreFlags & SECMF_NOPATHING)) &&
(flags9 & MF9_PATHING || Level->flags3 & LEVEL3_PATHING))
{
if ((flags6 & MF6_NOFEAR))
return true;
// Can't pathfind while feared.
if (!(flags4 & MF4_FRIGHTENED))
{
if (!target)
return true;
if (!target->flags8 & MF8_FRIGHTENING)
return (!target->player || !(target->player->cheats & CF_FRIGHTENING));
}
}
return false;
}
DEFINE_ACTION_FUNCTION(AActor, CanPathfind)
{
PARAM_SELF_PROLOGUE(AActor);
return self->CanPathfind();
}
void AActor::CallReachedNode(AActor *node)
{
IFVIRTUAL(AActor, ReachedNode)
{
VMValue params[2] = { this, node };
VMCall(func, params, 2, nullptr, 0);
}
}
//==========================================================================
//
// A_Wander
@ -2515,30 +2555,8 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi
return;
}
if (actor->target && actor->flags9 & MF9_PATHING)
{
if (actor->goal && actor->goal->IsKindOf(NAME_PathNode))
{
AActor* temp = actor->target;
actor->target = actor->goal;
bool result = P_CheckMeleeRange(actor);
actor->target = temp;
if (result) // TO DO
{
}
}
if (!actor->goal)
{
if (actor->Path.Size() < 1 && actor->Level->FindPath(actor, actor->target))
actor->goal = actor->Path[0];
}
}
// [RH] Don't attack if just moving toward goal
else if (actor->target == actor->goal || (actor->flags5&MF5_CHASEGOAL && actor->goal != nullptr))
if (actor->target == actor->goal || (actor->flags5&MF5_CHASEGOAL && actor->goal != nullptr))
{
AActor * savedtarget = actor->target;
actor->target = actor->goal;
@ -2617,7 +2635,45 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi
}
}
}
}
if (actor->target && actor->CanPathfind())
{
if (actor->goal && !(actor->goal->flags & MF_AMBUSH) && actor->goal->IsKindOf(NAME_PathNode))
{
AActor* temp = actor->target;
actor->target = actor->goal;
bool reached = (P_CheckMeleeRange(actor));
actor->target = temp;
if (reached)
{
actor->CallReachedNode(actor->goal);
/*
AActor* next = nullptr;
if (!(actor->flags9 & MF9_KEEPPATH) &&
P_CheckSight(actor, actor->target, SF_IGNOREWATERBOUNDARY | SF_IGNOREVISIBILITY))
actor->Path.Clear();
else
{
unsigned int index = actor->Path.Find(actor->goal);
while (++index < actor->Path.Size() - 1)
{
next = actor->Path[index];
if (next && next != actor->goal)
break;
}
if (!next) actor->ClearPath();
else actor->goal = next;
}
*/
}
}
if (!actor->goal)
{
if (actor->Path.Size() > 0 || actor->Level->FindPath(actor, actor->target))
actor->goal = actor->Path[0];
}
}
// [RH] Scared monsters attack less frequently
@ -2670,7 +2726,7 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi
lookForBetter = true;
}
AActor * oldtarget = actor->target;
gotNew = P_LookForPlayers (actor, !(flags & CHF_DONTLOOKALLAROUND), NULL);
gotNew = P_LookForPlayers (actor, !(flags & CHF_DONTLOOKALLAROUND), nullptr);
if (lookForBetter)
{
actor->flags3 |= MF3_NOSIGHTCHECK;
@ -2678,6 +2734,7 @@ void A_DoChase (AActor *actor, bool fastchase, FState *meleestate, FState *missi
if (gotNew && actor->target != oldtarget)
{
actor->flags7 &= ~MF7_INCHASE;
actor->ClearPath();
return; // got a new target
}
}

View file

@ -592,7 +592,7 @@ bool P_TeleportMove(AActor* thing, const DVector3 &pos, bool telefrag, bool modi
thing->CheckSectorTransition(oldsec);
}
}
thing->CallReachedNode(thing->goal);
return true;
}
@ -2613,6 +2613,7 @@ bool P_TryMove(AActor *thing, const DVector2 &pos,
thing->LinkToWorld(&ctx);
P_FindFloorCeiling(thing);
thing->ClearInterpolation();
thing->CallReachedNode(thing->goal);
portalcrossed = true;
tm.portalstep = false;
}

View file

@ -355,6 +355,7 @@ static FFlagDef ActorFlagDefs[]=
DEFINE_FLAG(MF9, DECOUPLEDANIMATIONS, AActor, flags9),
DEFINE_FLAG(MF9, PATHING, AActor, flags9),
DEFINE_FLAG(MF9, KEEPPATH, AActor, flags9),
DEFINE_FLAG(MF9, NOPATHING, AActor, flags9),
// Effect flags
DEFINE_FLAG(FX, VISIBILITYPULSE, AActor, effects),

View file

@ -2786,6 +2786,7 @@ DEFINE_FIELD_X(LevelInfo, level_info_t, RedirectMapName)
DEFINE_FIELD_X(LevelInfo, level_info_t, teamdamage)
DEFINE_GLOBAL_NAMED(currentVMLevel, level)
DEFINE_FIELD(FLevelLocals, PathNodes)
DEFINE_FIELD(FLevelLocals, sectors)
DEFINE_FIELD(FLevelLocals, lines)
DEFINE_FIELD(FLevelLocals, sides)
@ -2853,6 +2854,7 @@ DEFINE_FIELD_BIT(FLevelLocals, flags2, infinite_flight, LEVEL2_INFINITE_FLIGHT)
DEFINE_FIELD_BIT(FLevelLocals, flags2, no_dlg_freeze, LEVEL2_CONV_SINGLE_UNFREEZE)
DEFINE_FIELD_BIT(FLevelLocals, flags2, keepfullinventory, LEVEL2_KEEPFULLINVENTORY)
DEFINE_FIELD_BIT(FLevelLocals, flags3, removeitems, LEVEL3_REMOVEITEMS)
DEFINE_FIELD_BIT(FLevelLocals, flags3, pathing, LEVEL3_PATHING)
DEFINE_FIELD_X(Sector, sector_t, floorplane)
DEFINE_FIELD_X(Sector, sector_t, ceilingplane)

View file

@ -44,6 +44,7 @@ DoomEdNums
5065 = InvisibleBridge8
9001 = MapSpot
9013 = MapSpotGravity
9022 = PathNode
9024 = PatrolPoint
9025 = SecurityCamera
9026 = Spark
@ -285,7 +286,6 @@ DoomEdNums
14163 = MusicChanger, 63
14164 = MusicChanger, 64
14165 = MusicChanger
14166 = PathNode
32000 = DoomBuilderCamera
}

View file

@ -664,7 +664,7 @@ class Actor : Thinker native
// called before and after triggering a teleporter
// return false in PreTeleport() to cancel the action early
virtual bool PreTeleport( Vector3 destpos, double destangle, int flags ) { return true; }
virtual void PostTeleport( Vector3 destpos, double destangle, int flags ) {}
virtual void PostTeleport( Vector3 destpos, double destangle, int flags ) { }
native virtual bool OkayToSwitchTarget(Actor other);
native clearscope static class<Actor> GetReplacement(class<Actor> cls);
@ -706,7 +706,6 @@ class Actor : Thinker native
native void ClearFOVInterpolation();
native clearscope Vector3 PosRelative(sector sec) const;
native void RailAttack(FRailParams p);
native void ClearPath();
native void HandleSpawnFlags();
native void ExplodeMissile(line lin = null, Actor target = null, bool onsky = false);
@ -798,6 +797,46 @@ class Actor : Thinker native
movecount = random[TryWalk](0, 15);
return true;
}
native void ClearPath();
native clearscope bool CanPathfind() const;
virtual void ReachedNode(Actor mo)
{
if (!mo && !goal)
return;
mo = goal;
let node = PathNode(mo);
if (!node || !target || (!bKEEPPATH && CheckSight(target)))
{
ClearPath();
return;
}
int i = Path.Find(node) + 1;
int end = Path.Size();
for (i; i < end; i++)
{
PathNode next = Path[i];
if (!next || next == node)
continue;
// Monsters will never 'reach' AMBUSH flagged nodes. Instead, the engine
// indicates they're reached the moment they tele/portal.
if (node.bAMBUSH && next.bAMBUSH)
continue;
goal = next;
break;
}
if (i >= end)
ClearPath();
}
native bool TryMove(vector2 newpos, int dropoff, bool missilecheck = false, FCheckPosition tm = null);
native bool CheckMove(vector2 newpos, int flags = 0, FCheckPosition tm = null);

View file

@ -246,36 +246,34 @@ class SpeakerIcon : Unknown
}
}
//===============================================================
// Path Nodes
//===============================================================
/*
=================================================================
Path Nodes
=================================================================
Special flags are as follows:
AMBUSH
Node is blind. Things cannot "touch" these nodes with A_Chase.
Useful for tele/portals since the engine makes them "touch"
upon transitioning. These nodes are fast forwarded over in Actor's
ReachedNode() function.
*/
class PathNode : Actor
{
// For non-connected paths. Stamina will be used to set this. Necessary for tele/portals.
private int group;
Array<PathNode> neighbors;
Default
{
//$Arg0 "TID 1"
//$Arg1 "TID 2"
//$Arg2 "TID 3"
//$Arg3 "TID 4"
//$Arg4 "TID 5"
//$Arg0Type 14
//$Arg1Type 14
//$Arg2Type 14
//$Arg3Type 14
//$Arg4Type 14
+NOINTERACTION
+NOBLOCKMAP
+INVISIBLE
+DONTSPLASH
+NOTONAUTOMAP
+NOGRAVITY // TO DO: Look into 3D variant for traversing up and down 3D floors and floating monsters.
+NOGRAVITY
Radius 16;
Height 56;
RenderStyle "None";
MeleeRange 2048; // Sight checks limited to this. 0 = infinite.
MeleeRange 0; // Sight checks limited to this. 0 = infinite. Set within map editor.
}
// Args are TIDs. Can be one way to force single directions.
@ -295,6 +293,13 @@ class PathNode : Actor
} while (node = PathNode(it.Next()))
}
level.HandlePathNode(self, true);
}
override void OnDestroy()
{
level.HandlePathNode(self, false);
Super.OnDestroy();
}
// For ACS access with ScriptCall.

View file

@ -408,7 +408,7 @@ struct LevelLocals native
const CLUSTER_HUB = 0x00000001; // Cluster uses hub behavior
native readonly Array<PathNode> PathNodes;
native Array<@Sector> Sectors;
native Array<@Line> Lines;
native Array<@Side> Sides;
@ -476,6 +476,7 @@ struct LevelLocals native
native readonly int compatflags;
native readonly int compatflags2;
native readonly LevelInfo info;
native readonly bool pathing;
native String GetUDMFString(int type, int index, Name key);
native int GetUDMFInt(int type, int index, Name key);
@ -554,7 +555,8 @@ struct LevelLocals native
native void SpawnParticle(FSpawnParticleParams p);
native VisualThinker SpawnVisualThinker(Class<VisualThinker> type);
native bool FindPath(Actor chaser, Actor target, Actor startnode = null, Actor goalnode = null, double maxSearch = 256.0);
native bool FindPath(Actor chaser, Actor target, PathNode startnode = null, PathNode goalnode = null);
native void HandlePathNode(PathNode node, bool add);
}
// a few values of this need to be readable by the play code.