Add Scripted state and bAutoStartBehavior for external NPC control
Allow NPCs to start without a Behavior Tree and be controlled externally via Blueprint, Level Sequences, or triggers. Adds Scripted enum state that prevents BT services from overriding state, plus StartBehavior/StopBehavior BlueprintCallable functions to toggle BT execution at runtime. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a31ac1d782
commit
c1afddc8b7
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -25,6 +25,9 @@ void UPS_AI_Behavior_BTService_EvaluateReaction::TickNode(
|
|||||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||||
if (!AIC) { UE_LOG(LogPS_AI_Behavior, Warning, TEXT("EvaluateReaction: no AIC!")); return; }
|
if (!AIC) { UE_LOG(LogPS_AI_Behavior, Warning, TEXT("EvaluateReaction: no AIC!")); return; }
|
||||||
|
|
||||||
|
// Scripted state: external control — don't touch the state
|
||||||
|
if (AIC->GetBehaviorState() == EPS_AI_Behavior_State::Scripted) { return; }
|
||||||
|
|
||||||
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
UPS_AI_Behavior_PersonalityComponent* Personality = AIC->GetPersonalityComponent();
|
||||||
if (!Personality) { UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] EvaluateReaction: no PersonalityComponent!"), *AIC->GetName()); return; }
|
if (!Personality) { UE_LOG(LogPS_AI_Behavior, Warning, TEXT("[%s] EvaluateReaction: no PersonalityComponent!"), *AIC->GetName()); return; }
|
||||||
|
|
||||||
|
|||||||
@ -25,6 +25,9 @@ void UPS_AI_Behavior_BTService_UpdateThreat::TickNode(
|
|||||||
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
APS_AI_Behavior_AIController* AIC = Cast<APS_AI_Behavior_AIController>(OwnerComp.GetAIOwner());
|
||||||
if (!AIC) return;
|
if (!AIC) return;
|
||||||
|
|
||||||
|
// Scripted state: external control — don't accumulate threat
|
||||||
|
if (AIC->GetBehaviorState() == EPS_AI_Behavior_State::Scripted) { return; }
|
||||||
|
|
||||||
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
UBlackboardComponent* BB = OwnerComp.GetBlackboardComponent();
|
||||||
if (!BB) return;
|
if (!BB) return;
|
||||||
|
|
||||||
|
|||||||
@ -80,12 +80,24 @@ void APS_AI_Behavior_AIController::OnPossess(APawn* InPawn)
|
|||||||
}
|
}
|
||||||
|
|
||||||
SetupBlackboard();
|
SetupBlackboard();
|
||||||
StartBehavior();
|
|
||||||
|
if (bAutoStartBehavior)
|
||||||
|
{
|
||||||
|
StartBehavior();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Stay in Scripted state — no BT, no services, controlled externally
|
||||||
|
SetBehaviorState(EPS_AI_Behavior_State::Scripted);
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] bAutoStartBehavior=false — entering Scripted state."),
|
||||||
|
*GetName());
|
||||||
|
}
|
||||||
|
|
||||||
TryBindConversationAgent();
|
TryBindConversationAgent();
|
||||||
TryBindGazeComponent();
|
TryBindGazeComponent();
|
||||||
|
|
||||||
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Possessed Pawn '%s' — BT started, TeamId=%d."),
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Possessed Pawn '%s' — TeamId=%d, AutoStart=%d."),
|
||||||
*GetName(), *InPawn->GetName(), TeamId);
|
*GetName(), *InPawn->GetName(), TeamId, (int32)bAutoStartBehavior);
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_AI_Behavior_AIController::OnUnPossess()
|
void APS_AI_Behavior_AIController::OnUnPossess()
|
||||||
@ -227,24 +239,84 @@ void APS_AI_Behavior_AIController::SetupBlackboard()
|
|||||||
|
|
||||||
void APS_AI_Behavior_AIController::StartBehavior()
|
void APS_AI_Behavior_AIController::StartBehavior()
|
||||||
{
|
{
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] StartBehavior called. Pawn=%s, HasBB=%d"),
|
||||||
|
*GetName(),
|
||||||
|
GetPawn() ? *GetPawn()->GetName() : TEXT("null"),
|
||||||
|
Blackboard != nullptr);
|
||||||
|
|
||||||
|
// Check if a BT was previously loaded and stopped (StopBehavior case)
|
||||||
|
UBehaviorTreeComponent* BTComp = Cast<UBehaviorTreeComponent>(GetBrainComponent());
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] BrainComponent=%s, BTComp=%s, CurrentTree=%s, IsRunning=%d"),
|
||||||
|
*GetName(),
|
||||||
|
GetBrainComponent() ? *GetBrainComponent()->GetName() : TEXT("null"),
|
||||||
|
BTComp ? *BTComp->GetName() : TEXT("null"),
|
||||||
|
(BTComp && BTComp->GetCurrentTree()) ? *BTComp->GetCurrentTree()->GetName() : TEXT("null"),
|
||||||
|
BTComp ? (int32)BTComp->IsRunning() : -1);
|
||||||
|
|
||||||
|
if (BTComp && BTComp->GetCurrentTree() && !BTComp->IsRunning())
|
||||||
|
{
|
||||||
|
BTComp->RestartLogic();
|
||||||
|
|
||||||
|
if (GetBehaviorState() == EPS_AI_Behavior_State::Scripted)
|
||||||
|
{
|
||||||
|
SetBehaviorState(EPS_AI_Behavior_State::Idle);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] StartBehavior — BT restarted (RestartLogic). IsRunning=%d"),
|
||||||
|
*GetName(), (int32)BTComp->IsRunning());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First-time start: load and run the BT
|
||||||
UBehaviorTree* BTToRun = BehaviorTreeAsset;
|
UBehaviorTree* BTToRun = BehaviorTreeAsset;
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] BehaviorTreeAsset=%s"),
|
||||||
|
*GetName(), BTToRun ? *BTToRun->GetName() : TEXT("null"));
|
||||||
|
|
||||||
// Fallback: get from personality profile
|
// Fallback: get from personality profile
|
||||||
if (!BTToRun && PersonalityComp && PersonalityComp->Profile)
|
if (!BTToRun && PersonalityComp && PersonalityComp->Profile)
|
||||||
{
|
{
|
||||||
BTToRun = PersonalityComp->Profile->DefaultBehaviorTree.LoadSynchronous();
|
BTToRun = PersonalityComp->Profile->DefaultBehaviorTree.LoadSynchronous();
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Loaded from Profile: %s"),
|
||||||
|
*GetName(), BTToRun ? *BTToRun->GetName() : TEXT("null"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (BTToRun)
|
if (!BTToRun)
|
||||||
{
|
|
||||||
RunBehaviorTree(BTToRun);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
UE_LOG(LogPS_AI_Behavior, Warning,
|
UE_LOG(LogPS_AI_Behavior, Warning,
|
||||||
TEXT("[%s] No BehaviorTree assigned and none in PersonalityProfile — NPC will be inert."),
|
TEXT("[%s] StartBehavior FAILED — No BehaviorTree assigned and none in PersonalityProfile."),
|
||||||
*GetName());
|
*GetName());
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool bSuccess = RunBehaviorTree(BTToRun);
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] RunBehaviorTree('%s') returned %d"),
|
||||||
|
*GetName(), *BTToRun->GetName(), (int32)bSuccess);
|
||||||
|
|
||||||
|
// Check post-run state
|
||||||
|
BTComp = Cast<UBehaviorTreeComponent>(GetBrainComponent());
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] Post-run: BTComp=%s, IsRunning=%d, BB=%s"),
|
||||||
|
*GetName(),
|
||||||
|
BTComp ? *BTComp->GetName() : TEXT("null"),
|
||||||
|
BTComp ? (int32)BTComp->IsRunning() : -1,
|
||||||
|
Blackboard ? TEXT("valid") : TEXT("null"));
|
||||||
|
|
||||||
|
if (bSuccess && GetBehaviorState() == EPS_AI_Behavior_State::Scripted)
|
||||||
|
{
|
||||||
|
SetBehaviorState(EPS_AI_Behavior_State::Idle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void APS_AI_Behavior_AIController::StopBehavior()
|
||||||
|
{
|
||||||
|
if (UBrainComponent* Brain = GetBrainComponent())
|
||||||
|
{
|
||||||
|
Brain->StopLogic(TEXT("Scripted"));
|
||||||
|
}
|
||||||
|
|
||||||
|
StopMovement();
|
||||||
|
SetBehaviorState(EPS_AI_Behavior_State::Scripted);
|
||||||
|
|
||||||
|
UE_LOG(LogPS_AI_Behavior, Log, TEXT("[%s] StopBehavior — BT stopped, entering Scripted state."), *GetName());
|
||||||
}
|
}
|
||||||
|
|
||||||
void APS_AI_Behavior_AIController::SetBehaviorState(EPS_AI_Behavior_State NewState)
|
void APS_AI_Behavior_AIController::SetBehaviorState(EPS_AI_Behavior_State NewState)
|
||||||
@ -274,6 +346,12 @@ void APS_AI_Behavior_AIController::SetBehaviorState(EPS_AI_Behavior_State NewSta
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Scripted: stop movement, NPC stays alive ──────────────
|
||||||
|
if (NewState == EPS_AI_Behavior_State::Scripted)
|
||||||
|
{
|
||||||
|
StopMovement();
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Dead: shut down all AI systems ─────────────────────────
|
// ─── Dead: shut down all AI systems ─────────────────────────
|
||||||
if (NewState == EPS_AI_Behavior_State::Dead)
|
if (NewState == EPS_AI_Behavior_State::Dead)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -72,6 +72,11 @@ EPS_AI_Behavior_State UPS_AI_Behavior_PersonalityComponent::EvaluateReaction() c
|
|||||||
return EPS_AI_Behavior_State::Dead;
|
return EPS_AI_Behavior_State::Dead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CurrentState == EPS_AI_Behavior_State::Scripted)
|
||||||
|
{
|
||||||
|
return EPS_AI_Behavior_State::Scripted;
|
||||||
|
}
|
||||||
|
|
||||||
const float Courage = GetTrait(EPS_AI_Behavior_TraitAxis::Courage);
|
const float Courage = GetTrait(EPS_AI_Behavior_TraitAxis::Courage);
|
||||||
const float Aggressivity = GetTrait(EPS_AI_Behavior_TraitAxis::Aggressivity);
|
const float Aggressivity = GetTrait(EPS_AI_Behavior_TraitAxis::Aggressivity);
|
||||||
const float Caution = GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
const float Caution = GetTrait(EPS_AI_Behavior_TraitAxis::Caution);
|
||||||
|
|||||||
@ -59,6 +59,10 @@ public:
|
|||||||
|
|
||||||
// ─── Configuration ──────────────────────────────────────────────────
|
// ─── Configuration ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** If false, the Behavior Tree does NOT start on possess. Call StartBehavior() manually. */
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Behavior")
|
||||||
|
bool bAutoStartBehavior = true;
|
||||||
|
|
||||||
/** Behavior Tree to run. If null, uses the Profile's DefaultBehaviorTree. */
|
/** Behavior Tree to run. If null, uses the Profile's DefaultBehaviorTree. */
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Behavior")
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Behavior")
|
||||||
TObjectPtr<UBehaviorTree> BehaviorTreeAsset;
|
TObjectPtr<UBehaviorTree> BehaviorTreeAsset;
|
||||||
@ -107,6 +111,14 @@ public:
|
|||||||
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Blackboard")
|
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior|Blackboard")
|
||||||
EPS_AI_Behavior_State GetBehaviorState() const;
|
EPS_AI_Behavior_State GetBehaviorState() const;
|
||||||
|
|
||||||
|
/** Start (or restart) the Behavior Tree. If state is Scripted, transitions to Idle. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior")
|
||||||
|
void StartBehavior();
|
||||||
|
|
||||||
|
/** Stop the Behavior Tree and enter Scripted state. NPC stays alive and perceptible. */
|
||||||
|
UFUNCTION(BlueprintCallable, Category = "PS AI Behavior")
|
||||||
|
void StopBehavior();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
virtual void OnPossess(APawn* InPawn) override;
|
virtual void OnPossess(APawn* InPawn) override;
|
||||||
virtual void OnUnPossess() override;
|
virtual void OnUnPossess() override;
|
||||||
@ -126,9 +138,6 @@ private:
|
|||||||
/** Initialize Blackboard with required keys. */
|
/** Initialize Blackboard with required keys. */
|
||||||
void SetupBlackboard();
|
void SetupBlackboard();
|
||||||
|
|
||||||
/** Start the Behavior Tree (from asset or profile). */
|
|
||||||
void StartBehavior();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to bind to PS_AI_ConvAgent_ElevenLabsComponent if present on the Pawn.
|
* Attempt to bind to PS_AI_ConvAgent_ElevenLabsComponent if present on the Pawn.
|
||||||
* Uses UObject reflection — no compile-time dependency on PS_AI_ConvAgent.
|
* Uses UObject reflection — no compile-time dependency on PS_AI_ConvAgent.
|
||||||
|
|||||||
@ -43,6 +43,7 @@ enum class EPS_AI_Behavior_State : uint8
|
|||||||
Fleeing UMETA(DisplayName = "Fleeing"),
|
Fleeing UMETA(DisplayName = "Fleeing"),
|
||||||
TakingCover UMETA(DisplayName = "Taking Cover"),
|
TakingCover UMETA(DisplayName = "Taking Cover"),
|
||||||
Dead UMETA(DisplayName = "Dead"),
|
Dead UMETA(DisplayName = "Dead"),
|
||||||
|
Scripted UMETA(DisplayName = "Scripted"),
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user