// Copyright Epic Games, Inc. All Rights Reserved. #include "Variant_Shooter/AI/ShooterStateTreeUtility.h" #include "StateTreeExecutionContext.h" #include "ShooterNPC.h" #include "Camera/CameraComponent.h" #include "AIController.h" #include "Perception/AIPerceptionComponent.h" #include "ShooterAIController.h" #include "StateTreeAsyncExecutionContext.h" bool FStateTreeLineOfSightToTargetCondition::TestCondition(FStateTreeExecutionContext& Context) const { const FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // ensure the target is valid if (!IsValid(InstanceData.Target)) { return !InstanceData.bMustHaveLineOfSight; } // check if the character is facing towards the target const FVector TargetDir = (InstanceData.Target->GetActorLocation() - InstanceData.Character->GetActorLocation()).GetSafeNormal(); const float FacingDot = FVector::DotProduct(TargetDir, InstanceData.Character->GetActorForwardVector()); const float MaxDot = FMath::Cos(FMath::DegreesToRadians(InstanceData.LineOfSightConeAngle)); // is the facing outside of our cone half angle? if (FacingDot <= MaxDot) { return !InstanceData.bMustHaveLineOfSight; } // get the target's bounding box FVector CenterOfMass, Extent; InstanceData.Target->GetActorBounds(true, CenterOfMass, Extent, false); // divide the vertical extent by the number of line of sight checks we'll do const float ExtentZOffset = Extent.Z * 2.0f / InstanceData.NumberOfVerticalLineOfSightChecks; // get the character's camera location as the source for the line checks const FVector Start = InstanceData.Character->GetFirstPersonCameraComponent()->GetComponentLocation(); // ignore the character and target. We want to ensure there's an unobstructed trace not counting them FCollisionQueryParams QueryParams; QueryParams.AddIgnoredActor(InstanceData.Character); QueryParams.AddIgnoredActor(InstanceData.Target); FHitResult OutHit; // run a number of vertically offset line traces to the target location for (int32 i = 0; i < InstanceData.NumberOfVerticalLineOfSightChecks - 1; ++i) { // calculate the endpoint for the trace const FVector End = CenterOfMass + FVector(0.0f, 0.0f, Extent.Z - ExtentZOffset * i); InstanceData.Character->GetWorld()->LineTraceSingleByChannel(OutHit, Start, End, ECC_Visibility, QueryParams); // is the trace unobstructed? if (!OutHit.bBlockingHit) { // we only need one unobstructed trace, so terminate early return InstanceData.bMustHaveLineOfSight; } } // no line of sight found return !InstanceData.bMustHaveLineOfSight; } #if WITH_EDITOR FText FStateTreeLineOfSightToTargetCondition::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const { return FText::FromString("Has Line of Sight"); } #endif //////////////////////////////////////////////////////////////////// EStateTreeRunStatus FStateTreeFaceActorTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // have we transitioned from another state? if (Transition.ChangeType == EStateTreeStateChangeType::Changed) { // get the instance data FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // set the AI Controller's focus InstanceData.Controller->SetFocus(InstanceData.ActorToFaceTowards); } return EStateTreeRunStatus::Running; } void FStateTreeFaceActorTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // have we transitioned to another state? if (Transition.ChangeType == EStateTreeStateChangeType::Changed) { // get the instance data FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // clear the AI Controller's focus InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay); } } #if WITH_EDITOR FText FStateTreeFaceActorTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const { return FText::FromString("Face Towards Actor"); } #endif // WITH_EDITOR //////////////////////////////////////////////////////////////////// EStateTreeRunStatus FStateTreeFaceLocationTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // have we transitioned from another state? if (Transition.ChangeType == EStateTreeStateChangeType::Changed) { // get the instance data FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // set the AI Controller's focus InstanceData.Controller->SetFocalPoint(InstanceData.FaceLocation); } return EStateTreeRunStatus::Running; } void FStateTreeFaceLocationTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // have we transitioned to another state? if (Transition.ChangeType == EStateTreeStateChangeType::Changed) { // get the instance data FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // clear the AI Controller's focus InstanceData.Controller->ClearFocus(EAIFocusPriority::Gameplay); } } #if WITH_EDITOR FText FStateTreeFaceLocationTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const { return FText::FromString("Face Towards Location"); } #endif // WITH_EDITOR //////////////////////////////////////////////////////////////////// EStateTreeRunStatus FStateTreeSetRandomFloatTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // have we transitioned to another state? if (Transition.ChangeType == EStateTreeStateChangeType::Changed) { // get the instance data FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // calculate the output value InstanceData.OutValue = FMath::RandRange(InstanceData.MinValue, InstanceData.MaxValue); } return EStateTreeRunStatus::Running; } #if WITH_EDITOR FText FStateTreeSetRandomFloatTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const { return FText::FromString("Set Random Float"); } #endif // WITH_EDITOR //////////////////////////////////////////////////////////////////// EStateTreeRunStatus FStateTreeShootAtTargetTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // have we transitioned from another state? if (Transition.ChangeType == EStateTreeStateChangeType::Changed) { // get the instance data FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // tell the character to shoot the target InstanceData.Character->StartShooting(InstanceData.Target); } return EStateTreeRunStatus::Running; } void FStateTreeShootAtTargetTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // have we transitioned to another state? if (Transition.ChangeType == EStateTreeStateChangeType::Changed) { // get the instance data FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // tell the character to stop shooting InstanceData.Character->StopShooting(); } } #if WITH_EDITOR FText FStateTreeShootAtTargetTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const { return FText::FromString("Shoot at Target"); } #endif // WITH_EDITOR EStateTreeRunStatus FStateTreeSenseEnemiesTask::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // have we transitioned from another state? if (Transition.ChangeType == EStateTreeStateChangeType::Changed) { // get the instance data FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // bind the perception updated delegate on the controller InstanceData.Controller->OnShooterPerceptionUpdated.BindLambda( [WeakContext = Context.MakeWeakExecutionContext()](AActor* SensedActor, const FAIStimulus& Stimulus) { // get the instance data inside the lambda const FStateTreeStrongExecutionContext StrongContext = WeakContext.MakeStrongExecutionContext(); if (FInstanceDataType* LambdaInstanceData = StrongContext.GetInstanceDataPtr()) { if (SensedActor->ActorHasTag(LambdaInstanceData->SenseTag)) { bool bDirectLOS = false; // calculate the direction of the stimulus const FVector StimulusDir = (Stimulus.StimulusLocation - LambdaInstanceData->Character->GetActorLocation()).GetSafeNormal(); // infer the angle from the dot product between the character facing and the stimulus direction const float DirDot = FVector::DotProduct(StimulusDir, LambdaInstanceData->Character->GetActorForwardVector()); const float MaxDot = FMath::Cos(FMath::DegreesToRadians(LambdaInstanceData->DirectLineOfSightCone)); // is the direction within our perception cone? if (DirDot >= MaxDot) { // run a line trace between the character and the sensed actor FCollisionQueryParams QueryParams; QueryParams.AddIgnoredActor(LambdaInstanceData->Character); QueryParams.AddIgnoredActor(SensedActor); FHitResult OutHit; // we have direct line of sight if this trace is unobstructed bDirectLOS = !LambdaInstanceData->Character->GetWorld()->LineTraceSingleByChannel(OutHit, LambdaInstanceData->Character->GetActorLocation(), SensedActor->GetActorLocation(), ECC_Visibility, QueryParams); } // check if we have a direct line of sight to the stimulus if (bDirectLOS) { // set the controller's target LambdaInstanceData->Controller->SetCurrentTarget(SensedActor); // set the task output LambdaInstanceData->TargetActor = SensedActor; // set the flags LambdaInstanceData->bHasTarget = true; LambdaInstanceData->bHasInvestigateLocation = false; // no direct line of sight to target } else { // if we already have a target, ignore the partial sense and keep on them if (!IsValid(LambdaInstanceData->TargetActor)) { // is this stimulus stronger than the last one we had? if (Stimulus.Strength > LambdaInstanceData->LastStimulusStrength) { // update the stimulus strength LambdaInstanceData->LastStimulusStrength = Stimulus.Strength; // set the investigate location LambdaInstanceData->InvestigateLocation = Stimulus.StimulusLocation; // set the investigate flag LambdaInstanceData->bHasInvestigateLocation = true; } } } } } } ); // bind the perception forgotten delegate on the controller InstanceData.Controller->OnShooterPerceptionForgotten.BindLambda( [WeakContext = Context.MakeWeakExecutionContext()](AActor* SensedActor) { // get the instance data inside the lambda FInstanceDataType* LambdaInstanceData = WeakContext.MakeStrongExecutionContext().GetInstanceDataPtr(); if (!LambdaInstanceData) { return; } bool bForget = false; // are we forgetting the current target? if (SensedActor == LambdaInstanceData->TargetActor) { bForget = true; } else { // are we forgetting about a partial sense? if (!IsValid(LambdaInstanceData->TargetActor)) { bForget = true; } } if (bForget) { // clear the target LambdaInstanceData->TargetActor = nullptr; // clear the flags LambdaInstanceData->bHasInvestigateLocation = false; LambdaInstanceData->bHasTarget = false; // reset the stimulus strength LambdaInstanceData->LastStimulusStrength = 0.0f; // clear the target on the controller LambdaInstanceData->Controller->ClearCurrentTarget(); LambdaInstanceData->Controller->ClearFocus(EAIFocusPriority::Gameplay); } } ); } return EStateTreeRunStatus::Running; } void FStateTreeSenseEnemiesTask::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const { // have we transitioned to another state? if (Transition.ChangeType == EStateTreeStateChangeType::Changed) { // get the instance data FInstanceDataType& InstanceData = Context.GetInstanceData(*this); // unbind the perception delegates InstanceData.Controller->OnShooterPerceptionUpdated.Unbind(); InstanceData.Controller->OnShooterPerceptionForgotten.Unbind(); } } #if WITH_EDITOR FText FStateTreeSenseEnemiesTask::GetDescription(const FGuid& ID, FStateTreeDataView InstanceDataView, const IStateTreeBindingLookup& BindingLookup, EStateTreeNodeFormatting Formatting /*= EStateTreeNodeFormatting::Text*/) const { return FText::FromString("Sense Enemies"); } #endif // WITH_EDITOR