Do navmesh links actually work?

OK! so…

I’ve been trying to debug a custom path following component for about a week now.

I’ve overridden things like set next segment etc. But… but!

I can’t quite seem to fine out a method that works to hook when the nav path is crossing over a custom navmesh link (via a proxy). Specifically I’m doing this to allow my AI guys to climb ladders, but once this works they’ll be doing other traversal tasks.

What I know so far:

The ID that recast uses for the off mesh link is completely different than the ID value the navmesh link registered with the navigation system uses. Which is why I’m not seeing any of the hook functions being called.

What I can’t get to the bottom of yet, is how and why the ID values are different.

Now that’s not to say the AI doesn’t follow the path with the link in, the link IS followed, but I need to call custom code to allow the traversal, so the link IS in effect, its just the AI doesn’t know when its following a link-based segment of the path.

Thoughts? feelings? suggestions?

I’ve played around with this stuff a bit and had some success, but I definitely found it to be really awkward to customize as I wanted.
Not clear on what level you’re doing this (Registering a delegate on the smartlink component of an ANavLinkProxy? Or customized at a lower level?), without seeing some code it’s hard for me to suggest what the problem might be.

As for the id stuff, I have had similar issues at times. It seemed like it was perhaps related to PIE and when the navigation system was built. Like the link component would register itself with the nav system, but then get recreated by the construction script or duplication during PIE startup and end up with an id from a different world/nav system. I never really understood though, just changed stuff randomly until it seemed to work. It feels like there are just far too many ways the navigation stuff can get desynced and nowhere near enough diagnostic information to help you understand why.

Thanks Kamrann, definitely confirms what I suspected. My biggest issue is that I can’t find a place where the NavLink proxy actually registers itself with the navigation system and then that somehow gets transferred to recast. The only clue I can find is in AddTile on the recast impl class.

I suspect you might be right though, the NavLinkProxy generates a unique ID value for itself. But this is probably not in sequence with the id value generated for the offmeshlink in the recast impl. The values are completely different.

As for you question about the level I’m working. I’m trying to get a custom path following component (derived from crowd following component) to follow a path that allows for the path to go up and down ladders as well as other types of traversal (doors, climbing over fences etc). I’m placing navlink proxies so the pathfinder in recast is giving me the correct paths. But I don’t have a nice way of implementing traversing custom path segments like doors and ladders, mainly because the segment traversal callbacks don’t fire for custom nav links. So the virtual methods I’ve overridden for SetMoveSegment etc simply give me 0’s for the segment index etc.

I’ve fudged doors using triggers, but I can’t do the same for ladders without it being a really big fudge :slight_smile:

What I want is basically some method to override, or an event callback when traversing the path nodes that start/end the ladder. None of which are working. I guess the next option is to implement a custom navlink proxy and see if I can get a callback on that when my agent starts using the link. But I think I already tried that before and it didn’t work :slight_smile:

Doesn’t help that all of this stuff is kind of untested and undocumented :slight_smile:

I extended navlink proxy and used it to trigger a jump montage for a cat between a start and end point that are set in the Editor. It worked but was tricky to setup.

It was on a previous project that I worked on so I’m not in that project much. Here is the thread that I followed when I was making it. There are code examples. https://forums.unrealengine.com/showthread.php?3512-NavLink-and-making-AI-jump

For posterity and because its unlikely to get any better in the near term. Here’s what I found more recently (went away and did other code because it was so annoying).

So I have a custom UTacticalPathFollowingComponent derived from from UCrowdFollowingComponent which derives from UPathFollowingComponent. In that component, I override OnNavNodeChanged which gets passed the NavPolyRef (an integer that recast knows about) for the next “poly” in the path.

Now Recast stores “off mesh links” which is what you get with SmartLinks etc… those green arrowed things you get in editor? as poly’s in the specific Navmesh Tile, just ones with only two vertices and one edge. The verts are the start/end of the link.

Anyway, long story short, you need to get the NavMesh path you are following, then request the Recast Navigation Data from that. From there, there is a function called GetLinkEndPoints that will return true if that poly is a navlink and will return the vertex positions of the link alongside that.

So I’m currently digging through the code to understand 1) Why the UserID’s are always 0 when clearly they shouldn’t be and 2) How to get the area class of the link from the navpolyref so I can then map the area class to specific behavior (in this case ladders, doors, vaulting over fences etc). The use-case for all this being so I can actually organize custom animations to do the door opening, or the ladder climbing or whatever. But to get you started this code currently works as of 4.16.2:



