├── .gitignore ├── Config └── FilterPlugin.ini ├── Content ├── EditorResources │ └── S_Waypoint.uasset └── Tasks │ ├── BTTask_PushNextWaypoint.uasset │ └── BTTask_WaitAtWaypoint.uasset ├── LICENSE ├── README.md ├── Resources ├── Icon128.png ├── Icon256.png ├── Icon512.png └── Slate │ └── Icons │ ├── waypoint_icon_16x.png │ ├── waypoint_icon_20x.png │ ├── waypoint_icon_256x.png │ ├── waypoint_icon_40x.png │ └── waypoint_icon_64x.png ├── Source ├── Waypoints │ ├── Private │ │ ├── BTTask_MoveToNextWaypoint.cpp │ │ ├── Waypoint.cpp │ │ ├── WaypointLoop.cpp │ │ └── WaypointsModule.cpp │ ├── Public │ │ ├── BTTask_MoveToNextWaypoint.h │ │ ├── Waypoint.h │ │ ├── WaypointLoop.h │ │ └── WaypointsModule.h │ └── Waypoints.Build.cs └── WaypointsEditorExtension │ ├── Private │ └── WaypointsEditorExtensionModule.cpp │ ├── Public │ └── WaypointsEditorExtensionModule.h │ └── WaypointsEditorExtension.Build.cs ├── Waypoints.uplugin └── demonstration.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | 22 | # Compiled Static libraries 23 | *.lai 24 | *.la 25 | *.a 26 | *.lib 27 | 28 | # Executables 29 | *.exe 30 | *.out 31 | *.app 32 | *.ipa 33 | 34 | # These project files can be generated by the engine 35 | *.xcodeproj 36 | *.xcworkspace 37 | *.sln 38 | *.suo 39 | *.opensdf 40 | *.sdf 41 | *.VC.db 42 | *.VC.opendb 43 | 44 | # Precompiled Assets 45 | SourceArt/**/*.png 46 | SourceArt/**/*.tga 47 | 48 | # Binary Files 49 | Binaries/* 50 | Plugins/*/Binaries/* 51 | 52 | # Builds 53 | Build/* 54 | 55 | # Whitelist PakBlacklist-.txt files 56 | !Build/*/ 57 | Build/*/** 58 | !Build/*/PakBlacklist*.txt 59 | 60 | # Don't ignore icon files in Build 61 | !Build/**/*.ico 62 | 63 | # Built data for maps 64 | *_BuiltData.uasset 65 | 66 | # Configuration files generated by the Editor 67 | Saved/* 68 | 69 | # Compiled source files for the engine to use 70 | Intermediate/* 71 | Plugins/*/Intermediate/* 72 | 73 | # Cache files for the editor to use 74 | DerivedDataCache/* -------------------------------------------------------------------------------- /Config/FilterPlugin.ini: -------------------------------------------------------------------------------- 1 | [FilterPlugin] 2 | ; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and 3 | ; may include "...", "*", and "?" wildcards to match directories, files, and individual characters respectively. 4 | ; 5 | ; Examples: 6 | ; /README.txt 7 | ; /Extras/... 8 | ; /Binaries/ThirdParty/*.dll 9 | -------------------------------------------------------------------------------- /Content/EditorResources/S_Waypoint.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Content/EditorResources/S_Waypoint.uasset -------------------------------------------------------------------------------- /Content/Tasks/BTTask_PushNextWaypoint.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Content/Tasks/BTTask_PushNextWaypoint.uasset -------------------------------------------------------------------------------- /Content/Tasks/BTTask_WaitAtWaypoint.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Content/Tasks/BTTask_WaitAtWaypoint.uasset -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Nicholas Chalkley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Waypoints 2 | 3 | Have you ever wanted to make a loop for your UE4 AI guards to patrol, only to find disappointment when you realize that there isn't already a tool like that in Unreal Engine? Well, if that's you, then this is the plugin for you! 4 | 5 | ![Demonstration](/demonstration.gif) 6 | 7 | ### Plugin Features 8 | 9 | - Easy 2 use 10 | - Free as in freedom 11 | - Free as in free beer 12 | 13 | # How to Use the Plugin 14 | 15 | 1. Put the plugin inside your project's plugins folder and enable it 16 | 2. Inside the level editor, drag out a `Waypoint` from the `Place Actors` panel and place it in your level. 17 | 3. Click the `Create Waypoint Loop` button inside of the details panel for the Waypoint actor in your level. 18 | 4. Hold alt and click and move the Waypoint actor in your level to create the next Waypoint in the loop. 19 | 5. Have your AI patrol the Waypoints. todo @nicholas explain this 20 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Resources/Icon128.png -------------------------------------------------------------------------------- /Resources/Icon256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Resources/Icon256.png -------------------------------------------------------------------------------- /Resources/Icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Resources/Icon512.png -------------------------------------------------------------------------------- /Resources/Slate/Icons/waypoint_icon_16x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Resources/Slate/Icons/waypoint_icon_16x.png -------------------------------------------------------------------------------- /Resources/Slate/Icons/waypoint_icon_20x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Resources/Slate/Icons/waypoint_icon_20x.png -------------------------------------------------------------------------------- /Resources/Slate/Icons/waypoint_icon_256x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Resources/Slate/Icons/waypoint_icon_256x.png -------------------------------------------------------------------------------- /Resources/Slate/Icons/waypoint_icon_40x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Resources/Slate/Icons/waypoint_icon_40x.png -------------------------------------------------------------------------------- /Resources/Slate/Icons/waypoint_icon_64x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/Resources/Slate/Icons/waypoint_icon_64x.png -------------------------------------------------------------------------------- /Source/Waypoints/Private/BTTask_MoveToNextWaypoint.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "BTTask_MoveToNextWaypoint.h" 4 | #include "WaypointsModule.h" 5 | 6 | #include "GameFramework/Actor.h" 7 | #include "AISystem.h" 8 | #include "Navigation/PathFollowingComponent.h" 9 | #include "BehaviorTree/Blackboard/BlackboardKeyType_Object.h" 10 | #include "BehaviorTree/Blackboard/BlackboardKeyType_Vector.h" 11 | #include "VisualLogger/VisualLogger.h" 12 | #include "AIController.h" 13 | #include "BehaviorTree/BlackboardComponent.h" 14 | #include "Tasks/AITask_MoveTo.h" 15 | 16 | #include "Waypoint.h" 17 | 18 | UBTTask_MoveToNextWaypoint::UBTTask_MoveToNextWaypoint(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) 19 | { 20 | NodeName = "Move To Next Waypoint"; 21 | bUseGameplayTasks = true; //GET_AI_CONFIG_VAR(bEnableBTAITasks); deprecated in 5.2, always true now 22 | bNotifyTick = !bUseGameplayTasks; 23 | bNotifyTaskFinished = true; 24 | 25 | bReachTestIncludesGoalRadius = bReachTestIncludesAgentRadius = GET_AI_CONFIG_VAR(bFinishMoveOnGoalOverlap); 26 | //bAllowStrafe = GET_AI_CONFIG_VAR(bAllowStrafing); 27 | 28 | bSetNextWaypointAfterFinishing = true; 29 | bWaitAtCheckpoint = true; 30 | 31 | // Accept only waypoints 32 | BlackboardKey.AddObjectFilter(this, GET_MEMBER_NAME_CHECKED(UBTTask_MoveToNextWaypoint, BlackboardKey), AWaypoint::StaticClass()); 33 | } 34 | 35 | EBTNodeResult::Type UBTTask_MoveToNextWaypoint::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) 36 | { 37 | EBTNodeResult::Type NodeResult = EBTNodeResult::InProgress; 38 | 39 | FBTMoveToNextWaypointTaskMemory* MyMemory = CastInstanceNodeMemory(NodeMemory); 40 | MyMemory->PreviousGoalLocation = FAISystem::InvalidLocation; 41 | MyMemory->MoveRequestID = FAIRequestID::InvalidRequest; 42 | 43 | AAIController* MyController = OwnerComp.GetAIOwner(); 44 | MyMemory->bWaitingForPath = bUseGameplayTasks ? false : MyController->ShouldPostponePathUpdates(); 45 | if (!MyMemory->bWaitingForPath) 46 | { 47 | NodeResult = PerformMoveTask(OwnerComp, NodeMemory); 48 | } 49 | else 50 | { 51 | UE_VLOG(MyController, LogBehaviorTree, Log, TEXT("Pathfinding requests are freezed, waiting...")); 52 | } 53 | 54 | return NodeResult; 55 | } 56 | 57 | EBTNodeResult::Type UBTTask_MoveToNextWaypoint::PerformMoveTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) 58 | { 59 | const UBlackboardComponent* MyBlackboard = OwnerComp.GetBlackboardComponent(); 60 | FBTMoveToNextWaypointTaskMemory* MyMemory = CastInstanceNodeMemory(NodeMemory); 61 | AAIController* MyController = OwnerComp.GetAIOwner(); 62 | 63 | EBTNodeResult::Type NodeResult = EBTNodeResult::Failed; 64 | if (MyController && MyBlackboard) 65 | { 66 | FAIMoveRequest MoveReq; 67 | MoveReq.SetNavigationFilter(MyController->GetDefaultNavigationFilterClass()); 68 | //MoveReq.SetCanStrafe(bAllowStrafe); 69 | MoveReq.SetUsePathfinding(true); 70 | //MoveReq.SetStopOnOverlap(true); 71 | MoveReq.SetAllowPartialPath(true); 72 | 73 | if (BlackboardKey.SelectedKeyType == UBlackboardKeyType_Object::StaticClass()) 74 | { 75 | UObject* KeyValue = MyBlackboard->GetValue(BlackboardKey.GetSelectedKeyID()); 76 | AWaypoint* TargetActor = Cast(KeyValue); 77 | if (TargetActor) 78 | { 79 | MoveReq.SetAcceptanceRadius(TargetActor->GetAcceptanceRadius()); 80 | MoveReq.SetReachTestIncludesAgentRadius(bReachTestIncludesAgentRadius); 81 | MoveReq.SetReachTestIncludesGoalRadius(bReachTestIncludesGoalRadius); 82 | MoveReq.SetGoalActor(TargetActor); 83 | } 84 | else 85 | { 86 | UE_VLOG(MyController, LogBehaviorTree, Warning, TEXT("UBTTask_MoveToNextWaypoint::ExecuteTask tried to go to actor while BB %s entry was empty"), *BlackboardKey.SelectedKeyName.ToString()); 87 | } 88 | } 89 | 90 | if (MoveReq.IsValid()) 91 | { 92 | if (true) //GET_AI_CONFIG_VAR(bEnableBTAITasks) deprecated in 5.2, always true now 93 | { 94 | UAITask_MoveTo* MoveTask = MyMemory->Task.Get(); 95 | const bool bReuseExistingTask = (MoveTask != nullptr); 96 | 97 | MoveTask = PrepareMoveTask(OwnerComp, MoveTask, MoveReq); 98 | if (MoveTask) 99 | { 100 | MyMemory->bObserverCanFinishTask = false; 101 | 102 | if (bReuseExistingTask) 103 | { 104 | if (MoveTask->IsActive()) 105 | { 106 | UE_VLOG(MyController, LogBehaviorTree, Verbose, TEXT("\'%s\' reusing AITask %s"), *GetNodeName(), *MoveTask->GetName()); 107 | MoveTask->ConditionalPerformMove(); 108 | } 109 | else 110 | { 111 | UE_VLOG(MyController, LogBehaviorTree, Verbose, TEXT("\'%s\' reusing AITask %s, but task is not active - handing over move performing to task mechanics"), *GetNodeName(), *MoveTask->GetName()); 112 | } 113 | } 114 | else 115 | { 116 | MyMemory->Task = MoveTask; 117 | UE_VLOG(MyController, LogBehaviorTree, Verbose, TEXT("\'%s\' task implementing move with task %s"), *GetNodeName(), *MoveTask->GetName()); 118 | MoveTask->ReadyForActivation(); 119 | } 120 | 121 | MyMemory->bObserverCanFinishTask = true; 122 | NodeResult = (MoveTask->GetState() != EGameplayTaskState::Finished) ? EBTNodeResult::InProgress : 123 | MoveTask->WasMoveSuccessful() ? EBTNodeResult::Succeeded : 124 | EBTNodeResult::Failed; 125 | } 126 | } 127 | else 128 | { 129 | FPathFollowingRequestResult RequestResult = MyController->MoveTo(MoveReq); 130 | if (RequestResult.Code == EPathFollowingRequestResult::RequestSuccessful) 131 | { 132 | MyMemory->MoveRequestID = RequestResult.MoveId; 133 | WaitForMessage(OwnerComp, UBrainComponent::AIMessage_MoveFinished, RequestResult.MoveId); 134 | WaitForMessage(OwnerComp, UBrainComponent::AIMessage_RepathFailed); 135 | 136 | NodeResult = EBTNodeResult::InProgress; 137 | } 138 | else if (RequestResult.Code == EPathFollowingRequestResult::AlreadyAtGoal) 139 | { 140 | NodeResult = EBTNodeResult::Succeeded; 141 | } 142 | } 143 | } 144 | } 145 | 146 | return NodeResult; 147 | } 148 | 149 | UAITask_MoveTo* UBTTask_MoveToNextWaypoint::PrepareMoveTask(UBehaviorTreeComponent& OwnerComp, UAITask_MoveTo* ExistingTask, FAIMoveRequest& MoveRequest) 150 | { 151 | UAITask_MoveTo* MoveTask = ExistingTask ? ExistingTask : NewBTAITask(OwnerComp); 152 | if (MoveTask) 153 | { 154 | MoveTask->SetUp(MoveTask->GetAIController(), MoveRequest); 155 | } 156 | 157 | return MoveTask; 158 | } 159 | 160 | EBTNodeResult::Type UBTTask_MoveToNextWaypoint::AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) 161 | { 162 | FBTMoveToNextWaypointTaskMemory* MyMemory = CastInstanceNodeMemory(NodeMemory); 163 | if (!MyMemory->bWaitingForPath) 164 | { 165 | if (MyMemory->MoveRequestID.IsValid()) 166 | { 167 | AAIController* MyController = OwnerComp.GetAIOwner(); 168 | if (MyController && MyController->GetPathFollowingComponent()) 169 | { 170 | MyController->GetPathFollowingComponent()->AbortMove(*this, FPathFollowingResultFlags::OwnerFinished, MyMemory->MoveRequestID); 171 | } 172 | } 173 | else 174 | { 175 | MyMemory->bObserverCanFinishTask = false; 176 | UAITask_MoveTo* MoveTask = MyMemory->Task.Get(); 177 | if (MoveTask) 178 | { 179 | MoveTask->ExternalCancel(); 180 | } 181 | else 182 | { 183 | UE_VLOG(&OwnerComp, LogBehaviorTree, Error, TEXT("Can't abort path following! bWaitingForPath:false, MoveRequestID:invalid, MoveTask:none!")); 184 | } 185 | } 186 | } 187 | 188 | return Super::AbortTask(OwnerComp, NodeMemory); 189 | } 190 | 191 | void UBTTask_MoveToNextWaypoint::OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) 192 | { 193 | FBTMoveToNextWaypointTaskMemory* MyMemory = CastInstanceNodeMemory(NodeMemory); 194 | MyMemory->Task.Reset(); 195 | 196 | // Set the blackboard value to the next waypoint 197 | if (bSetNextWaypointAfterFinishing && BlackboardKey.SelectedKeyType == UBlackboardKeyType_Object::StaticClass()) 198 | { 199 | UBlackboardComponent* MyBlackboard = OwnerComp.GetBlackboardComponent(); 200 | UObject* KeyValue = MyBlackboard->GetValue(BlackboardKey.GetSelectedKeyID()); 201 | 202 | if (AWaypoint* TargetActor = Cast(KeyValue)) 203 | { 204 | MyBlackboard->SetValueAsObject(BlackboardKey.SelectedKeyName, TargetActor->GetNextWaypoint()); 205 | } 206 | } 207 | 208 | // Reset the AI's focus 209 | if (AAIController* MyController = OwnerComp.GetAIOwner()) 210 | { 211 | MyController->ClearFocus(EAIFocusPriority::Gameplay); 212 | } 213 | 214 | Super::OnTaskFinished(OwnerComp, NodeMemory, TaskResult); 215 | } 216 | 217 | void UBTTask_MoveToNextWaypoint::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) 218 | { 219 | FBTMoveToNextWaypointTaskMemory* MyMemory = (FBTMoveToNextWaypointTaskMemory*)NodeMemory; 220 | 221 | if (MyMemory->bWaitingForPath && !OwnerComp.IsPaused()) 222 | { 223 | AAIController* MyController = OwnerComp.GetAIOwner(); 224 | if (MyController && !MyController->ShouldPostponePathUpdates()) 225 | { 226 | UE_VLOG(MyController, LogBehaviorTree, Log, TEXT("Pathfinding requests are unlocked!")); 227 | MyMemory->bWaitingForPath = false; 228 | 229 | const EBTNodeResult::Type NodeResult = PerformMoveTask(OwnerComp, NodeMemory); 230 | if (NodeResult != EBTNodeResult::InProgress) 231 | { 232 | FinishLatentTask(OwnerComp, NodeResult); 233 | } 234 | } 235 | } 236 | 237 | // If remaining wait time >= 0.f then we should be waiting 238 | if (MyMemory->RemainingWaitTime > 0.f) 239 | { 240 | MyMemory->RemainingWaitTime -= DeltaSeconds; 241 | 242 | if (MyMemory->RemainingWaitTime <= 0.f) 243 | { 244 | FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded); 245 | } 246 | } 247 | } 248 | 249 | void UBTTask_MoveToNextWaypoint::OnMessage(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, FName Message, int32 SenderID, bool bSuccess) 250 | { 251 | // AIMessage_RepathFailed means task has failed 252 | bSuccess &= (Message != UBrainComponent::AIMessage_RepathFailed); 253 | 254 | FBTMoveToNextWaypointTaskMemory* MyMemory = CastInstanceNodeMemory(NodeMemory); 255 | const UBlackboardComponent* MyBlackboard = OwnerComp.GetBlackboardComponent(); 256 | UObject* KeyValue = MyBlackboard->GetValue(BlackboardKey.GetSelectedKeyID()); 257 | AWaypoint* TargetActor = Cast(KeyValue); 258 | 259 | AAIController* MyController = OwnerComp.GetAIOwner(); 260 | 261 | // We've finished moving to the waypoint, now time to wait 262 | if (bSuccess && TargetActor && bWaitAtCheckpoint && (TargetActor->GetWaitTime() > 0.f)) 263 | { 264 | // Turn the actor towards the waypoint 265 | if (MyController && TargetActor->GetOrientGuardToWaypoint()) 266 | { 267 | APawn* Pawn = MyController->GetPawn(); 268 | const FVector PawnLocation = Pawn->GetActorLocation(); 269 | const FVector DirectionVector = TargetActor->GetActorForwardVector(); 270 | const FVector FocalPoint = PawnLocation + DirectionVector * 10000.0f; 271 | 272 | MyController->SetFocalPoint(FocalPoint, EAIFocusPriority::Gameplay); 273 | 274 | } 275 | 276 | MyMemory->RemainingWaitTime = TargetActor->GetWaitTime(); 277 | return; 278 | } 279 | 280 | Super::OnMessage(OwnerComp, NodeMemory, Message, SenderID, bSuccess); 281 | } 282 | 283 | void UBTTask_MoveToNextWaypoint::OnGameplayTaskDeactivated(UGameplayTask& Task) 284 | { 285 | // AI move task finished 286 | UAITask_MoveTo* MoveTask = Cast(&Task); 287 | if (MoveTask && MoveTask->GetAIController() && MoveTask->GetState() != EGameplayTaskState::Paused) 288 | { 289 | UBehaviorTreeComponent* BehaviorComp = GetBTComponentForTask(Task); 290 | if (BehaviorComp) 291 | { 292 | uint8* RawMemory = BehaviorComp->GetNodeMemory(this, BehaviorComp->FindInstanceContainingNode(this)); 293 | FBTMoveToNextWaypointTaskMemory* MyMemory = CastInstanceNodeMemory(RawMemory); 294 | 295 | if (MyMemory->bObserverCanFinishTask && (MoveTask == MyMemory->Task)) 296 | { 297 | const bool bSuccess = MoveTask->WasMoveSuccessful(); 298 | FinishLatentTask(*BehaviorComp, bSuccess ? EBTNodeResult::Succeeded : EBTNodeResult::Failed); 299 | } 300 | } 301 | } 302 | } 303 | 304 | FString UBTTask_MoveToNextWaypoint::GetStaticDescription() const 305 | { 306 | FString KeyDesc("invalid"); 307 | if (BlackboardKey.SelectedKeyType == UBlackboardKeyType_Object::StaticClass() || 308 | BlackboardKey.SelectedKeyType == UBlackboardKeyType_Vector::StaticClass()) 309 | { 310 | KeyDesc = BlackboardKey.SelectedKeyName.ToString(); 311 | } 312 | 313 | return FString::Printf(TEXT("%s: %s"), *Super::GetStaticDescription(), *KeyDesc); 314 | } 315 | 316 | void UBTTask_MoveToNextWaypoint::DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray& Values) const 317 | { 318 | Super::DescribeRuntimeValues(OwnerComp, NodeMemory, Verbosity, Values); 319 | 320 | const UBlackboardComponent* BlackboardComp = OwnerComp.GetBlackboardComponent(); 321 | 322 | if (BlackboardComp) 323 | { 324 | const FString KeyValue = BlackboardComp->DescribeKeyValue(BlackboardKey.GetSelectedKeyID(), EBlackboardDescription::OnlyValue); 325 | 326 | FBTMoveToNextWaypointTaskMemory* MyMemory = (FBTMoveToNextWaypointTaskMemory*)NodeMemory; 327 | const bool bIsUsingTask = MyMemory->Task.IsValid(); 328 | 329 | const FString ModeDesc = 330 | MyMemory->bWaitingForPath ? TEXT("(WAITING)") : 331 | bIsUsingTask ? TEXT("(task)") : 332 | TEXT(""); 333 | 334 | Values.Add(FString::Printf(TEXT("move target: %s%s"), *KeyValue, *ModeDesc)); 335 | } 336 | } 337 | 338 | uint16 UBTTask_MoveToNextWaypoint::GetInstanceMemorySize() const 339 | { 340 | return sizeof(FBTMoveToNextWaypointTaskMemory); 341 | } 342 | 343 | #if WITH_EDITOR 344 | 345 | FName UBTTask_MoveToNextWaypoint::GetNodeIconName() const 346 | { 347 | return FName("BTEditor.Graph.BTNode.Task.MoveTo.Icon"); 348 | } 349 | 350 | #endif // WITH_EDITOR 351 | -------------------------------------------------------------------------------- /Source/Waypoints/Private/Waypoint.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2020 EpicGameGuy. All Rights Reserved. 2 | 3 | #include "Waypoint.h" 4 | #include "Components/SceneComponent.h" 5 | #include "WaypointLoop.h" 6 | 7 | #if WITH_EDITOR 8 | #include "ObjectEditorUtils.h" 9 | #include "UObject/ConstructorHelpers.h" 10 | #include "Components/BillboardComponent.h" 11 | #include "Components/SplineComponent.h" 12 | 13 | #include "Editor/UnrealEdEngine.h" 14 | #include "Engine/Selection.h" 15 | #endif // WITH_EDITOR 16 | 17 | #include "GameFramework/Character.h" 18 | #include "Components/ArrowComponent.h" 19 | #include "Components/SphereComponent.h" 20 | #include "Components/CapsuleComponent.h" 21 | 22 | static bool IsCorrectWorldType(const UWorld* World) 23 | { 24 | return World->WorldType == EWorldType::Editor; 25 | } 26 | 27 | AWaypoint::AWaypoint(const FObjectInitializer& ObjectInitializer) 28 | : Super(ObjectInitializer) 29 | { 30 | Scene = CreateDefaultSubobject(TEXT("SceneComponent")); 31 | SetRootComponent(Scene); 32 | 33 | AcceptanceRadius = 128.f; 34 | WaitTime = 0.f; 35 | 36 | bStopOnOverlap = true; 37 | bOrientGuardToWaypoint = false; 38 | 39 | WaypointIndex = INDEX_NONE; 40 | 41 | #if WITH_EDITOR 42 | bRunConstructionScriptOnDrag = false; 43 | 44 | struct FConstructorStatics 45 | { 46 | ConstructorHelpers::FObjectFinderOptional WaypointIcon; 47 | 48 | FName ID_WaypointIcon; 49 | 50 | FText NAME_WaypointIcon; 51 | 52 | FConstructorStatics() 53 | : WaypointIcon(TEXT("/Waypoints/EditorResources/S_Waypoint")) 54 | , ID_WaypointIcon(TEXT("WaypointIcon")) 55 | , NAME_WaypointIcon(NSLOCTEXT("SpriteCategory", "Waypoints", "WaypointIcon")) 56 | { 57 | } 58 | }; 59 | static FConstructorStatics ConstructorStatics; 60 | 61 | Sprite = CreateEditorOnlyDefaultSubobject(TEXT("Icon")); 62 | if (Sprite) 63 | { 64 | 65 | Sprite->Sprite = ConstructorStatics.WaypointIcon.Get(); 66 | Sprite->SpriteInfo.Category = ConstructorStatics.ID_WaypointIcon; 67 | Sprite->SpriteInfo.DisplayName = ConstructorStatics.NAME_WaypointIcon; 68 | Sprite->SetupAttachment(Scene); 69 | } 70 | 71 | PathComponent = CreateDefaultSubobject(TEXT("PathRenderComponent")); 72 | if (PathComponent) 73 | { 74 | PathComponent->SetupAttachment(Scene); 75 | PathComponent->bSelectable = false; 76 | PathComponent->bEditableWhenInherited = true; 77 | PathComponent->SetUsingAbsoluteLocation(true); 78 | PathComponent->SetUsingAbsoluteRotation(true); 79 | PathComponent->SetUsingAbsoluteScale(true); 80 | } 81 | #endif // WITH_EDITOR 82 | 83 | OverlapSphere = CreateDefaultSubobject(TEXT("Overlap Sphere Visualization Component")); 84 | if (OverlapSphere) 85 | { 86 | OverlapSphere->SetupAttachment(Scene); 87 | OverlapSphere->bSelectable = true; 88 | OverlapSphere->bEditableWhenInherited = true; 89 | OverlapSphere->SetCollisionEnabled(ECollisionEnabled::QueryOnly); 90 | OverlapSphere->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore); 91 | OverlapSphere->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECR_Overlap); 92 | OverlapSphere->SetSphereRadius(AcceptanceRadius); 93 | } 94 | 95 | GuardFacingArrow = CreateDefaultSubobject(TEXT("Guard Facing Arrow Component")); 96 | if (GuardFacingArrow) 97 | { 98 | GuardFacingArrow->SetupAttachment(Scene); 99 | GuardFacingArrow->bSelectable = true; 100 | GuardFacingArrow->bEditableWhenInherited = false; 101 | GuardFacingArrow->SetVisibility(false); 102 | GuardFacingArrow->ArrowColor = FColor(255, 255, 255, 255); 103 | } 104 | 105 | bUseCharacterClassNavProperties = true; 106 | CharacterClass = ACharacter::StaticClass(); 107 | } 108 | 109 | TArray> AWaypoint::GetLoop() const 110 | { 111 | if (OwningLoop.IsValid()) 112 | { 113 | return OwningLoop->Waypoints; 114 | } 115 | 116 | return {}; 117 | } 118 | 119 | AWaypoint* AWaypoint::GetNextWaypoint() const 120 | { 121 | if (OwningLoop.IsValid()) 122 | { 123 | const TArray>& WaypointLoop = OwningLoop->Waypoints; 124 | auto Index = OwningLoop->FindWaypoint(this); 125 | if (Index != INDEX_NONE) 126 | { 127 | return WaypointLoop[(Index + 1) % WaypointLoop.Num()].Get(); 128 | } 129 | } 130 | 131 | return nullptr; 132 | } 133 | 134 | AWaypoint* AWaypoint::GetPreviousWaypoint() const 135 | { 136 | if (OwningLoop.IsValid()) 137 | { 138 | const TArray>& WaypointLoop = OwningLoop->Waypoints; 139 | auto Index = OwningLoop->FindWaypoint(this); 140 | if (Index != INDEX_NONE) 141 | { 142 | int32 WrappedIndex = (Index - 1) % WaypointLoop.Num(); 143 | if (WrappedIndex < 0) 144 | { 145 | WrappedIndex += WaypointLoop.Num(); 146 | } 147 | 148 | return WaypointLoop[WrappedIndex].Get(); 149 | } 150 | } 151 | 152 | return nullptr; 153 | } 154 | 155 | 156 | void AWaypoint::PostRegisterAllComponents() 157 | { 158 | Super::PostRegisterAllComponents(); 159 | 160 | #if WITH_EDITOR 161 | UWorld* World = GetWorld(); 162 | if (World && World->WorldType == EWorldType::Editor) 163 | { 164 | UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent(GetWorld()); 165 | if (NavSys) 166 | { 167 | NavSys->OnNavigationGenerationFinishedDelegate.RemoveAll(this); 168 | NavSys->OnNavigationGenerationFinishedDelegate.AddUniqueDynamic(this, &AWaypoint::OnNavigationGenerationFinished); 169 | } 170 | } 171 | #endif // WITH_EDITOR 172 | } 173 | 174 | #if WITH_EDITOR 175 | void AWaypoint::PreEditChange(FProperty* PropertyThatWillChange) 176 | { 177 | static const FName NAME_OwningLoop = GET_MEMBER_NAME_CHECKED(AWaypoint, OwningLoop); 178 | 179 | if (PropertyThatWillChange) 180 | { 181 | const FName ChangedPropName = PropertyThatWillChange->GetFName(); 182 | if (ChangedPropName == NAME_OwningLoop) 183 | { 184 | SetWaypointLoop(nullptr); 185 | } 186 | } 187 | 188 | Super::PreEditChange(PropertyThatWillChange); 189 | } 190 | 191 | void AWaypoint::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) 192 | { 193 | static const FName NAME_bOrientGuardToWaypoint = GET_MEMBER_NAME_CHECKED(AWaypoint, bOrientGuardToWaypoint); 194 | static const FName NAME_AcceptanceRadius = GET_MEMBER_NAME_CHECKED(AWaypoint, AcceptanceRadius); 195 | static const FName NAME_OwningLoop = GET_MEMBER_NAME_CHECKED(AWaypoint, OwningLoop); 196 | 197 | Super::PostEditChangeProperty(PropertyChangedEvent); 198 | 199 | if (PropertyChangedEvent.Property) 200 | { 201 | const FName ChangedPropName = PropertyChangedEvent.Property->GetFName(); 202 | 203 | if (ChangedPropName == NAME_bOrientGuardToWaypoint && GuardFacingArrow) 204 | { 205 | GuardFacingArrow->SetVisibility(bOrientGuardToWaypoint); 206 | } 207 | 208 | if (ChangedPropName == NAME_AcceptanceRadius) 209 | { 210 | OverlapSphere->SetSphereRadius(AcceptanceRadius); 211 | } 212 | 213 | if (ChangedPropName == NAME_OwningLoop) 214 | { 215 | SetWaypointLoop(OwningLoop.Get()); 216 | } 217 | } 218 | } 219 | 220 | void AWaypoint::PostEditMove(bool bFinished) 221 | { 222 | Super::PostEditMove(bFinished); 223 | 224 | CalculateSpline(); 225 | 226 | AWaypoint* PreviousWaypoint = GetPreviousWaypoint(); 227 | if (PreviousWaypoint && PreviousWaypoint != this) 228 | { 229 | PreviousWaypoint->CalculateSpline(); 230 | } 231 | } 232 | #endif // WITH_EDITOR 233 | 234 | void AWaypoint::PostDuplicate(EDuplicateMode::Type DuplicateMode) 235 | { 236 | Super::PostDuplicate(DuplicateMode); 237 | 238 | #if WITH_EDITOR 239 | if (DuplicateMode != EDuplicateMode::Normal) 240 | return; 241 | 242 | if (OwningLoop.IsValid() && OwningLoop->Waypoints.IsValidIndex(WaypointIndex)) 243 | { 244 | OwningLoop->InsertWaypoint(this, WaypointIndex + 1); 245 | RecalculateIndex(); 246 | } 247 | #endif 248 | } 249 | 250 | void AWaypoint::CalculateSpline() 251 | { 252 | #if WITH_EDITOR 253 | if (GetWorld()->WorldType != EWorldType::Editor) 254 | return; 255 | 256 | AWaypoint* NextWaypoint = GetNextWaypoint(); 257 | if (NextWaypoint && NextWaypoint != this) 258 | { 259 | PathComponent->SetVisibility(true); 260 | PathComponent->EditorUnselectedSplineSegmentColor = OwningLoop->SplineColor; 261 | 262 | UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent(GetWorld()); 263 | if (NavSys) 264 | { 265 | FPathFindingQuery NavParams; 266 | NavParams.StartLocation = GetActorLocation(); 267 | NavParams.EndLocation = NextWaypoint->GetActorLocation(); 268 | NavParams.NavData = GetNavData(); 269 | if (NavParams.NavData != nullptr) 270 | { 271 | NavParams.QueryFilter = NavParams.NavData->GetDefaultQueryFilter(); 272 | } 273 | NavParams.SetNavAgentProperties(GetNavAgentProperties()); 274 | 275 | FNavPathQueryDelegate Delegate; 276 | Delegate.BindLambda([WeakThis = TWeakObjectPtr(this)](uint32 aPathId, ENavigationQueryResult::Type, FNavPathSharedPtr NavPointer) 277 | { 278 | // Since this lambda is async it can be called after the object was deleted 279 | if (!NavPointer.IsValid() || !WeakThis.IsValid() || !WeakThis->PathComponent->IsValidLowLevel()) 280 | return; 281 | 282 | TArray SplinePoints; 283 | for (const FNavPathPoint& NavPoint : NavPointer->GetPathPoints()) 284 | { 285 | SplinePoints.Push(NavPoint.Location + FVector(0.f, 0.f, 128.f)); 286 | } 287 | 288 | WeakThis->PathComponent->SetSplineWorldPoints(SplinePoints); 289 | if (SplinePoints.Num() > 1) 290 | { 291 | for (int32 i = 0; i < SplinePoints.Num(); ++i) 292 | { 293 | WeakThis->PathComponent->SetTangentsAtSplinePoint(i, FVector(0.f, 0.f, 0.f), FVector(0.f, 0.f, 0.f), ESplineCoordinateSpace::World); 294 | } 295 | } 296 | }); 297 | NavSys->FindPathAsync(GetNavAgentProperties(), NavParams, Delegate); 298 | } 299 | } 300 | else 301 | { 302 | PathComponent->SetSplineWorldPoints(TArray()); 303 | } 304 | #endif // WITH_EDITOR 305 | } 306 | 307 | void AWaypoint::RecalculateIndex() 308 | { 309 | if (OwningLoop.IsValid()) 310 | { 311 | WaypointIndex = OwningLoop->FindWaypoint(this); 312 | } 313 | } 314 | 315 | const ANavigationData* AWaypoint::GetNavData() const 316 | { 317 | UNavigationSystemV1* NavSys = FNavigationSystem::GetCurrent(GetWorld()); 318 | if (NavSys == nullptr) 319 | { 320 | return nullptr; 321 | } 322 | 323 | if (CharacterClass) 324 | { 325 | const ACharacter* CharacterCDO = CharacterClass->GetDefaultObject(); 326 | return NavSys->GetNavDataForProps(CharacterCDO->GetNavAgentPropertiesRef()); 327 | } 328 | else 329 | { 330 | return NavSys->GetAbstractNavData(); 331 | } 332 | } 333 | 334 | const FNavAgentProperties& AWaypoint::GetNavAgentProperties() const 335 | { 336 | if (bUseCharacterClassNavProperties && CharacterClass) 337 | { 338 | const ACharacter* CharacterCDO = CharacterClass->GetDefaultObject(); 339 | static FNavAgentProperties NavAgentProps; 340 | NavAgentProps.NavWalkingSearchHeightScale = FNavigationSystem::GetDefaultSupportedAgent().NavWalkingSearchHeightScale; 341 | 342 | NavAgentProps.AgentRadius = CharacterCDO->GetCapsuleComponent()->GetScaledCapsuleRadius(); 343 | NavAgentProps.AgentHeight = CharacterCDO->GetCapsuleComponent()->GetScaledCapsuleHalfHeight() * 2.f; 344 | return NavAgentProps; 345 | } 346 | return NavProperties; 347 | } 348 | 349 | void AWaypoint::OnNavigationGenerationFinished(class ANavigationData* NavData) 350 | { 351 | CalculateSpline(); 352 | } 353 | 354 | void AWaypoint::Destroyed() 355 | { 356 | if (OwningLoop.IsValid()) 357 | { 358 | OwningLoop->RemoveWaypoint(this); 359 | OwningLoop = nullptr; 360 | } 361 | 362 | Super::Destroyed(); 363 | } 364 | 365 | void AWaypoint::SelectNextWaypoint() const 366 | { 367 | #if WITH_EDITOR 368 | USelection* Selection = GEditor->GetSelectedActors(); 369 | 370 | if (AWaypoint* NextWaypoint = GetNextWaypoint()) 371 | { 372 | Selection->DeselectAll(); 373 | Selection->Select(NextWaypoint); 374 | } 375 | #endif // WITH_EDITOR 376 | } 377 | 378 | void AWaypoint::CreateWaypointLoop() 379 | { 380 | #if WITH_EDITOR 381 | if (UWorld* World = GetWorld()) 382 | { 383 | //UE_LOG(LogWaypoints, Warning, TEXT("Spawning new waypoint loop!!!!!")); 384 | FActorSpawnParameters Params; 385 | Params.bAllowDuringConstructionScript = true; 386 | AWaypointLoop* NewOwningLoop = World->SpawnActor(AWaypointLoop::StaticClass(), Params); 387 | 388 | SetWaypointLoop(NewOwningLoop); 389 | } 390 | #endif // WITH_EDITOR 391 | } 392 | 393 | void AWaypoint::SetWaypointLoop(AWaypointLoop* Loop) 394 | { 395 | if (OwningLoop.IsValid()) 396 | { 397 | OwningLoop->RemoveWaypoint(this); 398 | 399 | DetachFromActor(FDetachmentTransformRules::KeepWorldTransform); 400 | 401 | WaypointIndex = INDEX_NONE; 402 | } 403 | 404 | OwningLoop = Loop; 405 | 406 | if (OwningLoop.IsValid()) 407 | { 408 | AttachToActor(OwningLoop.Get(), FAttachmentTransformRules::KeepWorldTransform); 409 | 410 | OwningLoop->AddWaypoint(this); 411 | WaypointIndex = OwningLoop->FindWaypoint(this); 412 | } 413 | } 414 | -------------------------------------------------------------------------------- /Source/Waypoints/Private/WaypointLoop.cpp: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | 4 | #include "WaypointLoop.h" 5 | #include "Waypoint.h" 6 | #include "Components/SceneComponent.h" 7 | #include "Internationalization/TextLocalizationResource.h" 8 | 9 | // Sets default values 10 | AWaypointLoop::AWaypointLoop(const FObjectInitializer& ObjectInitializer) 11 | : Super(ObjectInitializer) 12 | { 13 | Scene = CreateDefaultSubobject(TEXT("SceneComponent")); 14 | SetRootComponent(Scene); 15 | bSplineColorSetup = false; 16 | } 17 | 18 | #if WITH_EDITOR 19 | void AWaypointLoop::PostEditChangeProperty(FPropertyChangedEvent& Event) 20 | { 21 | Super::PostEditChangeProperty(Event); 22 | 23 | static const FName NAME_Waypoints = GET_MEMBER_NAME_CHECKED(AWaypointLoop, Waypoints); 24 | static const FName NAME_SplineColor = GET_MEMBER_NAME_CHECKED(AWaypointLoop, SplineColor); 25 | 26 | if (Event.Property) 27 | { 28 | const FName ChangedPropName = Event.Property->GetFName(); 29 | 30 | if (ChangedPropName == NAME_Waypoints) 31 | { 32 | RecalculateAllWaypoints(); 33 | } 34 | 35 | if (ChangedPropName == NAME_SplineColor) 36 | { 37 | bSplineColorSetup = true; 38 | RecalculateAllWaypoints(); 39 | } 40 | } 41 | } 42 | 43 | void AWaypointLoop::PostLoad() 44 | { 45 | Super::PostLoad(); 46 | 47 | if (!bSplineColorSetup) 48 | { 49 | FRandomStream Stream(FTextLocalizationResource::HashString(GetName(), 0)); 50 | SplineColor = FLinearColor::MakeFromHSV8((uint8)(Stream.GetUnsignedInt() % 256), 255, 255); 51 | bSplineColorSetup = true; 52 | } 53 | 54 | RecalculateAllWaypoints(); 55 | } 56 | #endif // WITH_EDITOR 57 | 58 | void AWaypointLoop::AddWaypoint(AWaypoint* NewWaypoint) 59 | { 60 | check(!Waypoints.Contains(TWeakObjectPtr(NewWaypoint))); 61 | 62 | Waypoints.Push(TWeakObjectPtr(NewWaypoint)); 63 | 64 | RecalculateAllWaypoints(); 65 | } 66 | 67 | void AWaypointLoop::InsertWaypoint(AWaypoint* NewWaypoint, int32 Index) 68 | { 69 | check(!Waypoints.Contains(TWeakObjectPtr(NewWaypoint))); 70 | 71 | Waypoints.Insert(NewWaypoint, Index); 72 | 73 | RecalculateAllWaypoints(); 74 | } 75 | 76 | void AWaypointLoop::RemoveWaypoint(const AWaypoint* Waypoint) 77 | { 78 | // Remove the last matching element 79 | for (int32 i = Waypoints.Num() - 1; i >= 0; --i) 80 | { 81 | if (Waypoints[i].Get() == Waypoint) 82 | { 83 | Waypoints.RemoveAt(i); 84 | 85 | // Tell the previous waypoint to recalculate its spline 86 | if (Waypoints.Num() != 0) 87 | { 88 | int32 WrappedIndex = (i - 1) % Waypoints.Num(); 89 | if (WrappedIndex < 0) 90 | { 91 | WrappedIndex += Waypoints.Num(); 92 | } 93 | 94 | Waypoints[WrappedIndex]->CalculateSpline(); 95 | } 96 | 97 | break; 98 | } 99 | } 100 | 101 | // Destroy this waypoint loop if there's no waypoints 102 | if (Waypoints.Num() == 0) 103 | { 104 | Destroy(); 105 | } 106 | else 107 | { 108 | RecalculateAllWaypoints(); 109 | } 110 | } 111 | 112 | int32 AWaypointLoop::FindWaypoint(const AWaypoint* Elem) const 113 | { 114 | for (int32 i = 0; i < Waypoints.Num(); ++i) 115 | { 116 | if (Waypoints[i].Get() == Elem) 117 | { 118 | return i; 119 | } 120 | } 121 | 122 | return INDEX_NONE; 123 | } 124 | 125 | void AWaypointLoop::RecalculateAllWaypoints() 126 | { 127 | // Recalculate all indicies 128 | for (int32 i = Waypoints.Num() - 1; i >= 0; --i) 129 | { 130 | if (Waypoints[i].IsValid()) 131 | { 132 | Waypoints[i]->RecalculateIndex(); 133 | } 134 | } 135 | 136 | // Recalculate splines 137 | for (int32 i = Waypoints.Num() - 1; i >= 0; --i) 138 | { 139 | if (Waypoints[i].IsValid()) 140 | { 141 | Waypoints[i]->CalculateSpline(); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Source/Waypoints/Private/WaypointsModule.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "WaypointsModule.h" 4 | #include "Modules/ModuleManager.h" 5 | 6 | DEFINE_LOG_CATEGORY(LogWaypoints); 7 | 8 | #define LOCTEXT_NAMESPACE "FWaypointsModule" 9 | 10 | void FWaypointsModule::StartupModule() 11 | { 12 | // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module 13 | } 14 | 15 | void FWaypointsModule::ShutdownModule() 16 | { 17 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 18 | // we call this function before unloading the module. 19 | } 20 | 21 | #undef LOCTEXT_NAMESPACE 22 | 23 | IMPLEMENT_MODULE(FWaypointsModule, Waypoints) -------------------------------------------------------------------------------- /Source/Waypoints/Public/BTTask_MoveToNextWaypoint.h: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "UObject/ObjectMacros.h" 7 | #include "InputCoreTypes.h" 8 | #include "Templates/SubclassOf.h" 9 | #include "NavFilters/NavigationQueryFilter.h" 10 | #include "AITypes.h" 11 | #include "BehaviorTree/Tasks/BTTask_BlackboardBase.h" 12 | #include "BTTask_MoveToNextWaypoint.generated.h" 13 | 14 | class UAITask_MoveTo; 15 | class UBlackboardComponent; 16 | 17 | struct FBTMoveToNextWaypointTaskMemory 18 | { 19 | /** Move request ID */ 20 | FAIRequestID MoveRequestID; 21 | 22 | FDelegateHandle BBObserverDelegateHandle; 23 | FVector PreviousGoalLocation; 24 | 25 | TWeakObjectPtr Task; 26 | 27 | uint8 bWaitingForPath : 1; 28 | uint8 bObserverCanFinishTask : 1; 29 | 30 | float RemainingWaitTime; 31 | }; 32 | 33 | /** 34 | * Move To task node. 35 | * Moves the AI pawn toward the specified Actor or Location blackboard entry using the navigation system. 36 | */ 37 | UCLASS(config=Game) 38 | class WAYPOINTS_API UBTTask_MoveToNextWaypoint : public UBTTask_BlackboardBase 39 | { 40 | GENERATED_UCLASS_BODY() 41 | 42 | // If true, then the current waypoint will be set to the next waypoint after this task is finished. 43 | UPROPERTY(Category = Node, EditAnywhere) 44 | uint32 bSetNextWaypointAfterFinishing : 1; 45 | 46 | UPROPERTY(Category = Node, EditAnywhere) 47 | uint32 bWaitAtCheckpoint : 1; 48 | 49 | /** if set, radius of AI's capsule will be added to threshold between AI and goal location in destination reach test */ 50 | UPROPERTY(Category = Node, EditAnywhere) 51 | uint32 bReachTestIncludesAgentRadius : 1; 52 | 53 | /** if set, radius of goal's capsule will be added to threshold between AI and goal location in destination reach test */ 54 | UPROPERTY(Category = Node, EditAnywhere) 55 | uint32 bReachTestIncludesGoalRadius : 1; 56 | 57 | /** set automatically if move should use GameplayTasks */ 58 | uint32 bUseGameplayTasks : 1; 59 | 60 | virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; 61 | virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; 62 | virtual void OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) override; 63 | virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override; 64 | virtual uint16 GetInstanceMemorySize() const override; 65 | 66 | virtual void OnGameplayTaskDeactivated(UGameplayTask& Task) override; 67 | virtual void OnMessage(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, FName Message, int32 RequestID, bool bSuccess) override; 68 | 69 | virtual void DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray& Values) const override; 70 | virtual FString GetStaticDescription() const override; 71 | 72 | #if WITH_EDITOR 73 | virtual FName GetNodeIconName() const override; 74 | #endif // WITH_EDITOR 75 | 76 | protected: 77 | 78 | EBTNodeResult::Type PerformMoveTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory); 79 | 80 | /** prepares move task for activation */ 81 | virtual UAITask_MoveTo* PrepareMoveTask(UBehaviorTreeComponent& OwnerComp, UAITask_MoveTo* ExistingTask, FAIMoveRequest& MoveRequest); 82 | }; 83 | -------------------------------------------------------------------------------- /Source/Waypoints/Public/Waypoint.h: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Nicholas Chalkley. All Rights Reserved 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "NavigationSystem.h" 7 | #include "GameFramework/Actor.h" 8 | #include "UObject/WeakObjectPtrTemplates.h" 9 | #include "Waypoint.generated.h" 10 | 11 | class AWaypointLoop; 12 | class ANavigationData; 13 | 14 | UCLASS(Blueprintable, BlueprintType, meta=(PrioritizeCategories="Waypoint")) 15 | class WAYPOINTS_API AWaypoint : public AActor 16 | { 17 | GENERATED_UCLASS_BODY() 18 | 19 | public: 20 | virtual void PostRegisterAllComponents() override; 21 | #if WITH_EDITOR 22 | virtual void PreEditChange(FProperty* PropertyThatWillChange) override; 23 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 24 | virtual void PostEditMove(bool bFinished) override; 25 | virtual bool CanDeleteSelectedActor(FText& OutReason) const override { return true; }; 26 | #endif // WITH_EDITOR 27 | virtual void PostDuplicate(EDuplicateMode::Type DuplicateMode) override; 28 | virtual void Destroyed() override; 29 | 30 | UFUNCTION(BlueprintCallable, BlueprintPure, Category="Waypoint") 31 | AWaypoint* GetNextWaypoint() const; 32 | 33 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Waypoint") 34 | AWaypoint* GetPreviousWaypoint() const; 35 | 36 | UFUNCTION() 37 | TArray> GetLoop() const; 38 | 39 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Waypoint") 40 | float GetWaitTime() const { return WaitTime; }; 41 | 42 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Waypoint") 43 | bool GetOrientGuardToWaypoint() const { return bOrientGuardToWaypoint; }; 44 | 45 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Waypoint") 46 | bool GetStopOnOverlap() const { return bStopOnOverlap; }; 47 | 48 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Waypoint") 49 | float GetAcceptanceRadius() const { return AcceptanceRadius; }; 50 | 51 | void CalculateSpline(); 52 | 53 | void RecalculateIndex(); 54 | 55 | protected: 56 | UPROPERTY(VisibleAnywhere, Category = "Waypoint") 57 | int32 WaypointIndex; 58 | 59 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Waypointz") 60 | class USceneComponent* Scene; 61 | 62 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Waypoint") 63 | class USphereComponent* OverlapSphere; 64 | 65 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Waypoint") 66 | class UBillboardComponent* Sprite; 67 | 68 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Waypoint") 69 | class USplineComponent* PathComponent; 70 | 71 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Waypoint") 72 | class UArrowComponent* GuardFacingArrow; 73 | 74 | UPROPERTY(EditInstanceOnly, Category="Waypoint") 75 | TWeakObjectPtr OwningLoop; 76 | 77 | const ANavigationData* GetNavData() const; 78 | const FNavAgentProperties& GetNavAgentProperties() const; 79 | 80 | protected: 81 | UFUNCTION(CallInEditor) 82 | void OnNavigationGenerationFinished(class ANavigationData* NavData); 83 | 84 | UFUNCTION(CallInEditor, Category = "Waypoint") 85 | void SelectNextWaypoint() const; 86 | 87 | UFUNCTION(CallInEditor, Category = "Waypoint") 88 | void CreateWaypointLoop(); 89 | 90 | UPROPERTY(EditAnywhere, Category = "Waypoint") 91 | bool bUseCharacterClassNavProperties; 92 | 93 | UPROPERTY(EditDefaultsOnly, Category = "Waypoint", Meta=(EditCondition="!bUseCharacterClassNavProperties")) 94 | FNavAgentProperties NavProperties; 95 | 96 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Waypoint", meta = (ClampMin = "0.0", UIMin = "0.0")) 97 | float WaitTime; 98 | 99 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Waypoint") 100 | bool bOrientGuardToWaypoint; 101 | 102 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Waypoint") 103 | bool bStopOnOverlap; 104 | 105 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Waypoint", meta = (ClampMin = "-1.0", UIMin = "-1.0")) 106 | float AcceptanceRadius; 107 | 108 | // Character class used for navigation data 109 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Waypoint") 110 | TSubclassOf CharacterClass; 111 | 112 | void SetWaypointLoop(AWaypointLoop* Loop); 113 | }; 114 | -------------------------------------------------------------------------------- /Source/Waypoints/Public/WaypointLoop.h: -------------------------------------------------------------------------------- 1 | // Fill out your copyright notice in the Description page of Project Settings. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "GameFramework/Actor.h" 7 | #include "UObject/WeakObjectPtrTemplates.h" 8 | #include "WaypointLoop.generated.h" 9 | 10 | class AWaypoint; 11 | class USceneComponent; 12 | 13 | UCLASS() 14 | class WAYPOINTS_API AWaypointLoop : public AActor 15 | { 16 | GENERATED_UCLASS_BODY() 17 | 18 | UPROPERTY() 19 | USceneComponent* Scene; 20 | 21 | UPROPERTY(EditInstanceOnly, Category="Waypoint Loop") 22 | TArray> Waypoints; 23 | 24 | UPROPERTY() 25 | bool bSplineColorSetup; 26 | 27 | UPROPERTY(EditInstanceOnly, Category = "Waypoint Loop") 28 | FLinearColor SplineColor; 29 | 30 | void AddWaypoint(AWaypoint* NewWaypoint); 31 | void InsertWaypoint(AWaypoint* NewWaypoint, int32 Index); 32 | void RemoveWaypoint(const AWaypoint* Waypoint); 33 | int32 FindWaypoint(const AWaypoint* Elem) const; 34 | 35 | void RecalculateAllWaypoints(); 36 | 37 | #if WITH_EDITOR 38 | virtual void PostEditChangeProperty(FPropertyChangedEvent& Event) override; 39 | virtual void PostLoad() override; 40 | #endif // WITH_EDITOR 41 | }; 42 | -------------------------------------------------------------------------------- /Source/Waypoints/Public/WaypointsModule.h: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "Logging/LogMacros.h" 6 | #include "Modules/ModuleInterface.h" 7 | 8 | class FWaypointsModule : public IModuleInterface 9 | { 10 | public: 11 | 12 | /** IModuleInterface implementation */ 13 | virtual void StartupModule() override; 14 | virtual void ShutdownModule() override; 15 | }; 16 | 17 | DECLARE_LOG_CATEGORY_EXTERN(LogWaypoints, Log, All); 18 | -------------------------------------------------------------------------------- /Source/Waypoints/Waypoints.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class Waypoints : ModuleRules 6 | { 7 | public Waypoints(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | IWYUSupport = IWYUSupport.None; 10 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 11 | 12 | PublicDependencyModuleNames.AddRange( 13 | new string[] 14 | { 15 | "Core", 16 | "NavigationSystem", 17 | "AIModule", 18 | "GameplayTasks", 19 | } 20 | ); 21 | 22 | 23 | PrivateDependencyModuleNames.AddRange( 24 | new string[] 25 | { 26 | "CoreUObject", 27 | "Engine", 28 | "Slate", 29 | "SlateCore", 30 | } 31 | ); 32 | 33 | if (Target.Type == TargetType.Editor) 34 | { 35 | PrivateDependencyModuleNames.AddRange( 36 | new string[] 37 | { 38 | "UnrealEd" 39 | } 40 | ); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Source/WaypointsEditorExtension/Private/WaypointsEditorExtensionModule.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "WaypointsEditorExtensionModule.h" 4 | 5 | #include "Engine/Engine.h" 6 | #include "Modules/ModuleManager.h" 7 | #include "Waypoint.h" 8 | #include "Styling/SlateStyle.h" 9 | #include "Styling/SlateStyleRegistry.h" 10 | #include "Editor/UnrealEdEngine.h" 11 | #include "UnrealEdGlobals.h" 12 | #include "Engine/Selection.h" 13 | #include "Framework/MultiBox/MultiBoxBuilder.h" 14 | #include "ClassIconFinder.h" 15 | #include "LevelEditor.h" 16 | #include "PluginUtils.h" 17 | #include "Interfaces/IPluginManager.h" 18 | 19 | #define IMAGE_BRUSH(RelativePath, ...) FSlateImageBrush(StyleSet->RootToContentDir(RelativePath, TEXT(".png")), __VA_ARGS__) 20 | 21 | #define LOCTEXT_NAMESPACE "FWaypointsEditorExtensionModule" 22 | 23 | FDelegateHandle LevelViewportExtenderHandle; 24 | 25 | class FWaypointsEditorExtensionModule_Impl : public IWaypointsEditorExtensionModule 26 | { 27 | public: 28 | virtual void StartupModule() override; 29 | virtual void ShutdownModule() override; 30 | 31 | static TSharedRef OnExtendLevelEditorActorContextMenu(const TSharedRef CommandList, const TArray SelectedActors); 32 | static void CreateWaypointsSelectionMenu(FMenuBuilder& MenuBuilder, const TArray Waypoints); 33 | 34 | private: 35 | TSharedPtr StyleSet; 36 | }; 37 | 38 | void FWaypointsEditorExtensionModule_Impl::StartupModule() 39 | { 40 | // Extend the level editor 41 | FLevelEditorModule& LevelEditorModule = FModuleManager::Get().LoadModuleChecked("LevelEditor"); 42 | auto& MenuExtenders = LevelEditorModule.GetAllLevelViewportContextMenuExtenders(); 43 | 44 | MenuExtenders.Add(FLevelEditorModule::FLevelViewportMenuExtender_SelectedActors::CreateStatic(&FWaypointsEditorExtensionModule_Impl::OnExtendLevelEditorActorContextMenu)); 45 | LevelViewportExtenderHandle = MenuExtenders.Last().GetHandle(); 46 | 47 | const FVector2D Icon16x16(16.0f, 16.0f); 48 | const FVector2D Icon64x64(64.0f, 64.0f); 49 | 50 | // Load in the class icon 51 | if (!StyleSet.IsValid()) 52 | { 53 | StyleSet = MakeShareable(new FSlateStyleSet("WaypointsPluginStyleSet")); 54 | 55 | static const FString SlateDirectory = IPluginManager::Get().FindPlugin(TEXT("Waypoints"))->GetBaseDir() / "Resources/Slate"; 56 | 57 | StyleSet->SetContentRoot(SlateDirectory); 58 | StyleSet->SetCoreContentRoot(SlateDirectory); 59 | 60 | StyleSet->Set("ClassIcon.Waypoint", new IMAGE_BRUSH(TEXT("Icons/waypoint_icon_16x"), Icon16x16)); 61 | StyleSet->Set("ClassThumbnail.Waypoint", new IMAGE_BRUSH(TEXT("Icons/waypoint_icon_64x"), Icon64x64)); 62 | 63 | FSlateStyleRegistry::RegisterSlateStyle(*StyleSet.Get()); 64 | } 65 | } 66 | 67 | void FWaypointsEditorExtensionModule_Impl::ShutdownModule() 68 | { 69 | // Unload editor extension 70 | if (LevelViewportExtenderHandle.IsValid()) 71 | { 72 | FLevelEditorModule* LevelEditorModule = FModuleManager::Get().GetModulePtr("LevelEditor"); 73 | if (LevelEditorModule) 74 | { 75 | typedef FLevelEditorModule::FLevelViewportMenuExtender_SelectedActors DelegateType; 76 | LevelEditorModule->GetAllLevelViewportContextMenuExtenders().RemoveAll([=](const DelegateType& In) { return In.GetHandle() == LevelViewportExtenderHandle; }); 77 | } 78 | } 79 | 80 | // Unload class icons 81 | if (StyleSet.IsValid()) 82 | { 83 | FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet.Get()); 84 | ensure(StyleSet.IsUnique()); 85 | StyleSet.Reset(); 86 | } 87 | } 88 | 89 | TSharedRef FWaypointsEditorExtensionModule_Impl::OnExtendLevelEditorActorContextMenu(const TSharedRef CommandList, const TArray SelectedActors) 90 | { 91 | TSharedRef Extender(new FExtender()); 92 | TArray Waypoints; 93 | 94 | for (AActor* Actor : SelectedActors) 95 | { 96 | if (AWaypoint* Waypoint = Cast(Actor)) 97 | { 98 | Waypoints.Push(Waypoint); 99 | } 100 | } 101 | 102 | // If the selection contains any WaypointActors 103 | if (Waypoints.Num() > 0) 104 | { 105 | Extender->AddMenuExtension( 106 | "SelectActorGeneral", 107 | EExtensionHook::After, 108 | CommandList, 109 | FMenuExtensionDelegate::CreateStatic(&FWaypointsEditorExtensionModule_Impl::CreateWaypointsSelectionMenu, Waypoints)); 110 | } 111 | 112 | return Extender; 113 | } 114 | 115 | void FWaypointsEditorExtensionModule_Impl::CreateWaypointsSelectionMenu(FMenuBuilder& MenuBuilder, const TArray Waypoints) 116 | { 117 | MenuBuilder.AddMenuSeparator(); 118 | MenuBuilder.AddMenuEntry( 119 | LOCTEXT("SelectWaypointLoop", "Select Waypoint Loop"), 120 | LOCTEXT("SelectWaypointsTooltip", ""), 121 | FSlateIcon(), 122 | FUIAction(FExecuteAction::CreateLambda([Waypoints]() 123 | { 124 | TArray LoopWaypoints; 125 | for (auto Waypoint : Waypoints) 126 | { 127 | if (LoopWaypoints.Contains(Waypoint)) 128 | { 129 | break; 130 | } 131 | 132 | for (TWeakObjectPtr& WaypointWeakPtr : Waypoint->GetLoop()) 133 | { 134 | LoopWaypoints.Push(WaypointWeakPtr.Get()); 135 | } 136 | } 137 | 138 | GEditor->GetSelectedActors()->BeginBatchSelectOperation(); 139 | for (AWaypoint* Waypoint : LoopWaypoints) 140 | { 141 | GUnrealEd->SelectActor(Waypoint, true, false, false); 142 | } 143 | 144 | GEditor->GetSelectedActors()->EndBatchSelectOperation(); 145 | } 146 | )) 147 | ); 148 | } 149 | 150 | #undef LOCTEXT_NAMESPACE 151 | 152 | IMPLEMENT_MODULE(FWaypointsEditorExtensionModule_Impl, WaypointsEditorExtension) -------------------------------------------------------------------------------- /Source/WaypointsEditorExtension/Public/WaypointsEditorExtensionModule.h: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "Modules/ModuleInterface.h" 6 | 7 | class IWaypointsEditorExtensionModule : public IModuleInterface 8 | { 9 | }; 10 | -------------------------------------------------------------------------------- /Source/WaypointsEditorExtension/WaypointsEditorExtension.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2019 Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class WaypointsEditorExtension : ModuleRules 6 | { 7 | public WaypointsEditorExtension(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | IWYUSupport = IWYUSupport.None; 10 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 11 | 12 | PublicDependencyModuleNames.AddRange( 13 | new string[] 14 | { 15 | "Core", 16 | } 17 | ); 18 | 19 | 20 | PrivateDependencyModuleNames.AddRange( 21 | new string[] 22 | { 23 | "CoreUObject", 24 | "Engine", 25 | "UnrealEd", 26 | "Slate", 27 | "SlateCore", 28 | "LevelEditor", 29 | "Waypoints", 30 | "PluginUtils", 31 | "Projects" 32 | } 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Waypoints.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "0.1.0", 5 | "FriendlyName": "Waypoints", 6 | "Description": "NPC Waypoint System", 7 | "Category": "Gameplay", 8 | "CreatedBy": "Nick", 9 | "CreatedByURL": "https://github.com/nicholas477/Waypoints", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": true, 15 | "IsExperimentalVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "Waypoints", 20 | "Type": "Runtime", 21 | "LoadingPhase": "Default" 22 | }, 23 | { 24 | "Name": "WaypointsEditorExtension", 25 | "Type": "Editor", 26 | "LoadingPhase": "Default" 27 | } 28 | ], 29 | "Plugins": [ 30 | { 31 | "Name": "PluginUtils", 32 | "Enabled": true 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /demonstration.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nicholas477/Waypoints/e771dea02de91f05fcd676328f57f275528e5524/demonstration.gif --------------------------------------------------------------------------------