I tried to implement simple squad system for the AI that allows AI of the same type to join the same squad and share information about enemies. Post includes a lot of code. Marked the important bits with bold.
The TLDR: Squad UObject has an array for the UEntruliaAIControllers that are in the Squad, but sometimes those pointers are invalid / point to invalid objects. Or the pointer to Squad UObject is pointing to a UObject that has already been destroyed.
My squad code:
The header file:
// Copyright Entrulia Team 2016
#pragma once
#include "EntrulianSquad.generated.h"
class APawn;
UCLASS(BlueprintType, Category = "AI")
class UEntrulianSquad : public UObject
{
GENERATED_BODY()
public:
UEntrulianSquad();
void AddMember(class AEntruliaAIController *Controller);
void RemoveMember(class AEntruliaAIController *Controller);
void OnSeePawn(class APawn *Pawn);
bool CanBeAddedToSquad(class AEntruliaCharacter *Character);
static bool CanBeInSameSquad(class AEntruliaCharacter *Character1, class AEntruliaCharacter *Character2);
UFUNCTION(BlueprintPure, Category = "Entrulian")
bool IsInSquad(class AEntruliaCharacter *Character);
UFUNCTION(BlueprintPure, Category = "Entrulian")
int32 Num() const { return Members.Num(); }
private:
UPROPERTY()
TArray<AEntruliaAIController*> Members;
};
And the .cpp file.
// Copyright Entrulia Team 2016
#include "Entrulia.h"
#include "EntruliaAIController.h"
#include "EntruliaCharacter.h"
#include "EntrulianSquad.h"
//=================================================================
//
//=================================================================
UEntrulianSquad::UEntrulianSquad()
{
}
//=================================================================
//
//=================================================================
void UEntrulianSquad::AddMember(class AEntruliaAIController *Controller)
{
if (!Controller || Controller->IsPendingKill())
{
UE_LOG(LogTemp, Warning, TEXT("Tried to add invalid controller to squad"));
return;
}
Members.AddUnique(Controller);
//UE_LOG(LogTemp, Warning, TEXT("Squad %s now has %d members"), *GetName(), Members.Num());
}
//=================================================================
//
//=================================================================
void UEntrulianSquad::RemoveMember(class AEntruliaAIController *Controller)
{
Members.Remove(Controller);
}
//=================================================================
//
//=================================================================
void UEntrulianSquad::OnSeePawn(class APawn *Pawn)
{
for (int32 i = 0; i < Members.Num(); i++)
{
Members*->OnSeePawn(Pawn, false);
}
}
//=================================================================
//
//=================================================================
bool UEntrulianSquad::IsInSquad(class AEntruliaCharacter *Character)
{
for (int32 i = Members.Num() - 1; i >= 0; i--)
{
class AEntruliaCharacter *SquadMember = Members*->GetEntrulian();
if (SquadMember == Character)
return true;
}
return false;
}
//=================================================================
//
//=================================================================
bool UEntrulianSquad::CanBeAddedToSquad(class AEntruliaCharacter *Entrulian)
{
//Check that everyone in the squad agrees they can be added
for (int32 i = 0; i < Members.Num(); i++)
{
class AEntruliaCharacter *SquadMember = Members*->GetEntrulian();
if (!SquadMember)
continue;
if (CanBeInSameSquad(SquadMember, Entrulian))
continue;
return false;
}
return true;
}
//=================================================================
//
//=================================================================
bool UEntrulianSquad::CanBeInSameSquad(class AEntruliaCharacter *Character1, class AEntruliaCharacter *Character2)
{
//Always allow to join people of same type
if (Character1->GetCharacterType() == Character2->GetCharacterType())
return true;
//If we are a guard allow people of higher ranking to join
if (Character1->GetCharacterType() >= ECharacterType::Guard && Character2->GetCharacterType() >= ECharacterType::Guard)
return true;
return false;
}
The part of the AI Controller that handles joining squad:
//=================================================================
//
//=================================================================
void AEntruliaAIController::TryJoinSquad(class AEntruliaCharacter *MyPawn, class AEntruliaCharacter *OtherPawn)
{
if (Squad && Squad->Num() > 1)
{
return;
}
class AEntruliaAIController *OtherController = OtherPawn->GetEntrulianAIController();
if (!OtherController)
return;
if (OtherController->GetSquad())
{
if (!OtherController->GetSquad()->CanBeAddedToSquad(MyPawn))
return;
if (Squad)
Squad->RemoveMember(this);
Squad = OtherController->GetSquad();
Squad->AddMember(this);
return;
}
//Tell them to join our squad instead
if (Squad)
{
OtherController->TryJoinSquad(OtherPawn, MyPawn);
return;
}
Squad = NewObject<UEntrulianSquad>();
Squad->AddMember(this);
OtherController->TryJoinSquad(OtherPawn, MyPawn);
}
Also, for the sake of full explanation. The code that shares the information between the squad members.
//=================================================================
//
//=================================================================
void AEntruliaAIController::OnSeePawn(class APawn *EnemyPawn, bool LinkToSquad)
{
//Sanity check
if (!EnemyPawn || EnemyPawn == GetPawn())
{
return;
}
//Link to all members of the squad if this is coming from the character OnSeePawn
if (LinkToSquad && Squad)
{
Squad->OnSeePawn(EnemyPawn);
return;
}
int32 iIndex = FindEnemyInfo(EnemyPawn);
if (iIndex != -1)
{
Enemies[iIndex].TimeSinceLastSeen = 0;
}
else
{
FEnemyInfo info;
info.Enemy = EnemyPawn;
info.TimeSinceLastSeen = 0;
Enemies.Add(info);
}
}
But the code often crashes on UEntrulianSquad::IsInSquad(). It seems that Members* can often be NULL even though it shouldn’t be. I am not sure if I am using NewObject correctly. It seems that the Squad object:
private:
UPROPERTY()
UEntrulianSquad *Squad;
sometimes gets destroyed even though there’s still squad members pointing to it? Or sometimes the Members Array are pointing to NULL in the squad code even though that shouldn’t be possible? Any hints what I am doing wrong. It is often when the first member of the squad dies that the game suddenly crashes. Even though other members of the squad should have a pointer to the squad and it should not get garbage collected.
EDIT: should mention I do this in my BeginDestroy of the controller:
//=================================================================
//
//=================================================================
void AEntruliaAIController::BeginDestroy()
{
PotentialItem = NULL;
FollowTarget = NULL;
PathTarget = NULL;
OccludedActors.SetNum(0);
Enemies.SetNum(0);
if (Squad)
{
Squad->RemoveMember(this);
Squad = NULL;
}
Super::BeginDestroy();
}