void UTacticalPathFollowingComponent::OnNavNodeChanged(NavNodeRef NewPolyRef, NavNodeRef PrevPolyRef, int32 CorridorSize)
{
	Super::OnNavNodeChanged(NewPolyRef, PrevPolyRef, CorridorSize);
	if (GEngine)
	{
		GEngine->AddOnScreenDebugMessage(1, 5.0f, FColor::Emerald, *FString::Printf(TEXT("TacPathFollow: Changing Nav Poly")));
	}

	FNavMeshPath* NavPath = Path.IsValid() ? Path->CastPath<FNavMeshPath>() : NULL;
	if (NavPath)
	{
		ARecastNavMesh* RecastNavData = Cast<ARecastNavMesh>(NavPath->GetNavigationDataUsed());
		if (RecastNavData != nullptr)
		{
			FVector PtA, PtB;
			const bool bStartIsNavLink = RecastNavData->GetLinkEndPoints(NewPolyRef, PtA, PtB);
			if (bStartIsNavLink)
			{
				// path for next movement is a nav link.. so see how to traverse it by querying the associated class? Seems weird.
				if (GEngine)
				{
					GEngine->AddOnScreenDebugMessage(1, 5.0f, FColor::Emerald, *FString::Printf(TEXT("TacPathFollow: New Nav Poly has Custom Link")));
				}

			}
		}
	}
}


I’ll update this post once I’ve figured out 1 and 2 above.

Figured I might as well update this eons old thread… here’s how to get the link type (i.e. the “Nav Area Class”) in this case, I check if the class is of the “Ladder” type (which I’d declared earlier as a new UNavArea class). I then teleport to the other end of the ladder if it is (basically, the navlink has two points, so find the farthest one from us and teleport to it).

Obviously you’d want to transition into some kind of animation state machine thing if you were doing the ladder case.


void UTacticalPathFollowingComponent::OnNavNodeChanged(NavNodeRef NewPolyRef, NavNodeRef PrevPolyRef, int32 CorridorSize)
{
    Super::OnNavNodeChanged(NewPolyRef, PrevPolyRef, CorridorSize);
    if (GEngine)
    {
        GEngine->AddOnScreenDebugMessage(1, 5.0f, FColor::Emerald, *FString::Printf(TEXT("TacPathFollow: Changing Nav Poly")));
    }

    FNavMeshPath* NavPath = Path.IsValid() ? Path->CastPath<FNavMeshPath>() : NULL;
    if (NavPath)
    {
        ARecastNavMesh* RecastNavData = Cast<ARecastNavMesh>(NavPath->GetNavigationDataUsed());
        if (RecastNavData != nullptr)
        {
            FVector PtA, PtB;
            const bool bStartIsNavLink = RecastNavData->GetLinkEndPoints(NewPolyRef, PtA, PtB);
            if (bStartIsNavLink)
            {
                // path for next movement is a nav link.. so see how to traverse it by querying the associated class? Seems weird.
                if (GEngine)
                {
                    GEngine->AddOnScreenDebugMessage(1, 5.0f, FColor::Emerald, *FString::Printf(TEXT("TacPathFollow: New Nav Poly has Custom Link")));
                }

                uint32 id = RecastNavData->GetPolyAreaID(NewPolyRef);
                FNavMeshNodeFlags Flags;
                if (RecastNavData->GetPolyFlags(NewPolyRef, Flags))
                {
                    const UClass* AreaClass = RecastNavData->GetAreaClass(Flags.Area);
                    const UNavArea* DefArea = AreaClass ? ((UClass*)AreaClass)->GetDefaultObject<UNavArea>() : NULL;
                    if (DefArea)
                    {
                        if (GEngine)
                        {
                            GEngine->AddOnScreenDebugMessage(2, 5.0f, FColor::Emerald, *FString::Printf(TEXT("TacPathFollow: Area ID: %d Area Class: %s"),id,*DefArea->GetName()));
                        }


                        //const UNavArea_Ladder * LadderArea = AreaClass ? ((UClass*)AreaClass)->GetDefaultObject<UNavArea_Ladder>() : NULL;
                        if (AreaClass->IsChildOf(UNavArea_Ladder::StaticClass()))
                        {
                            AAIController * controller = Cast<AAIController>(GetOwner());
                            if (controller && controller->GetControlledPawn())
                            {
                                APawn * pawn = controller->GetControlledPawn();

                                // find the closest point in the navlink to us.. then teleport to the other one
                                float distToA = (pawn->GetActorLocation() - PtA).SizeSquared();
                                float distToB = (pawn->GetActorLocation() - PtB).SizeSquared();
                                if (distToB > distToA)
                                {
                                    //GetOwner()->SetActorLocation(PtB + FVector(0.0f,0.0f,200.0f));
                                    FVector loc = PtB + FVector(0.0f, 0.0f, pawn->GetDefaultHalfHeight());
                                    DrawDebugSphere(GetWorld(), loc, 40.0f, 8, FColor::Red, false, 5.0f);
                                    bool bMoved = pawn->TeleportTo(loc, GetOwner()->GetActorRotation());
                                }
                                else
                                {
                                    //GetOwner()->SetActorLocation(PtA + FVector(0.0f,0.0f,200.0f));
                                    FVector loc = PtA + FVector(0.0f, 0.0f, pawn->GetDefaultHalfHeight());
                                    DrawDebugSphere(GetWorld(), loc, 40.0f, 8, FColor::Cyan, false, 5.0f);
                                    pawn->TeleportTo(loc, GetOwner()->GetActorRotation());
                                }
                            }



                        }
                    }

                };
            }
        }
    }
}


2 Likes