What Is the Proper Method to Check if AI Can Reach a Location?

It looks like we’re making something very similar. I’m using the paragon assets to create an Orcs Must Die style game as well. I’ve been stuck on the AI pathfinding for days so I’m definitely going to follow this thread closely!

1 Like

That’s interesting to see MasterFuzzFuzz, I will show what I have come up with soon, on this thread :slight_smile:.

I have not quite finished my implementation of this custom AI yet, but for reference, with the new AI Controller constructor implementation signature (as shown below), the enemies no longer clump together, when trying to reach their objective :slight_smile::

AGruxAIController::AGruxAIController(const FObjectInitializer& ObjectInitializer)
	: Super(ObjectInitializer.SetDefaultSubobjectClass<UCrowdFollowingComponent>(TEXT("PathFollowingComponent")))

Okay then, here is a video showing what I have so far,* for what I have in mind. It is not finished yet (I have ideas on how to address some of the issues), but the basic structure of the behavior is there. I have also had the AActor::TakeDamage method overwritten for all custom characters/obstacles (instead of using my own custom method for Actors* that the Player/enemies should be able to destroy).

Update Video:

I have a solution now. It is not perfect, but it works as expected (I can tweak it in time).

For reference, the code for the task to have Grux attack a target wall/placeable, is below:

#include "GruxAttackTargetWall.h"
#include "AIController.h"
#include "BaseTPSTDPlaceable.h"
#include "ThirdPersonTD/GruxAICharacter.h"

EBTNodeResult::Type UGruxAttackTargetWall::ExecuteTask(UBehaviorTreeComponent& OwnerComponent, uint8* NodeMemory)
{
	EBTNodeResult::Type TaskNodeResult = EBTNodeResult::Failed;
	
	Super::ExecuteTask(OwnerComponent, NodeMemory);

	AGruxAICharacter* ControlledCharacter = Cast<AGruxAICharacter>(OwnerComponent.GetAIOwner()
		->GetCharacter());
	const ABaseTPSTDPlaceable* TargetPlaceable = Cast<ABaseTPSTDPlaceable>(ControlledCharacter->GetTargetPlaceable());

	// Attack the blocking wall:
	if (ControlledCharacter && TargetPlaceable && TargetPlaceable->GetPlaceableIntact())
	{
		ControlledCharacter->StartPrimaryAttack();
		TaskNodeResult = EBTNodeResult::InProgress;
	}
	// The wall is no more:
	else if (ControlledCharacter && TargetPlaceable && !TargetPlaceable->GetPlaceableIntact() ||
		ControlledCharacter && !TargetPlaceable)
	{
		ControlledCharacter->StopPrimaryAttack();
		TaskNodeResult = EBTNodeResult::Succeeded;
	}
	
	return TaskNodeResult;
}

The code for character melee-weapons is below too:

#include "MeleeCharacterWeapon.h"
#include "Kismet/GameplayStatics.h"
#include "ThirdPersonTD/GridSquare.h"
#include "ThirdPersonTD/GruxAICharacter.h"
#include "BaseTPSTDPlaceable.h"

// Initialise:
AMeleeCharacterWeapon::AMeleeCharacterWeapon()
{

}

void AMeleeCharacterWeapon::Attack(FTimerHandle AttackTimerHandle)
{
	if (!CharacterWeaponOwner)
	{
		CharacterWeaponOwner = Cast<ABaseThirdPersonTDCharacter>(GetOwner());
	}

	if (CharacterWeaponOwner)
	{
		const TArray<TEnumAsByte<EObjectTypeQuery>> ObjectTypes {
			UEngineTypes::ConvertToObjectType(ECC_WorldDynamic) };
		TArray<AActor*> ActorsToIgnore;
		UGameplayStatics::GetAllActorsOfClass(GetWorld(), AGruxAICharacter::StaticClass(), ActorsToIgnore);
		TArray<AActor*> OtherActorsToIgnore;
		UGameplayStatics::GetAllActorsOfClass(GetWorld(), AGridSquare::StaticClass(), OtherActorsToIgnore);
		// Make sure to ignore grid squares as well:
		ActorsToIgnore.Append(OtherActorsToIgnore);
		const FVector CurrentUserPosition = CharacterWeaponOwner->GetActorLocation();
		// Only trace for an Actor, if this Actor has moved, or if the Actor they have
		// been hitting, is no longer valid:
		if (CurrentUserPosition != LastUserPosition || !CurrentHitPlaceable->IsValidLowLevel())
		{
			FHitResult SphereTraceHitResult;
			LastUserPosition = CurrentUserPosition;
			UKismetSystemLibrary::SphereTraceSingleForObjects(GetWorld(), GetActorLocation(),
				GetActorForwardVector() * CoreMeleeWeaponProperties.WeaponRange, CoreMeleeWeaponProperties.SphereCastRadius,
				ObjectTypes,false, ActorsToIgnore, EDrawDebugTrace::ForDuration, SphereTraceHitResult, true);
			const TWeakObjectPtr<AActor> HitActor = SphereTraceHitResult.Actor;
			if (HitActor->IsValidLowLevel())
			{
				CurrentHitPlaceable = Cast<ABaseTPSTDPlaceable>(HitActor.Get());
			}
		}

		// Damage the current placeable object:
		if (CurrentHitPlaceable->IsValidLowLevel() && CurrentHitPlaceable->GetPlaceableIntact())
		{
			const FDamageEvent MeleeDamageEvent = FDamageEvent(CoreMeleeWeaponProperties.WeaponDamageType);
			CurrentHitPlaceable->TakeDamage(CoreMeleeWeaponProperties.WeaponDamage, MeleeDamageEvent,
				CharacterWeaponOwner->GetController(), this);
		}
		// Stop attacking:
		else if (CurrentHitPlaceable->IsValidLowLevel() && !CurrentHitPlaceable->GetPlaceableIntact())
		{
			CharacterWeaponOwner->StopPrimaryAttack();
		}
	}
}

FMeleeWeaponProperties& AMeleeCharacterWeapon::GetCoreMeleeWeaponProperties()
{
	return CoreMeleeWeaponProperties;
}

Most of all though, in Grux’s behavior tree, simply using a Move To task-node, instead of my custom one, causes Grux to carry onto their objective, after destroying a wall or two.

A video showing all of this together, is below:

(Yes, one of the instances of Grux gets stuck in the floor/wall, that is something I will have to resolve in the future too).*

Please let me know if you would like any more details on this solution, before I mark this post as the solution.