├── Source ├── AeonixEditor │ ├── Private │ │ ├── AeonixEditorModule.h │ │ ├── AeonixVolumeDetails.h │ │ ├── AeonixEditorUtilityWidget.h │ │ ├── AeonixPathDebugActor.h │ │ ├── AeonixEditorUtilityWidget.cpp │ │ ├── AeonixBlockedVoxelVisualizer.h │ │ ├── AeonixEditor.cpp │ │ ├── AeonixPathDebugActor.cpp │ │ ├── AeonixBatchTestActor.h │ │ ├── AenoixEditorDebugSubsystem.h │ │ └── SAeonixNavigationTreeView.h │ ├── AeonixEditor.h │ ├── AeonixEditor.build.cs │ └── Public │ │ ├── AeonixDebugFloodFillActor.h │ │ └── AeonixPerformanceTypes.h ├── AeonixNavigationTests │ ├── Private │ │ ├── AeonixNavigationTestsModule.cpp │ │ ├── Tests │ │ │ └── README.txt │ │ ├── AeonixNavigationBasicTest.cpp │ │ └── AeonixNavigationWallSplitTest.cpp │ └── AeonixNavigationTests.Build.cs ├── AeonixNavigation │ ├── Private │ │ ├── Settings │ │ │ └── AeonixSettings.cpp │ │ ├── Data │ │ │ ├── AeonixCustomVersion.cpp │ │ │ └── AeonixDefines.cpp │ │ ├── AeonixNavigationLibrary.cpp │ │ ├── Task │ │ │ ├── AeonixFindPathTask.cpp │ │ │ ├── AeonixFindPathAsyncAction.cpp │ │ │ └── StateTreeTask_AeonixMoveTo.cpp │ │ ├── AeonixNavigation.cpp │ │ ├── Component │ │ │ ├── AeonixBlockingComponent.cpp │ │ │ ├── AeonixFlyingMovementComponent.cpp │ │ │ ├── AeonixNavAgentComponent.cpp │ │ │ └── AeonixDynamicObstacleComponent.cpp │ │ ├── Library │ │ │ └── libmorton │ │ │ │ ├── morton_common.h │ │ │ │ ├── LICENSE │ │ │ │ ├── morton_BMI.h │ │ │ │ ├── README.md │ │ │ │ ├── morton_LUT_generators.h │ │ │ │ ├── morton.h │ │ │ │ └── morton2D_LUTs.h │ │ ├── Subsystem │ │ │ └── AeonixCollisionSubsystem.cpp │ │ ├── Actor │ │ │ └── AeonixModifierVolume.cpp │ │ ├── Util │ │ │ └── AeonixMediator.cpp │ │ └── Pathfinding │ │ │ └── AeonixNavigationPath.cpp │ ├── Public │ │ ├── Util │ │ │ └── AeonixMediator.h │ │ ├── AeonixNavigation.h │ │ ├── Data │ │ │ ├── AeonixCustomVersion.h │ │ │ ├── AeonixNode.h │ │ │ ├── AeonixHandleTypes.h │ │ │ ├── AeonixLeafNode.h │ │ │ ├── AeonixDefines.h │ │ │ ├── AeonixStats.h │ │ │ ├── AeonixOctreeData.h │ │ │ ├── AeonixLink.h │ │ │ ├── AeonixTypes.h │ │ │ ├── AeonixData.h │ │ │ ├── AeonixGenerationParameters.h │ │ │ ├── AeonixAsyncRegen.h │ │ │ └── AeonixThreading.h │ │ ├── Component │ │ │ ├── AeonixBlockingComponent.h │ │ │ ├── AeonixFlyingMovementComponent.h │ │ │ ├── AeonixNavAgentComponent.h │ │ │ ├── AeonixPathFollowingComponent.h │ │ │ └── AeonixDynamicObstacleComponent.h │ │ ├── AeonixNavigationLibrary.h │ │ ├── Interface │ │ │ ├── AeonixDebugDrawInterface.h │ │ │ ├── AeonixCollisionQueryInterface.h │ │ │ └── AeonixSubsystemInterface.h │ │ ├── Subsystem │ │ │ └── AeonixCollisionSubsystem.h │ │ ├── Task │ │ │ ├── AeonixFindPathTask.h │ │ │ ├── StateTreeTask_AeonixMoveTo.h │ │ │ ├── AeonixFindPathAsyncAction.h │ │ │ ├── BTTask_AeonixMoveTo.h │ │ │ └── AITask_AeonixMoveTo.h │ │ ├── EQS │ │ │ ├── AeonixEQSFloodFillGenerator.h │ │ │ ├── AeonixEQS3DGridGenerator.h │ │ │ └── AeonixEQSFloodFillGenerator.cpp │ │ ├── Actor │ │ │ ├── AeonixModifierVolume.h │ │ │ └── AeonixBoundingVolume.h │ │ ├── Settings │ │ │ └── AeonixSettings.h │ │ └── Pathfinding │ │ │ ├── AeonixPathfindBenchmark.h │ │ │ └── AeonixNavigationPath.h │ └── AeonixNavigation.Build.cs └── AeonixGameUtils │ ├── Public │ ├── AeonixGameUtils.h │ ├── Component │ │ ├── AeonixAutopathTargetComponent.h │ │ └── AeonixAutopathComponent.h │ └── Subsystem │ │ └── AeonixAutopathSubsystem.h │ ├── Private │ ├── AeonixGameUtils.cpp │ └── Component │ │ └── AeonixAutopathTargetComponent.cpp │ └── AeonixGameUtils.Build.cs ├── Config ├── DefaultAeonixNavigation.ini └── FilterPlugin.ini ├── Content ├── TestMat.uasset ├── AeonixDebug.uasset ├── AeonixDebugTools.uasset ├── EUW_AeonixEditor.uasset └── MEC_AeonixVolume.uasset ├── Resources └── Icon128.png ├── .gitattributes ├── AeonixNavigation.uplugin ├── .clang-format ├── .gitignore ├── LICENSE └── README.md /Source/AeonixEditor/Private/AeonixEditorModule.h: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Config/DefaultAeonixNavigation.ini: -------------------------------------------------------------------------------- 1 | [CoreRedirects] 2 | -------------------------------------------------------------------------------- /Content/TestMat.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midgen/AeonixNavigation/HEAD/Content/TestMat.uasset -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midgen/AeonixNavigation/HEAD/Resources/Icon128.png -------------------------------------------------------------------------------- /Content/AeonixDebug.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midgen/AeonixNavigation/HEAD/Content/AeonixDebug.uasset -------------------------------------------------------------------------------- /Content/AeonixDebugTools.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midgen/AeonixNavigation/HEAD/Content/AeonixDebugTools.uasset -------------------------------------------------------------------------------- /Content/EUW_AeonixEditor.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midgen/AeonixNavigation/HEAD/Content/EUW_AeonixEditor.uasset -------------------------------------------------------------------------------- /Content/MEC_AeonixVolume.uasset: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/midgen/AeonixNavigation/HEAD/Content/MEC_AeonixVolume.uasset -------------------------------------------------------------------------------- /Source/AeonixNavigationTests/Private/AeonixNavigationTestsModule.cpp: -------------------------------------------------------------------------------- 1 | #include "Modules/ModuleManager.h" 2 | 3 | IMPLEMENT_MODULE(FDefaultModuleImpl, AeonixNavigationTests); 4 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Settings/AeonixSettings.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | 4 | #include "Settings/AeonixSettings.h" 5 | 6 | UAeonixSettings::UAeonixSettings() 7 | { 8 | // Default values are set in the header via UPROPERTY initializers 9 | } -------------------------------------------------------------------------------- /Source/AeonixNavigationTests/Private/Tests/README.txt: -------------------------------------------------------------------------------- 1 | This folder contains automation tests for the AeonixNavigation plugin. 2 | - Tests are implemented using Unreal's AutomationTest framework. 3 | - Use this module for all C++ unit and integration tests for navigation functionality. 4 | -------------------------------------------------------------------------------- /Source/AeonixGameUtils/Public/AeonixGameUtils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Modules/ModuleManager.h" 4 | 5 | class FAeonixGameUtilsModule : public IModuleInterface 6 | { 7 | public: 8 | virtual void StartupModule() override; 9 | virtual void ShutdownModule() override; 10 | }; 11 | -------------------------------------------------------------------------------- /Source/AeonixGameUtils/Private/AeonixGameUtils.cpp: -------------------------------------------------------------------------------- 1 | #include "AeonixGameUtils.h" 2 | 3 | #define LOCTEXT_NAMESPACE "FAeonixGameUtilsModule" 4 | 5 | void FAeonixGameUtilsModule::StartupModule() 6 | { 7 | } 8 | 9 | void FAeonixGameUtilsModule::ShutdownModule() 10 | { 11 | } 12 | 13 | #undef LOCTEXT_NAMESPACE 14 | 15 | IMPLEMENT_MODULE(FAeonixGameUtilsModule, AeonixGameUtils) 16 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Data/AeonixCustomVersion.cpp: -------------------------------------------------------------------------------- 1 | #include"Data/AeonixCustomVersion.h" 2 | #include "Serialization/CustomVersion.h" 3 | 4 | const FGuid FAeonixCustomVersion::GUID(0x2625B0F1, 0xBA46495F, 0xA0F4C769, 0xEC2F38C1); 5 | 6 | FCustomVersionRegistration GAeonixAnimationCustomVersion(FAeonixCustomVersion::GUID, FAeonixCustomVersion::LatestVersion, TEXT("AeonixVer")); 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Util/AeonixMediator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | class AAeonixBoundingVolume; 4 | struct AeonixLink; 5 | 6 | class AEONIXNAVIGATION_API AeonixMediator 7 | { 8 | public: 9 | static bool GetLinkFromPosition(const FVector& aPosition, const AAeonixBoundingVolume& aVolume, AeonixLink& oLink); 10 | 11 | static void GetVolumeXYZ(const FVector& aPosition, const AAeonixBoundingVolume& aVolume, const int aLayer, FIntVector& oXYZ); 12 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/AeonixNavigationLibrary.cpp: -------------------------------------------------------------------------------- 1 | #include "AeonixNavigationLibrary.h" 2 | 3 | TArray UAeonixNavigationLibrary::GetPathPoints(const FAeonixNavigationPath& Path) 4 | { 5 | return Path.GetPathPoints(); 6 | } 7 | 8 | int32 UAeonixNavigationLibrary::GetNumPathPoints(const FAeonixNavigationPath& Path) 9 | { 10 | return Path.GetNumPoints(); 11 | } 12 | 13 | bool UAeonixNavigationLibrary::IsPathReady(const FAeonixNavigationPath& Path) 14 | { 15 | return Path.IsReady(); 16 | } 17 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/AeonixNavigation.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Modules/ModuleManager.h" 4 | 5 | AEONIXNAVIGATION_API DECLARE_LOG_CATEGORY_EXTERN(LogAeonixNavigation, Log, All); 6 | AEONIXNAVIGATION_API DECLARE_LOG_CATEGORY_EXTERN(VLogAeonixNavigation, Log, All); 7 | AEONIXNAVIGATION_API DECLARE_LOG_CATEGORY_EXTERN(LogAeonixRegen, Warning, All); 8 | 9 | class FAeonixNavigationModule : public IModuleInterface 10 | { 11 | public: 12 | virtual void StartupModule() override; 13 | virtual void ShutdownModule() override; 14 | }; -------------------------------------------------------------------------------- /Source/AeonixEditor/AeonixEditor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Modules/ModuleManager.h" 5 | #include "UnrealEd.h" 6 | 7 | 8 | DECLARE_LOG_CATEGORY_EXTERN(LogAeonixEditor, All, All) 9 | 10 | class FAeonixEditorModule : public IModuleInterface 11 | { 12 | public: 13 | virtual void StartupModule() override; 14 | virtual void ShutdownModule() override; 15 | 16 | private: 17 | TSharedRef SpawnNavigationTreeTab(const FSpawnTabArgs& SpawnTabArgs); 18 | 19 | TSharedPtr PluginCommands; 20 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigationTests/AeonixNavigationTests.Build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | 3 | public class AeonixNavigationTests : ModuleRules 4 | { 5 | public AeonixNavigationTests(ReadOnlyTargetRules Target) : base(Target) 6 | { 7 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 8 | bLegacyParentIncludePaths = false; 9 | CppStandard = CppStandardVersion.Default; 10 | 11 | PrivateDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "AeonixNavigation" }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Task/AeonixFindPathTask.cpp: -------------------------------------------------------------------------------- 1 | #include "Task/AeonixFindPathTask.h" 2 | 3 | #include "Pathfinding/AeonixPathFinder.h" 4 | #include "Data/AeonixTypes.h" 5 | 6 | void FAeonixFindPathTask::DoWork() 7 | { 8 | AeonixPathFinder PathFinder(NavigationData, Settings); 9 | 10 | //Request.SetPathRequestStatusLocked(EAeonixPathFindStatus::InProgress); 11 | 12 | if (PathFinder.FindPath(Start, Goal, StartPos, TargetPos, Path)) 13 | { 14 | //Request.SetPathRequestStatusLocked(EAeonixPathFindStatus::Complete); 15 | } 16 | else 17 | { 18 | //Request.SetPathRequestStatusLocked(EAeonixPathFindStatus::Failed); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixCustomVersion.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Misc/Guid.h" 4 | 5 | // Custom serialization version for assets/classes in the Aeonix module 6 | struct AEONIXNAVIGATION_API FAeonixCustomVersion 7 | { 8 | enum Type 9 | { 10 | // Before any version changes were made in the plugin 11 | BeforeCustomVersionWasAdded = 0, 12 | 13 | // Major refactor of plugin 14 | V2Refactor = 1, 15 | 16 | // ------------------------------------------------------ 17 | VersionPlusOne, 18 | LatestVersion = VersionPlusOne - 1 19 | }; 20 | 21 | // The GUID for this custom version number 22 | const static FGuid GUID; 23 | 24 | private: 25 | FAeonixCustomVersion() {} 26 | }; 27 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Component/AeonixBlockingComponent.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "Components/ActorComponent.h" 6 | 7 | #include "AeonixBlockingComponent.generated.h" 8 | 9 | UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) 10 | class AEONIXNAVIGATION_API UAeonixBlockingComponent : public UActorComponent 11 | { 12 | GENERATED_BODY() 13 | 14 | public: 15 | // Sets default values for this component's properties 16 | UAeonixBlockingComponent(); 17 | 18 | protected: 19 | // Called when the game starts 20 | virtual void BeginPlay() override; 21 | 22 | public: 23 | // Called every frame 24 | virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; 25 | }; -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/AeonixVolumeDetails.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | #include "CoreMinimal.h" 5 | #include "IDetailCustomization.h" 6 | 7 | class IDetailLayoutBuilder; 8 | class AAeonixBoundingVolume; 9 | 10 | class FAeonixVolumeDetails : public IDetailCustomization 11 | { 12 | public: 13 | /** Makes a new instance of this detail layout class for a specific detail view requesting it */ 14 | static TSharedRef MakeInstance(); 15 | 16 | /** IDetailCustomization interface */ 17 | virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override; 18 | 19 | 20 | FReply OnUpdateVolume(); 21 | 22 | FReply OnClearVolumeClick(); 23 | 24 | FReply OnRegenerateDynamicRegions(); 25 | 26 | private: 27 | TWeakObjectPtr myVolume; 28 | }; 29 | -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/AeonixEditorUtilityWidget.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Editor/Blutility/Classes/EditorUtilityWidget.h" 7 | #include "AeonixEditorUtilityWidget.generated.h" 8 | 9 | /** 10 | * 11 | */ 12 | UCLASS() 13 | class AEONIXEDITOR_API UAeonixEditorUtilityWidget : public UEditorUtilityWidget 14 | { 15 | GENERATED_BODY() 16 | 17 | UFUNCTION(BlueprintCallable, Category = "Aeonix") 18 | void CompleteAllPendingPathfindingTasks(); 19 | 20 | UFUNCTION(BlueprintCallable, Category = "Aeonix") 21 | int32 GetNumberOfPendingPathFindTasks() const; 22 | 23 | UFUNCTION(BlueprintCallable, Category = "Aeonix") 24 | int32 GetNumberOfRegisteredNavAgents() const; 25 | 26 | UFUNCTION(BlueprintCallable, Category = "Aeonix") 27 | int32 GetNumberOfRegisteredNavVolumes() const; 28 | }; 29 | -------------------------------------------------------------------------------- /Source/AeonixEditor/AeonixEditor.build.cs: -------------------------------------------------------------------------------- 1 | using UnrealBuildTool; 2 | 3 | public class AeonixEditor : ModuleRules 4 | { 5 | public AeonixEditor(ReadOnlyTargetRules Target) : base(Target) 6 | { 7 | 8 | PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "AeonixNavigation", "InputCore" }); 9 | 10 | PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore", "PropertyEditor", "EditorStyle", "UnrealEd", "GraphEditor", "BlueprintGraph", "EditorSubsystem", "Blutility", "UMG", "ToolMenus", "LevelEditor", "WorkspaceMenuStructure" }); 11 | 12 | PrivateIncludePaths.AddRange(new string[] { "AeonixEditor/Private" }); 13 | 14 | PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; 15 | bLegacyParentIncludePaths = false; 16 | CppStandard = CppStandardVersion.Default; 17 | 18 | } 19 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/AeonixNavigation.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. 2 | 3 | #include "AeonixNavigation.h" 4 | 5 | DEFINE_LOG_CATEGORY(LogAeonixNavigation); 6 | DEFINE_LOG_CATEGORY(VLogAeonixNavigation); 7 | DEFINE_LOG_CATEGORY(LogAeonixRegen); 8 | 9 | 10 | 11 | #define LOCTEXT_NAMESPACE "FAeonixNavigationModule" 12 | 13 | void FAeonixNavigationModule::StartupModule() 14 | { 15 | // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module 16 | } 17 | 18 | void FAeonixNavigationModule::ShutdownModule() 19 | { 20 | // This function may be called during shutdown to clean up your module. For modules that support dynamic reloading, 21 | // we call this function before unloading the module. 22 | } 23 | 24 | #undef LOCTEXT_NAMESPACE 25 | 26 | IMPLEMENT_MODULE(FAeonixNavigationModule, AeonixNavigation) -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/AeonixNavigationLibrary.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Kismet/BlueprintFunctionLibrary.h" 4 | #include "Pathfinding/AeonixNavigationPath.h" 5 | #include "AeonixNavigationLibrary.generated.h" 6 | 7 | UCLASS() 8 | class AEONIXNAVIGATION_API UAeonixNavigationLibrary : public UBlueprintFunctionLibrary 9 | { 10 | GENERATED_BODY() 11 | 12 | public: 13 | /** Get the array of path points from a navigation path */ 14 | UFUNCTION(BlueprintPure, Category = "Aeonix|Path") 15 | static TArray GetPathPoints(const FAeonixNavigationPath& Path); 16 | 17 | /** Get the number of points in the path */ 18 | UFUNCTION(BlueprintPure, Category = "Aeonix|Path") 19 | static int32 GetNumPathPoints(const FAeonixNavigationPath& Path); 20 | 21 | /** Check if the path is ready/valid */ 22 | UFUNCTION(BlueprintPure, Category = "Aeonix|Path") 23 | static bool IsPathReady(const FAeonixNavigationPath& Path); 24 | }; 25 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Interface/AeonixDebugDrawInterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "AeonixDebugDrawInterface.generated.h" 4 | 5 | UINTERFACE(MinimalAPI) 6 | class UAeonixDebugDrawInterface : public UInterface 7 | { 8 | GENERATED_BODY() 9 | }; 10 | 11 | class AEONIXNAVIGATION_API IAeonixDebugDrawInterface 12 | { 13 | GENERATED_BODY() 14 | 15 | public: 16 | /** Add interface function declarations here */ 17 | 18 | virtual void AeonixDrawDebugString(const FVector& Position, const FString& String, const FColor& Color) const = 0; 19 | virtual void AeonixDrawDebugBox(const FVector& Position, const float Size, const FColor& Color) const = 0; 20 | virtual void AeonixDrawDebugLine(const FVector& Start, const FVector& End, const FColor& Color, float Thickness = 0.0f) const = 0; 21 | virtual void AeonixDrawDebugDirectionalArrow(const FVector& Start, const FVector& End, const FColor& Color, float ArrowSize = 0.0f) const = 0; 22 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Component/AeonixBlockingComponent.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | 4 | #include "Component/AeonixBlockingComponent.h" 5 | 6 | 7 | // Sets default values for this component's properties 8 | UAeonixBlockingComponent::UAeonixBlockingComponent() 9 | { 10 | // Set this component to be initialized when the game starts, and to be ticked every frame. You can turn these features 11 | // off to improve performance if you don't need them. 12 | PrimaryComponentTick.bCanEverTick = true; 13 | 14 | // ... 15 | } 16 | 17 | 18 | // Called when the game starts 19 | void UAeonixBlockingComponent::BeginPlay() 20 | { 21 | Super::BeginPlay(); 22 | 23 | // ... 24 | 25 | } 26 | 27 | 28 | // Called every frame 29 | void UAeonixBlockingComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) 30 | { 31 | Super::TickComponent(DeltaTime, TickType, ThisTickFunction); 32 | 33 | // ... 34 | } 35 | 36 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and normalize line endings to LF in the repository 2 | * text=auto 3 | 4 | # Source code - always LF 5 | *.cpp text eol=lf 6 | *.h text eol=lf 7 | *.c text eol=lf 8 | *.hpp text eol=lf 9 | *.cs text eol=lf 10 | *.inl text eol=lf 11 | 12 | # Unreal Engine files - always LF 13 | *.uproject text eol=lf 14 | *.uplugin text eol=lf 15 | *.ini text eol=lf 16 | *.uasset binary 17 | *.umap binary 18 | 19 | # Documentation - always LF 20 | *.md text eol=lf 21 | *.txt text eol=lf 22 | 23 | # Scripts - platform specific 24 | *.sh text eol=lf 25 | *.bat text eol=crlf 26 | *.cmd text eol=crlf 27 | 28 | # Data formats 29 | *.json text eol=lf 30 | *.xml text eol=lf 31 | *.yaml text eol=lf 32 | *.yml text eol=lf 33 | 34 | # Binary files 35 | *.dll binary 36 | *.exe binary 37 | *.so binary 38 | *.dylib binary 39 | *.a binary 40 | *.lib binary 41 | *.pdb binary 42 | *.png binary 43 | *.jpg binary 44 | *.jpeg binary 45 | *.gif binary 46 | *.ico binary 47 | *.wav binary 48 | *.mp3 binary 49 | -------------------------------------------------------------------------------- /AeonixNavigation.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "FriendlyName": "Aeonix Navigation", 6 | "Description": "3D Navigation System", 7 | "Category": "Other", 8 | "CreatedBy": "Chris Ashworth", 9 | "CreatedByURL": "", 10 | "DocsURL": "", 11 | "MarketplaceURL": "", 12 | "SupportURL": "", 13 | "CanContainContent": true, 14 | "IsBetaVersion": true, 15 | "Installed": false, 16 | "Modules": [ 17 | { 18 | "Name": "AeonixNavigation", 19 | "Type": "Runtime", 20 | "LoadingPhase": "PreDefault" 21 | }, 22 | { 23 | "Name": "AeonixEditor", 24 | "Type": "Editor", 25 | "LoadingPhase": "PostEngineInit" 26 | }, 27 | { 28 | "Name": "AeonixNavigationTests", 29 | "Type": "DeveloperTool", 30 | "LoadingPhase": "Default" 31 | }, 32 | { 33 | "Name": "AeonixGameUtils", 34 | "Type": "Runtime", 35 | "LoadingPhase": "Default" 36 | } 37 | ], 38 | "Plugins": [ 39 | { 40 | "Name": "StateTree", 41 | "Enabled": true 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixNode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Data/AeonixLink.h" 4 | #include "Data/AeonixDefines.h" 5 | 6 | struct AEONIXNAVIGATION_API AeonixNode 7 | { 8 | mortoncode_t Code; 9 | 10 | AeonixLink Parent; 11 | AeonixLink FirstChild; 12 | 13 | AeonixLink myNeighbours[6]; 14 | 15 | AeonixNode() : 16 | Code(0), 17 | Parent(AeonixLink::GetInvalidLink()), 18 | FirstChild(AeonixLink::GetInvalidLink()) {} 19 | 20 | bool HasChildren() const { return FirstChild.IsValid(); } 21 | 22 | }; 23 | 24 | FORCEINLINE FArchive &operator <<(FArchive &Ar, AeonixNode& aAeonixNode) 25 | { 26 | // Cast mortoncode_t (uint_fast64_t) to uint64 for serialization 27 | uint64 CodeValue = aAeonixNode.Code; 28 | Ar << CodeValue; 29 | 30 | if (Ar.IsLoading()) 31 | { 32 | aAeonixNode.Code = CodeValue; 33 | } 34 | 35 | Ar << aAeonixNode.Parent; 36 | Ar << aAeonixNode.FirstChild; 37 | 38 | for (int i = 0; i < 6; i++) 39 | { 40 | Ar << aAeonixNode.myNeighbours[i]; 41 | } 42 | 43 | return Ar; 44 | } -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | IndentWidth: 4 5 | TabWidth: 4 6 | UseTab: Always 7 | Standard: Cpp11 8 | AccessModifierOffset: -4 9 | AlignAfterOpenBracket: DontAlign 10 | AlignEscapedNewlines: Right 11 | AlignTrailingComments: true 12 | AllowShortCaseLabelsOnASingleLine: true 13 | AllowShortFunctionsOnASingleLine: InlineOnly 14 | BreakBeforeBraces: Allman 15 | BreakConstructorInitializersBeforeComma: true 16 | ColumnLimit: 0 17 | PointerAlignment: Left 18 | SpacesInAngles: false 19 | --- 20 | Language: ObjC 21 | -------------------------------------------------------------------------------- /.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 | *.sln 37 | *.suo 38 | *.opensdf 39 | *.sdf 40 | *.VC.opendb 41 | *.VC.db 42 | 43 | # Precompiled Assets 44 | SourceArt/**/*.png 45 | SourceArt/**/*.tga 46 | 47 | # Binary Files 48 | **/Binaries/* 49 | 50 | # Builds 51 | **/Build/* 52 | 53 | # Don't ignore icon files in Build 54 | !Build/**/*.ico 55 | 56 | # Configuration files generated by the Editor 57 | **/Saved/* 58 | 59 | # Compiled source files for the engine to use 60 | **/Intermediate/* 61 | 62 | 63 | # Cache files for the editor to use 64 | **/DerivedDataCache/* 65 | *.cpp~ 66 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Subsystem/AeonixCollisionSubsystem.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "Interface/AeonixCollisionQueryInterface.h" 6 | 7 | #include "Subsystems/WorldSubsystem.h" 8 | 9 | #include "AeonixCollisionSubsystem.generated.h" 10 | 11 | /** 12 | * Subsystem that provides an interface for doing blocking tests in the world 13 | */ 14 | UCLASS() 15 | class AEONIXNAVIGATION_API UAeonixCollisionSubsystem : public UWorldSubsystem, public IAeonixCollisionQueryInterface 16 | { 17 | GENERATED_BODY() 18 | 19 | /* IAeonixCollisionQueryInterface BEGIN */ 20 | virtual bool IsBlocked(const FVector& Position, const float VoxelSize, ECollisionChannel CollisionChannel, const float AgentRadius) const override; 21 | virtual bool IsLeafBlocked(const FVector& Position, const float LeafSize, ECollisionChannel CollisionChannel, const float AgentRadius) const override; 22 | /* IAeonixCollisionQueryInterface END */ 23 | 24 | protected: 25 | virtual bool DoesSupportWorldType(const EWorldType::Type WorldType) const override; 26 | }; 27 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Library/libmorton/morton_common.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // Libmorton - Common helper methods needed in Morton encoding/decoding 4 | 5 | #include 6 | #if _MSC_VER 7 | #include 8 | #endif 9 | 10 | template 11 | inline bool findFirstSetBit(const morton x, unsigned long* firstbit_location) { 12 | #if _MSC_VER && !_WIN64 13 | // 32 BIT on 32 BIT 14 | if (sizeof(morton) <= 4) { 15 | return _BitScanReverse(firstbit_location, x) != 0; 16 | } 17 | // 64 BIT on 32 BIT 18 | else { 19 | *firstbit_location = 0; 20 | if (_BitScanReverse(firstbit_location, (x >> 32))) { // check first part 21 | firstbit_location += 32; 22 | return true; 23 | } 24 | return _BitScanReverse(firstbit_location, (x & 0xFFFFFFFF)) != 0; 25 | } 26 | #elif _MSC_VER && _WIN64 27 | // 32 or 64 BIT on 64 BIT 28 | return _BitScanReverse64(firstbit_location, x) != 0; 29 | #elif __GNUC__ 30 | if (x == 0) { 31 | return false; 32 | } 33 | else { 34 | *firstbit_location = static_cast((sizeof(morton)*8) - __builtin_clzll(x)); 35 | return true; 36 | } 37 | #endif 38 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Chris Ashworth 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 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Library/libmorton/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Jeroen Baert 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 | -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/AeonixPathDebugActor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "GameFramework/Actor.h" 4 | 5 | #include "AeonixPathDebugActor.generated.h" 6 | 7 | class AAeonixBoundingVolume; 8 | class UAeonixNavAgentComponent; 9 | 10 | UENUM(BlueprintType) 11 | enum class EAeonixPathDebugActorType : uint8 12 | { 13 | START, 14 | END 15 | }; 16 | 17 | /** 18 | * Debug actor for testing Aeonix pathfinding, drop two into your level, set one to start and one to end 19 | */ 20 | UCLASS() 21 | class AEONIXEDITOR_API AAeonixPathDebugActor : public AActor 22 | { 23 | GENERATED_BODY() 24 | 25 | public: 26 | explicit AAeonixPathDebugActor(const FObjectInitializer& Initializer); 27 | 28 | virtual void PostEditMove(bool bFinished) override; 29 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 30 | virtual void OnConstruction(const FTransform& Transform) override; 31 | virtual void BeginDestroy() override; 32 | virtual void Destroyed() override; 33 | 34 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Aeonix") 35 | EAeonixPathDebugActorType DebugType; 36 | 37 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Aeonix") 38 | TObjectPtr NavAgentComponent; 39 | }; 40 | 41 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixHandleTypes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "AeonixHandleTypes.generated.h" 4 | 5 | class AAeonixBoundingVolume; 6 | class AAeonixModifierVolume; 7 | class UAeonixNavAgentComponent; 8 | 9 | USTRUCT() 10 | struct FAeonixBoundingVolumeHandle 11 | { 12 | GENERATED_BODY() 13 | 14 | FAeonixBoundingVolumeHandle(){} 15 | FAeonixBoundingVolumeHandle(AAeonixBoundingVolume* Volume) : VolumeHandle(Volume) {} 16 | 17 | bool operator==(const FAeonixBoundingVolumeHandle& Volume ) const { return Volume.VolumeHandle == VolumeHandle; } 18 | 19 | UPROPERTY() 20 | TObjectPtr VolumeHandle; 21 | 22 | UPROPERTY() 23 | TArray> ModifierVolumes; 24 | }; 25 | 26 | USTRUCT() 27 | struct FAeonixNavAgentHandle 28 | { 29 | GENERATED_BODY() 30 | 31 | FAeonixNavAgentHandle(){} 32 | explicit FAeonixNavAgentHandle(UAeonixNavAgentComponent* Agent) : NavAgentComponent(Agent) {} 33 | 34 | bool operator==(const FAeonixNavAgentHandle& Agent ) const { return Agent.NavAgentComponent == NavAgentComponent; } 35 | bool operator==(UAeonixNavAgentComponent* AgentPtr ) const { return AgentPtr == NavAgentComponent; } 36 | 37 | UPROPERTY() 38 | TObjectPtr NavAgentComponent; 39 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Interface/AeonixCollisionQueryInterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "AeonixCollisionQueryInterface.generated.h" 4 | 5 | UINTERFACE(MinimalAPI, Blueprintable) 6 | class UAeonixCollisionQueryInterface : public UInterface 7 | { 8 | GENERATED_BODY() 9 | }; 10 | 11 | class IAeonixCollisionQueryInterface 12 | { 13 | GENERATED_BODY() 14 | 15 | public: 16 | /** Add interface function declarations here */ 17 | virtual bool IsBlocked(const FVector& Position, const float VoxelSize, ECollisionChannel CollisionChannel, const float AgentRadius) const = 0; 18 | 19 | /** 20 | * Tests if an entire leaf node (4x4x4 voxel block) contains any blocking geometry. 21 | * Used for two-pass rasterization optimization - if this returns false, all 64 voxels in the leaf are guaranteed clear. 22 | * @param Position Center position of the leaf node 23 | * @param LeafSize Size of the entire leaf node (typically VoxelSize * 4) 24 | * @param CollisionChannel Collision channel to test against 25 | * @param AgentRadius Radius of the agent for clearance testing 26 | * @return true if the leaf node contains any blocking geometry, false if completely clear 27 | */ 28 | virtual bool IsLeafBlocked(const FVector& Position, const float LeafSize, ECollisionChannel CollisionChannel, const float AgentRadius) const = 0; 29 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Data/AeonixDefines.cpp: -------------------------------------------------------------------------------- 1 | #include "Data/AeonixDefines.h" 2 | 3 | const FIntVector AeonixStatics::dirs[] = { 4 | FIntVector(1, 0, 0), 5 | FIntVector(-1, 0, 0), 6 | FIntVector(0, 1, 0), 7 | FIntVector(0, -1, 0), 8 | FIntVector(0, 0, 1), 9 | FIntVector(0, 0, -1)}; 10 | 11 | const nodeindex_t AeonixStatics::dirChildOffsets[6][4] = { 12 | {0, 4, 2, 6}, 13 | {1, 3, 5, 7}, 14 | {0, 1, 4, 5}, 15 | {2, 3, 6, 7}, 16 | {0, 1, 2, 3}, 17 | {4, 5, 6, 7}}; 18 | 19 | const nodeindex_t AeonixStatics::dirLeafChildOffsets[6][16] = { 20 | {0, 2, 16, 18, 4, 6, 20, 22, 32, 34, 48, 50, 36, 38, 52, 54}, 21 | {9, 11, 25, 27, 13, 15, 29, 31, 41, 43, 57, 59, 45, 47, 61, 63}, 22 | {0, 1, 8, 9, 4, 5, 12, 13, 32, 33, 40, 41, 36, 37, 44, 45}, 23 | {18, 19, 26, 27, 22, 23, 30, 31, 50, 51, 58, 59, 54, 55, 62, 63}, 24 | {0, 1, 8, 9, 2, 3, 10, 11, 16, 17, 24, 25, 18, 19, 26, 27}, 25 | {36, 37, 44, 45, 38, 39, 46, 47, 52, 53, 60, 61, 54, 55, 62, 63} 26 | 27 | }; 28 | 29 | const FColor AeonixStatics::myLayerColors[] = {FColor::Orange, FColor::Yellow, FColor::White, FColor::Blue, FColor::Turquoise, FColor::Cyan, FColor::Emerald, FColor::Orange}; 30 | 31 | const FColor AeonixStatics::myLinkColors[] = {FColor(0xFF000000), FColor(0xFF444444), FColor(0xFF888888), FColor(0xFFBBBBBB), FColor(0xFFFFFFFF), FColor(0xFF999999), FColor(0xFF777777), FColor(0xFF555555)}; -------------------------------------------------------------------------------- /Source/AeonixGameUtils/AeonixGameUtils.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | using System.IO; 5 | 6 | public class AeonixGameUtils : ModuleRules 7 | { 8 | public AeonixGameUtils(ReadOnlyTargetRules Target) : base(Target) 9 | { 10 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 11 | 12 | bLegacyParentIncludePaths = false; 13 | CppStandard = CppStandardVersion.Default; 14 | 15 | PublicIncludePaths.AddRange( 16 | new string[] { 17 | Path.Combine(ModuleDirectory, "Public") 18 | } 19 | ); 20 | 21 | PrivateIncludePaths.AddRange( 22 | new string[] { 23 | "AeonixGameUtils/Private", 24 | } 25 | ); 26 | 27 | PublicDependencyModuleNames.AddRange( 28 | new string[] 29 | { 30 | "Core", 31 | "AeonixNavigation", 32 | } 33 | ); 34 | 35 | PrivateDependencyModuleNames.AddRange( 36 | new string[] 37 | { 38 | "CoreUObject", 39 | "Engine", 40 | } 41 | ); 42 | 43 | DynamicallyLoadedModuleNames.AddRange( 44 | new string[] 45 | { 46 | } 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Task/AeonixFindPathTask.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Data/AeonixLink.h" 4 | #include "Pathfinding/AeonixPathFinder.h" 5 | #include "Data/AeonixTypes.h" 6 | 7 | #include "Async/AsyncWork.h" 8 | #include "HAL/ThreadSafeBool.h" 9 | 10 | struct FAeonixData; 11 | struct AeonixPathFinderSettings; 12 | 13 | class FAeonixFindPathTask : public FNonAbandonableTask 14 | { 15 | friend class FAutoDeleteAsyncTask; 16 | 17 | public: 18 | FAeonixFindPathTask(const FAeonixData& Data, const FAeonixPathFinderSettings& aSettings, const AeonixLink aStart, const AeonixLink aGoal, const FVector& aStartPos, const FVector& aTargetPos, FAeonixNavigationPath& oPath, FAeonixPathFindRequest& aRequest) 19 | : NavigationData(Data) 20 | , Start(aStart) 21 | , Goal(aGoal) 22 | , StartPos(aStartPos) 23 | , TargetPos(aTargetPos) 24 | , Path(oPath) 25 | , Settings(aSettings) 26 | , Request(aRequest) 27 | { 28 | } 29 | 30 | protected: 31 | const FAeonixData& NavigationData; 32 | 33 | AeonixLink Start; 34 | AeonixLink Goal; 35 | FVector StartPos; 36 | FVector TargetPos; 37 | FAeonixNavigationPath& Path; 38 | const FAeonixPathFinderSettings Settings; 39 | FAeonixPathFindRequest& Request; 40 | 41 | void DoWork(); 42 | 43 | FORCEINLINE TStatId GetStatId() const 44 | { 45 | RETURN_QUICK_DECLARE_CYCLE_STAT(FAeonixFindPathTask, STATGROUP_ThreadPoolAsyncTasks); 46 | } 47 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Subsystem/AeonixCollisionSubsystem.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | 4 | #include "Subsystem/AeonixCollisionSubsystem.h" 5 | 6 | bool UAeonixCollisionSubsystem::IsBlocked(const FVector& Position, const float VoxelSize, ECollisionChannel CollisionChannel, const float AgentRadius) const 7 | { 8 | FCollisionQueryParams Params; 9 | Params.bFindInitialOverlaps = true; 10 | Params.bTraceComplex = false; 11 | Params.TraceTag = "AeonixLeafRasterize"; 12 | 13 | return GetWorld()->OverlapBlockingTestByChannel(Position, FQuat::Identity, CollisionChannel, FCollisionShape::MakeBox(FVector(VoxelSize + AgentRadius)), Params); 14 | } 15 | 16 | bool UAeonixCollisionSubsystem::IsLeafBlocked(const FVector& Position, const float LeafSize, ECollisionChannel CollisionChannel, const float AgentRadius) const 17 | { 18 | FCollisionQueryParams Params; 19 | Params.bFindInitialOverlaps = true; 20 | Params.bTraceComplex = false; 21 | Params.TraceTag = "AeonixWholeLeafTest"; 22 | 23 | // Test the entire leaf volume (4x4x4 voxel block) with agent radius buffer 24 | return GetWorld()->OverlapBlockingTestByChannel(Position, FQuat::Identity, CollisionChannel, FCollisionShape::MakeBox(FVector(LeafSize + AgentRadius)), Params); 25 | } 26 | 27 | bool UAeonixCollisionSubsystem::DoesSupportWorldType(const EWorldType::Type WorldType) const 28 | { 29 | // All the worlds, so it works in editor 30 | return true; 31 | } -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/AeonixEditorUtilityWidget.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | 4 | #include "AeonixEditorUtilityWidget.h" 5 | 6 | #include "Subsystem/AeonixSubsystem.h" 7 | 8 | void UAeonixEditorUtilityWidget::CompleteAllPendingPathfindingTasks() 9 | { 10 | 11 | 12 | 13 | 14 | GetWorld()->GetSubsystem()->CompleteAllPendingPathfindingTasks(); 15 | } 16 | 17 | int32 UAeonixEditorUtilityWidget::GetNumberOfPendingPathFindTasks() const 18 | { 19 | if (GEditor->GetPIEWorldContext()) 20 | { 21 | return GEditor->GetPIEWorldContext()->World()->GetSubsystem()->GetNumberOfPendingTasks(); 22 | } 23 | 24 | return GetWorld()->GetSubsystem()->GetNumberOfPendingTasks(); 25 | } 26 | 27 | int32 UAeonixEditorUtilityWidget::GetNumberOfRegisteredNavAgents() const 28 | { 29 | if (GEditor->GetPIEWorldContext()) 30 | { 31 | return GEditor->GetPIEWorldContext()->World()->GetSubsystem()->GetNumberOfRegisteredNavAgents(); 32 | } 33 | 34 | return GetWorld()->GetSubsystem()->GetNumberOfRegisteredNavAgents(); 35 | } 36 | 37 | int32 UAeonixEditorUtilityWidget::GetNumberOfRegisteredNavVolumes() const 38 | { 39 | if (GEditor->GetPIEWorldContext()) 40 | { 41 | return GEditor->GetPIEWorldContext()->World()->GetSubsystem()->GetNumberOfRegisteredNavVolumes(); 42 | } 43 | 44 | return GetWorld()->GetSubsystem()->GetNumberOfRegisteredNavVolumes(); 45 | } -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixLeafNode.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "AeonixNavigation/Private/Library/libmorton/morton.h" 4 | #include "Data/AeonixDefines.h" 5 | 6 | struct AEONIXNAVIGATION_API AeonixLeafNode 7 | { 8 | uint_fast64_t VoxelGrid = 0; 9 | 10 | inline bool GetNodeAt(uint_fast32_t aX, uint_fast32_t aY, uint_fast32_t aZ) const 11 | { 12 | uint_fast64_t index = morton3D_64_encode(aX, aY, aZ); 13 | return (VoxelGrid & (1ULL << index)) != 0; 14 | } 15 | 16 | inline void SetNodeAt(uint_fast32_t aX, uint_fast32_t aY, uint_fast32_t aZ) 17 | { 18 | uint_fast64_t index = morton3D_64_encode(aX, aY, aZ); 19 | VoxelGrid |= 1ULL << index; 20 | } 21 | 22 | inline void SetNode(uint8 aIndex) 23 | { 24 | VoxelGrid |= 1ULL << aIndex; 25 | } 26 | 27 | inline bool GetNode(mortoncode_t aIndex) const 28 | { 29 | return (VoxelGrid & (1ULL << aIndex)) != 0; 30 | } 31 | 32 | inline bool IsCompletelyBlocked() const 33 | { 34 | return VoxelGrid == -1; 35 | } 36 | 37 | inline bool IsEmpty() const 38 | { 39 | return VoxelGrid == 0; 40 | } 41 | 42 | inline void Clear() 43 | { 44 | VoxelGrid = 0; 45 | } 46 | }; 47 | 48 | FORCEINLINE FArchive& operator<<(FArchive& Ar, AeonixLeafNode& aAeonixLeafNode) 49 | { 50 | // Cast to uint64 for serialization since FArchive doesn't support uint_fast64_t directly 51 | uint64 VoxelGridValue = aAeonixLeafNode.VoxelGrid; 52 | Ar << VoxelGridValue; 53 | 54 | if (Ar.IsLoading()) 55 | { 56 | aAeonixLeafNode.VoxelGrid = VoxelGridValue; 57 | } 58 | 59 | return Ar; 60 | } 61 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixDefines.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "AITypes.h" 4 | 5 | typedef uint8 layerindex_t; 6 | typedef int32 nodeindex_t; 7 | typedef uint8 subnodeindex_t; 8 | typedef uint_fast64_t mortoncode_t; 9 | typedef uint_fast32_t posint_t; 10 | 11 | UENUM(BlueprintType) 12 | enum class EBuildTrigger : uint8 13 | { 14 | OnEdit UMETA(DisplayName = "On Edit"), 15 | Manual UMETA(DisplayName = "Manual") 16 | }; 17 | 18 | enum class dir : uint8 19 | { 20 | pX, 21 | nX, 22 | pY, 23 | nY, 24 | pZ, 25 | nZ 26 | }; 27 | 28 | #define LEAF_LAYER_INDEX 14 29 | 30 | class AEONIXNAVIGATION_API AeonixStatics 31 | { 32 | public: 33 | static const FIntVector dirs[]; 34 | static const nodeindex_t dirChildOffsets[6][4]; 35 | static const nodeindex_t dirLeafChildOffsets[6][16]; 36 | static const FColor myLayerColors[]; 37 | static const FColor myLinkColors[]; 38 | }; 39 | 40 | UENUM(BlueprintType) 41 | namespace EAeonixPathfindingRequestResult 42 | { 43 | enum Type 44 | { 45 | Failed, // Something went wrong 46 | ReadyToPath, // Pre-reqs satisfied 47 | AlreadyAtGoal, // No need to move 48 | Deferred, // Passed request to another thread, need to wait 49 | Success // it worked! 50 | }; 51 | } 52 | 53 | struct AEONIXNAVIGATION_API FAeonixPathfindingRequestResult 54 | { 55 | FAIRequestID MoveId; 56 | TEnumAsByte Code; 57 | 58 | FAeonixPathfindingRequestResult() 59 | : MoveId(FAIRequestID::InvalidRequest) 60 | , Code(EAeonixPathfindingRequestResult::Failed) 61 | { 62 | } 63 | operator EAeonixPathfindingRequestResult::Type() const { return Code; } 64 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixStats.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Aeonix Navigation. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "Stats/Stats.h" 6 | 7 | /** 8 | * Aeonix Navigation Performance Stats 9 | * 10 | * Use 'stat Aeonix' in console to view these stats in real-time. 11 | * Use 'stat startfile' / 'stat stopfile' for CSV profiling. 12 | */ 13 | 14 | // Declare the main Aeonix stats group 15 | DECLARE_STATS_GROUP(TEXT("Aeonix"), STATGROUP_Aeonix, STATCAT_Advanced); 16 | 17 | // Octree Generation Stats 18 | DECLARE_CYCLE_STAT(TEXT("Full Octree Generation"), STAT_AeonixFullOctreeGen, STATGROUP_Aeonix); 19 | DECLARE_CYCLE_STAT(TEXT("Dynamic Region Sync"), STAT_AeonixDynamicSync, STATGROUP_Aeonix); 20 | 21 | // Async Dynamic Region Stats (3 levels of granularity) 22 | DECLARE_CYCLE_STAT(TEXT("Dynamic Region Async"), STAT_AeonixDynamicAsync, STATGROUP_Aeonix); 23 | DECLARE_CYCLE_STAT(TEXT("Dynamic Async Chunk"), STAT_AeonixDynamicAsyncChunk, STATGROUP_Aeonix); 24 | DECLARE_CYCLE_STAT(TEXT("Dynamic Async Leaf"), STAT_AeonixDynamicAsyncLeaf, STATGROUP_Aeonix); 25 | 26 | // Pathfinding Stats 27 | DECLARE_CYCLE_STAT(TEXT("Pathfinding Sync"), STAT_AeonixPathfindingSync, STATGROUP_Aeonix); 28 | DECLARE_CYCLE_STAT(TEXT("Pathfinding Async"), STAT_AeonixPathfindingAsync, STATGROUP_Aeonix); 29 | 30 | // Path Smoothing Stats 31 | DECLARE_CYCLE_STAT(TEXT("Path Chaikin Smoothing"), STAT_AeonixPathChaikinSmoothing, STATGROUP_Aeonix); 32 | DECLARE_CYCLE_STAT(TEXT("Path String Pulling"), STAT_AeonixPathStringPulling, STATGROUP_Aeonix); 33 | DECLARE_CYCLE_STAT(TEXT("Path Position Smoothing"), STAT_AeonixPathPositionSmoothing, STATGROUP_Aeonix); 34 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Task/StateTreeTask_AeonixMoveTo.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "StateTreeTaskBase.h" 4 | #include "StateTreeTask_AeonixMoveTo.generated.h" 5 | 6 | class UAITask_AeonixMoveTo; 7 | class AAeonixBoundingVolume; 8 | class UAeonixNavAgentComponent; 9 | 10 | USTRUCT() 11 | struct FStateTreeTask_AeonixMoveToInstanceData 12 | { 13 | GENERATED_BODY() 14 | 15 | // Blackboard/StateTree input: goal location 16 | UPROPERTY(EditAnywhere, Category = Input) 17 | FVector GoalLocation = FVector::ZeroVector; 18 | 19 | // Optional: goal actor 20 | UPROPERTY(EditAnywhere, Category = Input) 21 | TWeakObjectPtr GoalActor; 22 | 23 | // Optional: acceptance radius 24 | UPROPERTY(EditAnywhere, Category = Input) 25 | float AcceptableRadius = 50.f; 26 | 27 | // Optional: agent component 28 | UPROPERTY(EditAnywhere, Category = Input) 29 | TWeakObjectPtr AgentComponent; 30 | 31 | // Internal task state 32 | UPROPERTY(Transient) 33 | UAITask_AeonixMoveTo* MoveTask = nullptr; 34 | }; 35 | 36 | USTRUCT() 37 | struct AEONIXNAVIGATION_API FStateTreeTask_AeonixMoveTo : public FStateTreeTaskBase 38 | { 39 | GENERATED_BODY(); 40 | 41 | typedef FStateTreeTask_AeonixMoveToInstanceData FInstanceDataType; 42 | 43 | EStateTreeRunStatus EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const; 44 | EStateTreeRunStatus Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const; 45 | void ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const; 46 | }; 47 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Component/AeonixFlyingMovementComponent.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "GameFramework/PawnMovementComponent.h" 6 | #include "CoreMinimal.h" 7 | #include "AeonixFlyingMovementComponent.generated.h" 8 | 9 | USTRUCT(BlueprintType) 10 | struct AEONIXNAVIGATION_API FAeonixFlyingSettings 11 | { 12 | GENERATED_BODY() 13 | 14 | // Maximum flight speed in units per second 15 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Flying Movement") 16 | float MaxSpeed = 1200.0f; 17 | }; 18 | 19 | UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) 20 | class AEONIXNAVIGATION_API UAeonixFlyingMovementComponent : public UPawnMovementComponent 21 | { 22 | GENERATED_BODY() 23 | 24 | public: 25 | UAeonixFlyingMovementComponent(); 26 | 27 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix Flying Movement") 28 | FAeonixFlyingSettings FlyingSettings; 29 | 30 | // Movement properties 31 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Movement") 32 | float MaxSpeed; 33 | 34 | // Movement interface 35 | virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; 36 | virtual void RequestDirectMove(const FVector& MoveVelocity, bool bForceMaxSpeed) override; 37 | virtual bool CanStartPathFollowing() const override; 38 | 39 | UFUNCTION(BlueprintCallable, Category = "Aeonix Flying Movement") 40 | FVector GetCurrentVelocity() const { return Velocity; } 41 | 42 | UFUNCTION(BlueprintCallable, Category = "Aeonix Flying Movement") 43 | float GetCurrentSpeed() const { return Velocity.Size(); } 44 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixOctreeData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Data/AeonixLeafNode.h" 4 | #include "Data/AeonixNode.h" 5 | 6 | #include "AeonixOctreeData.generated.h" 7 | 8 | USTRUCT() 9 | struct AEONIXNAVIGATION_API FAeonixOctreeData 10 | { 11 | GENERATED_BODY() 12 | 13 | // SVO data 14 | TArray> Layers; 15 | TArray LeafNodes; 16 | // temporary data used during nav data generation first pass rasterize 17 | TArray> BlockedIndices; 18 | 19 | void Reset() 20 | { 21 | Layers.Empty(); 22 | LeafNodes.Empty(); 23 | } 24 | 25 | int GetSize() const 26 | { 27 | int Result = 0; 28 | Result += LeafNodes.Num() * sizeof(AeonixLeafNode); 29 | for (int i = 0; i < Layers.Num(); i++) 30 | { 31 | Result += Layers[i].Num() * sizeof(AeonixNode); 32 | } 33 | 34 | return Result; 35 | } 36 | 37 | uint8 NumLayers = 0; 38 | int NumBytes = 0; 39 | 40 | const uint8 GetNumLayers() const { return NumLayers; } 41 | TArray& GetLayer(layerindex_t aLayer) { return Layers[aLayer]; }; 42 | const TArray& GetLayer(layerindex_t aLayer) const { return Layers[aLayer]; }; 43 | const AeonixNode& GetNode(const AeonixLink& aLink) const; 44 | const AeonixLeafNode& GetLeafNode(nodeindex_t aIndex) const; 45 | void GetLeafNeighbours(const AeonixLink& aLink, TArray& oNeighbours) const; 46 | void GetNeighbours(const AeonixLink& aLink, TArray& oNeighbours) const; 47 | }; 48 | 49 | FORCEINLINE FArchive& operator<<(FArchive& Ar, FAeonixOctreeData& AeonixData) 50 | { 51 | Ar << AeonixData.Layers; 52 | Ar << AeonixData.LeafNodes; 53 | Ar << AeonixData.NumLayers; 54 | 55 | return Ar; 56 | } 57 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixLink.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | struct AEONIXNAVIGATION_API AeonixLink 4 | { 5 | unsigned int LayerIndex:4; 6 | unsigned int NodeIndex:22; 7 | unsigned int SubnodeIndex:6; 8 | 9 | AeonixLink() : 10 | LayerIndex(15), 11 | NodeIndex(0), 12 | SubnodeIndex(0) {} 13 | 14 | AeonixLink(uint8 aLayer, uint_fast32_t aNodeIndex, uint8 aSubNodeIndex) 15 | : LayerIndex(aLayer), 16 | NodeIndex(aNodeIndex), 17 | SubnodeIndex(aSubNodeIndex) {} 18 | 19 | uint8 GetLayerIndex() const { return LayerIndex; } 20 | void SetLayerIndex(const uint8 aLayerIndex) { LayerIndex = aLayerIndex; } 21 | 22 | uint_fast32_t GetNodeIndex() const { return NodeIndex; } 23 | void SetNodeIndex(const uint_fast32_t aNodeIndex) { NodeIndex = aNodeIndex; } 24 | 25 | uint8 GetSubnodeIndex() const { return SubnodeIndex; } 26 | void SetSubnodeIndex(const uint8 aSubnodeIndex) { SubnodeIndex = aSubnodeIndex; } 27 | 28 | bool IsValid() const { return LayerIndex != 15; } 29 | void SetInvalid() { LayerIndex = 15; } 30 | 31 | bool operator==(const AeonixLink& aOther) const { 32 | return memcmp(this, &aOther, sizeof(AeonixLink)) == 0; 33 | } 34 | 35 | static AeonixLink GetInvalidLink() { return AeonixLink(15, 0, 0); } 36 | 37 | FString ToString() 38 | { 39 | return FString::Printf(TEXT("%i:%i:%i"), LayerIndex, NodeIndex, SubnodeIndex); 40 | }; 41 | 42 | }; 43 | 44 | FORCEINLINE uint32 GetTypeHash(const AeonixLink& b) 45 | { 46 | uint32 Result; 47 | FMemory::Memcpy(&Result, &b, sizeof(uint32)); 48 | return Result; 49 | } 50 | 51 | 52 | FORCEINLINE FArchive &operator <<(FArchive &Ar, AeonixLink& aAeonixLink) 53 | { 54 | Ar.Serialize(&aAeonixLink, sizeof(AeonixLink)); 55 | return Ar; 56 | } -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Component/AeonixFlyingMovementComponent.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #include "Component/AeonixFlyingMovementComponent.h" 4 | #include "GameFramework/Pawn.h" 5 | #include "Components/StaticMeshComponent.h" 6 | #include "Engine.h" 7 | #include "DrawDebugHelpers.h" 8 | 9 | UAeonixFlyingMovementComponent::UAeonixFlyingMovementComponent() 10 | { 11 | PrimaryComponentTick.bCanEverTick = true; 12 | 13 | // Configure base pawn movement 14 | MaxSpeed = FlyingSettings.MaxSpeed; 15 | } 16 | 17 | void UAeonixFlyingMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) 18 | { 19 | // Update base movement settings from our settings 20 | MaxSpeed = FlyingSettings.MaxSpeed; 21 | 22 | // Apply movement to the UpdatedComponent 23 | // Velocity is set by path following via RequestDirectMove 24 | if (UpdatedComponent && !Velocity.IsNearlyZero()) 25 | { 26 | FHitResult Hit; 27 | SafeMoveUpdatedComponent(Velocity * DeltaTime, UpdatedComponent->GetComponentRotation(), true, Hit); 28 | } 29 | 30 | // DO NOT call Super::TickComponent - we're handling all movement ourselves 31 | // Calling Super would cause the base class to also process movement, fighting with our commands 32 | // Just call the component base class for housekeeping 33 | UActorComponent::TickComponent(DeltaTime, TickType, ThisTickFunction); 34 | } 35 | 36 | void UAeonixFlyingMovementComponent::RequestDirectMove(const FVector& MoveVelocity, bool bForceMaxSpeed) 37 | { 38 | // Simple direct velocity assignment - no smoothing, no interpolation 39 | Velocity = MoveVelocity; 40 | } 41 | 42 | bool UAeonixFlyingMovementComponent::CanStartPathFollowing() const 43 | { 44 | return true; 45 | } -------------------------------------------------------------------------------- /Source/AeonixGameUtils/Public/Component/AeonixAutopathTargetComponent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Components/ActorComponent.h" 4 | 5 | #include "AeonixAutopathTargetComponent.generated.h" 6 | 7 | class UAeonixAutopathSubsystem; 8 | 9 | /** 10 | * Component that marks an actor as an autopath target. 11 | * Only one target can be registered at a time. 12 | * The component does not tick itself - instead the UAeonixAutopathSubsystem 13 | * tracks the registered target and triggers pathfinding when it moves. 14 | */ 15 | UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent)) 16 | class AEONIXGAMEUTILS_API UAeonixAutopathTargetComponent : public UActorComponent 17 | { 18 | GENERATED_BODY() 19 | 20 | public: 21 | UAeonixAutopathTargetComponent(const FObjectInitializer& ObjectInitializer); 22 | 23 | /** Position threshold in cm - triggers pathfinding when moved beyond this distance */ 24 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix|Autopath", meta = (ClampMin = "0.0")) 25 | float PositionThreshold = 50.0f; 26 | 27 | /** Whether this target is active for autopath tracking */ 28 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix|Autopath") 29 | bool bEnableAutopath = true; 30 | 31 | protected: 32 | virtual void OnRegister() override; 33 | virtual void OnUnregister() override; 34 | virtual void BeginPlay() override; 35 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 36 | 37 | private: 38 | /** Whether we're currently registered with the subsystem */ 39 | bool bRegisteredWithSubsystem = false; 40 | 41 | /** Initialize and register with subsystem */ 42 | void RegisterWithSubsystem(); 43 | 44 | /** Unregister from subsystem */ 45 | void UnregisterFromSubsystem(); 46 | }; 47 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Task/AeonixFindPathAsyncAction.cpp: -------------------------------------------------------------------------------- 1 | #include "Task/AeonixFindPathAsyncAction.h" 2 | #include "Component/AeonixNavAgentComponent.h" 3 | #include "Subsystem/AeonixSubsystem.h" 4 | #include "Engine/World.h" 5 | 6 | UAeonixFindPathAsyncAction* UAeonixFindPathAsyncAction::FindPathAsync( 7 | UObject* WorldContextObject, 8 | UAeonixNavAgentComponent* NavAgentComponent, 9 | FVector TargetLocation) 10 | { 11 | UAeonixFindPathAsyncAction* Action = NewObject(); 12 | Action->NavAgent = NavAgentComponent; 13 | Action->Target = TargetLocation; 14 | 15 | if (WorldContextObject) 16 | { 17 | Action->WorldPtr = WorldContextObject->GetWorld(); 18 | } 19 | 20 | Action->RegisterWithGameInstance(WorldContextObject); 21 | return Action; 22 | } 23 | 24 | void UAeonixFindPathAsyncAction::Activate() 25 | { 26 | if (!NavAgent.IsValid()) 27 | { 28 | OnFailed.Broadcast(); 29 | return; 30 | } 31 | 32 | if (!WorldPtr.IsValid()) 33 | { 34 | OnFailed.Broadcast(); 35 | return; 36 | } 37 | 38 | // Get the Aeonix subsystem 39 | UAeonixSubsystem* Subsystem = WorldPtr->GetSubsystem(); 40 | if (!Subsystem) 41 | { 42 | OnFailed.Broadcast(); 43 | return; 44 | } 45 | 46 | // Request async pathfinding 47 | FAeonixPathFindRequestCompleteDelegate& Delegate = 48 | Subsystem->FindPathAsyncAgent(NavAgent.Get(), Target, ResultPath); 49 | Delegate.BindDynamic(this, &UAeonixFindPathAsyncAction::OnPathFindComplete); 50 | } 51 | 52 | void UAeonixFindPathAsyncAction::OnPathFindComplete(EAeonixPathFindStatus Status) 53 | { 54 | if (Status == EAeonixPathFindStatus::Complete) 55 | { 56 | OnSuccess.Broadcast(ResultPath.GetPathPoints()); 57 | } 58 | else 59 | { 60 | OnFailed.Broadcast(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/EQS/AeonixEQSFloodFillGenerator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "DataProviders/AIDataProvider.h" 5 | #include "EnvironmentQuery/EnvQueryGenerator.h" 6 | #include "EnvironmentQuery/EnvQueryTypes.h" 7 | #include "AeonixEQSFloodFillGenerator.generated.h" 8 | 9 | /** 10 | * EQS Generator that flood fills valid points from a given origin using Aeonix octree navigation data 11 | */ 12 | UCLASS(EditInlineNew, Category = "Aeonix|EQS") 13 | class AEONIXNAVIGATION_API UAeonixEQSFloodFillGenerator : public UEnvQueryGenerator 14 | { 15 | GENERATED_BODY() 16 | public: 17 | UAeonixEQSFloodFillGenerator(const FObjectInitializer& ObjectInitialize); 18 | 19 | /** Maximum distance from origin to flood fill. Points farther than this radius will not be generated. */ 20 | UPROPERTY(EditDefaultsOnly, Category = Generator) 21 | FAIDataProviderFloatValue FloodRadius; 22 | 23 | /** Maximum number of steps to explore during flood fill. Stops when either radius or step limit is reached. */ 24 | UPROPERTY(EditDefaultsOnly, Category = Generator) 25 | FAIDataProviderIntValue FloodStepsMax; 26 | 27 | // Optionally restrict to a specific navigation agent 28 | UPROPERTY(EditDefaultsOnly, Category = Generator) 29 | FAIDataProviderIntValue NavAgentIndex; 30 | 31 | /** Minimum spacing between generated points (0 = no filtering, returns all navigable points) */ 32 | UPROPERTY(EditDefaultsOnly, Category = Generator) 33 | FAIDataProviderFloatValue MinPointSpacing; 34 | 35 | /** context */ 36 | UPROPERTY(EditAnywhere, Category = Generator) 37 | TSubclassOf Context; 38 | 39 | protected: 40 | virtual void GenerateItems(FEnvQueryInstance& QueryInstance) const override; 41 | }; 42 | -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/AeonixBlockedVoxelVisualizer.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | class AAeonixBoundingVolume; 8 | 9 | /** 10 | * Static utility class for visualizing blocked voxels in the octree. 11 | * Uses a grid-based BFS flood fill to find and display blocked leaf sub-voxels. 12 | */ 13 | class AEONIXEDITOR_API FAeonixBlockedVoxelVisualizer 14 | { 15 | public: 16 | /** 17 | * Visualize blocked voxels starting from a point projected forward from the camera. 18 | * @param World The world context 19 | * @param Volume The bounding volume containing the octree data 20 | * @param MaxVoxels Maximum number of blocked voxels to draw 21 | * @param Range Distance in front of camera to project the flood fill center 22 | */ 23 | static void VisualizeBlockedVoxels(UWorld* World, AAeonixBoundingVolume* Volume, int32 MaxVoxels, float Range); 24 | 25 | /** 26 | * Clear the blocked voxel visualization. 27 | * @param World The world context 28 | */ 29 | static void ClearVisualization(UWorld* World); 30 | 31 | /** 32 | * Get the editor camera position. 33 | * @return Position of the active perspective viewport camera 34 | */ 35 | static FVector GetCameraPosition(); 36 | 37 | /** 38 | * Get a position projected forward from the camera. 39 | * @param Range Distance to project forward 40 | * @return Position Range units in front of the active perspective viewport camera 41 | */ 42 | static FVector GetCameraStartPosition(float Range); 43 | 44 | private: 45 | /** 46 | * Check if a world position is inside a blocked leaf sub-voxel. 47 | * @param WorldPos The world position to check 48 | * @param Volume The bounding volume containing the octree data 49 | * @return True if the position is blocked 50 | */ 51 | static bool IsPositionBlocked(const FVector& WorldPos, const AAeonixBoundingVolume* Volume); 52 | }; 53 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/EQS/AeonixEQS3DGridGenerator.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "EnvironmentQuery/EnvQueryGenerator.h" 7 | #include "DataProviders/AIDataProvider.h" 8 | #include "AeonixEQS3DGridGenerator.generated.h" 9 | 10 | /** 11 | * Generates a uniform 3D grid of test points within a spherical radius 12 | * with fixed spacing between points, independent of the Aeonix voxel layout. 13 | * 14 | * This generator provides predictable, uniform point distribution for large levels 15 | * where the flood fill generator's voxel-dependent spacing is inefficient. 16 | * 17 | * Performance: O((2*Radius/Spacing)³) - depends only on spacing, not octree complexity 18 | */ 19 | UCLASS(EditInlineNew, Category = "Aeonix|EQS") 20 | class AEONIXNAVIGATION_API UAeonixEQS3DGridGenerator : public UEnvQueryGenerator 21 | { 22 | GENERATED_BODY() 23 | 24 | public: 25 | UAeonixEQS3DGridGenerator(); 26 | 27 | virtual void GenerateItems(FEnvQueryInstance& QueryInstance) const override; 28 | 29 | virtual FText GetDescriptionTitle() const override; 30 | virtual FText GetDescriptionDetails() const override; 31 | 32 | public: 33 | /** Maximum distance from the origin to generate points (spherical bounds) */ 34 | UPROPERTY(EditDefaultsOnly, Category = Generator) 35 | FAIDataProviderFloatValue GridRadius; 36 | 37 | /** Fixed distance between grid points in all axes */ 38 | UPROPERTY(EditDefaultsOnly, Category = Generator) 39 | FAIDataProviderFloatValue GridSpacing; 40 | 41 | /** If true, only generate points that are in navigable space according to Aeonix navigation data */ 42 | UPROPERTY(EditDefaultsOnly, Category = Generator) 43 | uint8 bOnlyNavigablePoints : 1; 44 | 45 | /** If true, snap generated points to the nearest Aeonix voxel center */ 46 | UPROPERTY(EditDefaultsOnly, Category = Generator) 47 | uint8 bProjectToNavigation : 1; 48 | 49 | /** Context to use as the center point for grid generation */ 50 | UPROPERTY(EditAnywhere, Category = Generator) 51 | TSubclassOf GenerateAround; 52 | }; 53 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Task/AeonixFindPathAsyncAction.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Kismet/BlueprintAsyncActionBase.h" 4 | #include "Data/AeonixTypes.h" 5 | #include "Pathfinding/AeonixNavigationPath.h" 6 | #include "AeonixFindPathAsyncAction.generated.h" 7 | 8 | class UAeonixNavAgentComponent; 9 | 10 | // Output pin delegates 11 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAeonixPathFound, const TArray&, PathPoints); 12 | DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAeonixPathFailed); 13 | 14 | /** 15 | * Latent Blueprint node for async pathfinding. 16 | * Finds a path asynchronously and outputs the path points on completion. 17 | */ 18 | UCLASS() 19 | class AEONIXNAVIGATION_API UAeonixFindPathAsyncAction : public UBlueprintAsyncActionBase 20 | { 21 | GENERATED_BODY() 22 | 23 | public: 24 | /** Called when pathfinding completes successfully */ 25 | UPROPERTY(BlueprintAssignable) 26 | FOnAeonixPathFound OnSuccess; 27 | 28 | /** Called when pathfinding fails */ 29 | UPROPERTY(BlueprintAssignable) 30 | FOnAeonixPathFailed OnFailed; 31 | 32 | /** 33 | * Find a path asynchronously from the agent's current location to the target. 34 | * @param WorldContextObject World context 35 | * @param NavAgentComponent The navigation agent component 36 | * @param TargetLocation The destination location 37 | * @return The async action 38 | */ 39 | UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", WorldContext = "WorldContextObject", DisplayName = "Aeonix Find Path Async"), Category = "Aeonix|Pathfinding") 40 | static UAeonixFindPathAsyncAction* FindPathAsync( 41 | UObject* WorldContextObject, 42 | UAeonixNavAgentComponent* NavAgentComponent, 43 | FVector TargetLocation); 44 | 45 | // UBlueprintAsyncActionBase interface 46 | virtual void Activate() override; 47 | 48 | private: 49 | UFUNCTION() 50 | void OnPathFindComplete(EAeonixPathFindStatus Status); 51 | 52 | // Stored parameters 53 | TWeakObjectPtr NavAgent; 54 | FVector Target; 55 | TWeakObjectPtr WorldPtr; 56 | 57 | // Path storage 58 | FAeonixNavigationPath ResultPath; 59 | }; 60 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/AeonixNavigation.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | using System.IO; 5 | using EpicGames.Core; 6 | 7 | public class AeonixNavigation : ModuleRules 8 | { 9 | public AeonixNavigation(ReadOnlyTargetRules Target) : base(Target) 10 | { 11 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 12 | 13 | bLegacyParentIncludePaths = false; 14 | CppStandard = CppStandardVersion.Default; 15 | 16 | 17 | PublicIncludePaths.AddRange( 18 | new string[] { 19 | Path.Combine(ModuleDirectory, "Public") 20 | // ... add public include paths required here ... 21 | } 22 | ); 23 | 24 | 25 | PrivateIncludePaths.AddRange( 26 | new string[] { 27 | "AeonixNavigation/Private", 28 | // ... add other private include paths required here ... 29 | } 30 | ); 31 | 32 | 33 | PublicDependencyModuleNames.AddRange( 34 | new string[] 35 | { 36 | "Core", 37 | "AIModule", 38 | "NavigationSystem", 39 | "GameplayTasks", 40 | "StateTreeModule", 41 | 42 | // ... add other public dependencies that you statically link with here ... 43 | } 44 | ); 45 | 46 | 47 | PrivateDependencyModuleNames.AddRange( 48 | new string[] 49 | { 50 | "CoreUObject", 51 | "Engine", 52 | "Slate", 53 | "SlateCore", 54 | "DeveloperSettings", 55 | // ... add private dependencies that you statically link with here ... 56 | } 57 | ); 58 | 59 | // Add editor-only dependencies 60 | if (Target.bBuildEditor) 61 | { 62 | PrivateDependencyModuleNames.Add("UnrealEd"); 63 | } 64 | 65 | 66 | DynamicallyLoadedModuleNames.AddRange( 67 | new string[] 68 | { 69 | // ... add any modules that your module loads dynamically here ... 70 | } 71 | ); 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Component/AeonixNavAgentComponent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Pathfinding/AeonixPathFinder.h" 4 | #include "Pathfinding/AeonixNavigationPath.h" 5 | #include "Interface/AeonixSubsystemInterface.h" 6 | 7 | #include "Components/ActorComponent.h" 8 | 9 | #include "AeonixNavAgentComponent.generated.h" 10 | 11 | class AAeonixBoundingVolume; 12 | struct AeonixLink; 13 | 14 | /** 15 | * Component to provide Aeonix Navigation capabilities to an Agent. 16 | * Typically placed on your AI Controller, but can also be on any actor (see debug actor) 17 | */ 18 | UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent)) 19 | class AEONIXNAVIGATION_API UAeonixNavAgentComponent : public UActorComponent 20 | { 21 | GENERATED_BODY() 22 | 23 | public: 24 | UAeonixNavAgentComponent(const FObjectInitializer& ObjectInitializer); 25 | 26 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix") 27 | FAeonixPathFinderSettings PathfinderSettings; 28 | 29 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix") 30 | FVector StartPointOffset = FVector::ZeroVector; 31 | 32 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix") 33 | FVector EndPointOffset = FVector::ZeroVector; 34 | 35 | FAeonixNavigationPath& GetPath() { return CurrentPath; } 36 | const FAeonixNavigationPath& GetPath() const { return CurrentPath; } 37 | FVector GetAgentPosition() const; 38 | FVector GetPathfindingStartPosition() const; 39 | FVector GetPathfindingEndPosition(const FVector& TargetLocation) const; 40 | 41 | // Debug rendering for the current path 42 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Debug") 43 | bool bEnablePathDebugRendering = false; 44 | 45 | UFUNCTION(BlueprintCallable, Category = "Debug") 46 | void RegisterPathForDebugRendering(); 47 | 48 | // Need to make a sane multithreading implementation, this is very crude and will have edge case crashes at present. 49 | // Thread safe counter to track async path requests 50 | 51 | protected: 52 | virtual void BeginPlay() override; 53 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 54 | 55 | protected: 56 | UPROPERTY() 57 | TScriptInterface AeonixSubsystem; 58 | 59 | FAeonixNavigationPath CurrentPath{}; 60 | }; 61 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixTypes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "HAL/ThreadSafeCounter.h" 4 | #include "Data/AeonixThreading.h" 5 | #include "Pathfinding/AeonixNavigationPath.h" 6 | 7 | #include "AeonixTypes.generated.h" 8 | 9 | class AAeonixBoundingVolume; 10 | class UAeonixNavAgentComponent; 11 | 12 | UENUM() 13 | enum class EAeonixPathFindStatus : uint8 14 | { 15 | Idle = 0, 16 | Initialized = 1, 17 | InProgress = 2, 18 | Complete = 3, 19 | Consumed = 4, 20 | Failed = 5, 21 | Cancelled = 6, 22 | Invalidated = 7 23 | }; 24 | 25 | DECLARE_DYNAMIC_DELEGATE_OneParam(FAeonixPathFindRequestCompleteDelegate, EAeonixPathFindStatus, PathFindStatus); 26 | 27 | struct FAeonixPathFindRequest 28 | { 29 | TPromise PathFindPromise; 30 | TFuture PathFindFuture = PathFindPromise.GetFuture(); 31 | 32 | FAeonixPathFindRequestCompleteDelegate OnPathFindRequestComplete{}; 33 | 34 | // Threading enhancements 35 | EAeonixRequestPriority Priority = EAeonixRequestPriority::Normal; 36 | double SubmitTime = 0.0; 37 | std::atomic bCancelled{false}; 38 | std::atomic bAgentInvalidated{false}; // Set by game thread when agent is unregistered 39 | TWeakObjectPtr RequestingAgent; 40 | TMap RegionVersionSnapshot; // For invalidation detection 41 | 42 | // Deferred delivery: workers write to WorkerPath, game thread moves to DestinationPath 43 | FAeonixNavigationPath WorkerPath; 44 | FAeonixNavigationPath* DestinationPath = nullptr; 45 | std::atomic bPathReady{false}; // Set by worker when path is complete 46 | 47 | /** Thread-safe staleness check (NO UObject access - safe from worker threads) */ 48 | bool IsStale() const 49 | { 50 | return bCancelled.load(std::memory_order_acquire) || 51 | bAgentInvalidated.load(std::memory_order_acquire); 52 | } 53 | 54 | /** Comparison operator for priority queue sorting */ 55 | bool operator<(const FAeonixPathFindRequest& Other) const 56 | { 57 | // Higher priority first (lower enum value = higher priority) 58 | if (Priority != Other.Priority) 59 | { 60 | return Priority < Other.Priority; 61 | } 62 | // Within same priority, FIFO (earlier submit time first) 63 | return SubmitTime < Other.SubmitTime; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Library/libmorton/morton_BMI.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #if defined(__BMI2__)// || __AVX2__ 3 | #include 4 | #include 5 | 6 | namespace bmi2_detail { 7 | inline uint32_t pdep(uint32_t source, uint32_t mask) noexcept { 8 | return _pdep_u32(source, mask); 9 | } 10 | inline uint64_t pdep(uint64_t source, uint64_t mask) noexcept { 11 | return _pdep_u64(source, mask); 12 | } 13 | inline uint32_t pext(uint32_t source, uint32_t mask) noexcept { 14 | return _pext_u32(source, mask); 15 | } 16 | inline uint64_t pext(uint64_t source, uint64_t mask) noexcept { 17 | return _pext_u64(source, mask); 18 | } 19 | } // namespace bmi2_detail 20 | 21 | #define BMI_2D_X_MASK 0x5555555555555555 22 | #define BMI_2D_Y_MASK 0xAAAAAAAAAAAAAAAA 23 | 24 | template 25 | inline morton m2D_e_BMI(const coord x, const coord y) { 26 | morton m = 0; 27 | m |= bmi2_detail::pdep(static_cast(x), static_cast(BMI_2D_X_MASK)) 28 | | bmi2_detail::pdep(static_cast(y), static_cast(BMI_2D_Y_MASK)); 29 | return m; 30 | } 31 | 32 | template 33 | inline void m2D_d_BMI(const morton m, coord& x, coord& y) { 34 | x = static_cast(bmi2_detail::pext(m, static_cast(BMI_2D_X_MASK))); 35 | y = static_cast(bmi2_detail::pext(m, static_cast(BMI_2D_Y_MASK))); 36 | } 37 | 38 | #define BMI_3D_X_MASK 0x9249249249249249 39 | #define BMI_3D_Y_MASK 0x2492492492492492 40 | #define BMI_3D_Z_MASK 0x4924924924924924 41 | 42 | template 43 | inline morton m3D_e_BMI(const coord x, const coord y, const coord z){ 44 | morton m = 0; 45 | m |= bmi2_detail::pdep(static_cast(x), static_cast(BMI_3D_X_MASK)) 46 | | bmi2_detail::pdep(static_cast(y), static_cast(BMI_3D_Y_MASK)) 47 | | bmi2_detail::pdep(static_cast(z), static_cast(BMI_3D_Z_MASK)); 48 | return m; 49 | } 50 | 51 | template 52 | inline void m3D_d_BMI(const morton m, coord& x, coord& y, coord& z){ 53 | x = static_cast(bmi2_detail::pext(m, static_cast(BMI_3D_X_MASK))); 54 | y = static_cast(bmi2_detail::pext(m, static_cast(BMI_3D_Y_MASK))); 55 | z = static_cast(bmi2_detail::pext(m, static_cast(BMI_3D_Z_MASK))); 56 | } 57 | #endif 58 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Component/AeonixNavAgentComponent.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "Component/AeonixNavAgentComponent.h" 3 | 4 | #include "Subsystem/AeonixSubsystem.h" 5 | #include "AeonixNavigation.h" 6 | 7 | #include "GameFramework/Actor.h" 8 | #include "DrawDebugHelpers.h" 9 | 10 | UAeonixNavAgentComponent::UAeonixNavAgentComponent(const FObjectInitializer& ObjectInitializer) 11 | { 12 | PrimaryComponentTick.bCanEverTick = false; 13 | } 14 | 15 | void UAeonixNavAgentComponent::BeginPlay() 16 | { 17 | Super::BeginPlay(); 18 | 19 | AeonixSubsystem = GetWorld()->GetSubsystem(); 20 | if (!AeonixSubsystem.GetInterface()) 21 | { 22 | UE_LOG(LogAeonixNavigation, Error, TEXT("No AeonixSubsystem with a valid AeonixInterface found")); 23 | } 24 | else 25 | { 26 | AeonixSubsystem->RegisterNavComponent(this); 27 | } 28 | } 29 | 30 | void UAeonixNavAgentComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) 31 | { 32 | if (!AeonixSubsystem.GetInterface()) 33 | { 34 | UE_LOG(LogAeonixNavigation, Error, TEXT("No AeonixSubsystem with a valid AeonixInterface found")); 35 | } 36 | else 37 | { 38 | AeonixSubsystem->UnRegisterNavComponent(this); 39 | } 40 | 41 | Super::EndPlay(EndPlayReason); 42 | } 43 | 44 | FVector UAeonixNavAgentComponent::GetAgentPosition() const 45 | { 46 | FVector Result; 47 | 48 | AController* Controller = Cast(GetOwner()); 49 | 50 | if (Controller) 51 | { 52 | if (APawn* Pawn = Controller->GetPawn()) 53 | Result = Pawn->GetActorLocation(); 54 | } 55 | else // Maybe this is just on a debug actor, rather than an AI controller 56 | { 57 | Result = GetOwner()->GetActorLocation(); 58 | } 59 | 60 | return Result; 61 | } 62 | 63 | FVector UAeonixNavAgentComponent::GetPathfindingStartPosition() const 64 | { 65 | return GetAgentPosition() + StartPointOffset; 66 | } 67 | 68 | FVector UAeonixNavAgentComponent::GetPathfindingEndPosition(const FVector& TargetLocation) const 69 | { 70 | return TargetLocation + EndPointOffset; 71 | } 72 | 73 | void UAeonixNavAgentComponent::RegisterPathForDebugRendering() 74 | { 75 | if (bEnablePathDebugRendering && CurrentPath.GetPathPoints().Num() > 0) 76 | { 77 | UE_LOG(LogAeonixNavigation, Log, TEXT("NavAgent: Registering path with %d points for debug rendering"), CurrentPath.GetPathPoints().Num()); 78 | CurrentPath.DebugDrawLite(GetWorld(), FColor::Green, 10.0f); 79 | } 80 | } -------------------------------------------------------------------------------- /Source/AeonixEditor/Public/AeonixDebugFloodFillActor.h: -------------------------------------------------------------------------------- 1 | // Copyright Notice 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "GameFramework/Actor.h" 7 | #include "Components/BillboardComponent.h" 8 | #include "AeonixDebugFloodFillActor.generated.h" 9 | 10 | /** 11 | * Debug actor that performs a flood fill visualization of the Aeonix navigation octree. 12 | * Visualizes octree connectivity from its location using debug drawing. 13 | */ 14 | UCLASS() 15 | class AEONIXEDITOR_API AAeonixDebugFloodFillActor : public AActor 16 | { 17 | GENERATED_BODY() 18 | 19 | public: 20 | AAeonixDebugFloodFillActor(); 21 | 22 | /** Billboard component for editor visibility */ 23 | UPROPERTY() 24 | TObjectPtr BillboardComponent; 25 | 26 | // AActor interface 27 | virtual void OnConstruction(const FTransform& Transform) override; 28 | virtual void PostEditMove(bool bFinished) override; 29 | virtual void BeginDestroy() override; 30 | 31 | /** Maximum number of voxels to visit during flood fill */ 32 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix Debug|Flood Fill", meta = (ClampMin = "1", ClampMax = "10000")) 33 | int32 MaxVoxelCount = 1000; 34 | 35 | /** Thickness of the debug connection lines */ 36 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix Debug|Flood Fill", meta = (ClampMin = "0.1", ClampMax = "50.0")) 37 | float LineThickness = 5.0f; 38 | 39 | /** Automatically update flood fill when navigation is regenerated */ 40 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix Debug|Flood Fill") 41 | bool bAutoUpdateOnRegeneration = true; 42 | 43 | /** Manually trigger clearing of the flood fill visualization */ 44 | UFUNCTION(CallInEditor, Category = "Aeonix Debug|Flood Fill") 45 | void ClearVisualization(); 46 | 47 | /** Performs the flood fill algorithm and visualizes the results */ 48 | void PerformFloodFill(); 49 | 50 | // AActor interface 51 | virtual void BeginPlay() override; 52 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 53 | 54 | private: 55 | /** Handler for when a bounding volume regenerates its navigation */ 56 | void OnBoundingVolumeRegenerated(class AAeonixBoundingVolume* Volume); 57 | 58 | /** Bind to all bounding volumes in the level */ 59 | void BindToBoundingVolumes(); 60 | 61 | /** Unbind from all bounding volumes */ 62 | void UnbindFromBoundingVolumes(); 63 | }; 64 | -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/AeonixEditor.cpp: -------------------------------------------------------------------------------- 1 | #include "AeonixEditor/AeonixEditor.h" 2 | #include "AeonixEditor/Private/AeonixVolumeDetails.h" 3 | #include "SAeonixNavigationTreeView.h" 4 | 5 | #include "PropertyEditorModule.h" 6 | #include "WorkspaceMenuStructure.h" 7 | #include "WorkspaceMenuStructureModule.h" 8 | #include "Widgets/Docking/SDockTab.h" 9 | #include "Framework/Docking/TabManager.h" 10 | #include "LevelEditor.h" 11 | #include "ToolMenus.h" 12 | 13 | IMPLEMENT_GAME_MODULE(FAeonixEditorModule, AeonixEditor); 14 | 15 | DEFINE_LOG_CATEGORY(LogAeonixEditor) 16 | 17 | #define LOCTEXT_NAMESPACE "AeonixEditor" 18 | 19 | static const FName AeonixNavigationTreeTabName("AeonixNavigationTree"); 20 | 21 | void FAeonixEditorModule::StartupModule() 22 | { 23 | UE_LOG(LogAeonixEditor, Log, TEXT("AeonixEditorModule: Log Started")); 24 | 25 | FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); 26 | 27 | PropertyModule.RegisterCustomClassLayout("AeonixBoundingVolume", FOnGetDetailCustomizationInstance::CreateStatic(&FAeonixVolumeDetails::MakeInstance)); 28 | 29 | // Register the navigation tree tab 30 | FGlobalTabmanager::Get()->RegisterNomadTabSpawner(AeonixNavigationTreeTabName, FOnSpawnTab::CreateRaw(this, &FAeonixEditorModule::SpawnNavigationTreeTab)) 31 | .SetDisplayName(LOCTEXT("AeonixNavigationTreeTabTitle", "Aeonix")) 32 | .SetTooltipText(LOCTEXT("AeonixNavigationTreeTabTooltip", "View and manage Aeonix navigation elements")) 33 | .SetGroup(WorkspaceMenu::GetMenuStructure().GetLevelEditorCategory()) 34 | .SetIcon(FSlateIcon(FAppStyle::GetAppStyleSetName(), "ClassIcon.Volume")); 35 | 36 | } 37 | 38 | void FAeonixEditorModule::ShutdownModule() 39 | { 40 | UE_LOG(LogAeonixEditor, Log, TEXT("AeonixEditorModule: Log Ended")); 41 | 42 | FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(AeonixNavigationTreeTabName); 43 | 44 | if (FModuleManager::Get().IsModuleLoaded("PropertyEditor")) 45 | { 46 | FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked("PropertyEditor"); 47 | PropertyModule.UnregisterCustomClassLayout("AeonixBoundingVolume"); 48 | } 49 | } 50 | 51 | TSharedRef FAeonixEditorModule::SpawnNavigationTreeTab(const FSpawnTabArgs& SpawnTabArgs) 52 | { 53 | return SNew(SDockTab) 54 | .TabRole(ETabRole::NomadTab) 55 | [ 56 | SNew(SAeonixNavigationTreeView) 57 | ]; 58 | } 59 | 60 | #undef LOCTEXT_NAMESPACE -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Task/StateTreeTask_AeonixMoveTo.cpp: -------------------------------------------------------------------------------- 1 | #include "Task/StateTreeTask_AeonixMoveTo.h" 2 | #include "Task/AITask_AeonixMoveTo.h" 3 | #include "AIController.h" 4 | #include "GameFramework/Actor.h" 5 | #include "NavigationSystem.h" 6 | #include "Subsystem/AeonixSubsystem.h" 7 | #include "Component/AeonixNavAgentComponent.h" 8 | #include "DrawDebugHelpers.h" 9 | #include "StateTreeExecutionContext.h" 10 | 11 | EStateTreeRunStatus FStateTreeTask_AeonixMoveTo::EnterState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const 12 | { 13 | auto& Data = Context.GetInstanceData(*this); 14 | if (!Data.AgentComponent.IsValid()) 15 | { 16 | AActor* OwnerActor = Cast(Context.GetOwner()); 17 | if (OwnerActor) 18 | { 19 | Data.AgentComponent = OwnerActor->FindComponentByClass(); 20 | } 21 | } 22 | if (!Data.AgentComponent.IsValid()) 23 | { 24 | return EStateTreeRunStatus::Failed; 25 | } 26 | 27 | FVector Target = Data.GoalLocation; 28 | if (Data.GoalActor.IsValid()) 29 | { 30 | Target = Data.GoalActor->GetActorLocation(); 31 | } 32 | 33 | AAIController* Controller = Cast(Data.AgentComponent->GetOwner()); 34 | if (!Controller) 35 | { 36 | return EStateTreeRunStatus::Failed; 37 | } 38 | 39 | Data.MoveTask = UAITask_AeonixMoveTo::AeonixAIMoveTo(Controller, Target, false, Data.GoalActor.Get(), Data.AcceptableRadius, EAIOptionFlag::Default, false, false); 40 | if (!Data.MoveTask) 41 | { 42 | return EStateTreeRunStatus::Failed; 43 | } 44 | Data.MoveTask->ReadyForActivation(); 45 | return EStateTreeRunStatus::Running; 46 | } 47 | 48 | EStateTreeRunStatus FStateTreeTask_AeonixMoveTo::Tick(FStateTreeExecutionContext& Context, const float DeltaTime) const 49 | { 50 | auto& Data = Context.GetInstanceData(*this); 51 | if (!Data.MoveTask) 52 | { 53 | return EStateTreeRunStatus::Failed; 54 | } 55 | if (Data.MoveTask->WasMoveSuccessful()) 56 | { 57 | return EStateTreeRunStatus::Succeeded; 58 | } 59 | if (Data.MoveTask->IsFinished()) 60 | { 61 | return EStateTreeRunStatus::Failed; 62 | } 63 | return EStateTreeRunStatus::Running; 64 | } 65 | 66 | void FStateTreeTask_AeonixMoveTo::ExitState(FStateTreeExecutionContext& Context, const FStateTreeTransitionResult& Transition) const 67 | { 68 | auto& Data = Context.GetInstanceData(*this); 69 | if (Data.MoveTask && !Data.MoveTask->IsFinished()) 70 | { 71 | Data.MoveTask->EndTask(); 72 | } 73 | Data.MoveTask = nullptr; 74 | } 75 | -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/AeonixPathDebugActor.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include "AeonixPathDebugActor.h" 3 | 4 | #include "AenoixEditorDebugSubsystem.h" 5 | #include "Component/AeonixNavAgentComponent.h" 6 | #include "Subsystem/AeonixSubsystem.h" 7 | 8 | static const FName NavAgentComponentName(TEXT("AeonixNavAgentComponent")); 9 | static const FName RootComponentName(TEXT("RootComponent")); 10 | 11 | AAeonixPathDebugActor::AAeonixPathDebugActor(const FObjectInitializer& Initializer) 12 | : Super(Initializer) 13 | , DebugType(EAeonixPathDebugActorType::START) 14 | , NavAgentComponent(CreateDefaultSubobject(NavAgentComponentName)) 15 | { 16 | SetRootComponent(CreateDefaultSubobject(RootComponentName)); 17 | } 18 | 19 | void AAeonixPathDebugActor::OnConstruction(const FTransform& Transform) 20 | { 21 | Super::OnConstruction(Transform); 22 | 23 | GetWorld()->GetSubsystem()->RegisterNavComponent(NavAgentComponent); 24 | 25 | GEditor->GetEditorSubsystem()->UpdateDebugActor(this); 26 | } 27 | 28 | void AAeonixPathDebugActor::PostEditMove(bool bFinished) 29 | { 30 | Super::PostEditMove(bFinished); 31 | 32 | GEditor->GetEditorSubsystem()->UpdateDebugActor(this); 33 | } 34 | 35 | void AAeonixPathDebugActor::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) 36 | { 37 | Super::PostEditChangeProperty(PropertyChangedEvent); 38 | 39 | // Only trigger path recalculation for properties that affect pathfinding 40 | if (PropertyChangedEvent.Property && PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(AAeonixPathDebugActor, DebugType)) 41 | { 42 | GEditor->GetEditorSubsystem()->UpdateDebugActor(this); 43 | } 44 | } 45 | 46 | void AAeonixPathDebugActor::BeginDestroy() 47 | { 48 | // Notify debug subsystem to clear references to this actor 49 | if (GEditor && GEditor->GetEditorSubsystem()) 50 | { 51 | GEditor->GetEditorSubsystem()->ClearDebugActor(this); 52 | } 53 | 54 | // Unregister nav component from subsystem 55 | if (NavAgentComponent && GetWorld() && GetWorld()->GetSubsystem()) 56 | { 57 | GetWorld()->GetSubsystem()->UnRegisterNavComponent(NavAgentComponent); 58 | } 59 | 60 | Super::BeginDestroy(); 61 | } 62 | 63 | void AAeonixPathDebugActor::Destroyed() 64 | { 65 | // Additional safety cleanup 66 | if (GEditor && GEditor->GetEditorSubsystem()) 67 | { 68 | GEditor->GetEditorSubsystem()->ClearDebugActor(this); 69 | } 70 | 71 | Super::Destroyed(); 72 | } -------------------------------------------------------------------------------- /Source/AeonixGameUtils/Public/Subsystem/AeonixAutopathSubsystem.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Data/AeonixTypes.h" 4 | #include "Subsystems/WorldSubsystem.h" 5 | 6 | #include "AeonixAutopathSubsystem.generated.h" 7 | 8 | class UAeonixAutopathComponent; 9 | class UAeonixAutopathTargetComponent; 10 | 11 | /** 12 | * Subsystem that manages autopath components, tracking movement and triggering pathfinding. 13 | * When source or target actors move beyond their position threshold, an async pathfind is requested. 14 | */ 15 | UCLASS() 16 | class AEONIXGAMEUTILS_API UAeonixAutopathSubsystem : public UTickableWorldSubsystem 17 | { 18 | GENERATED_BODY() 19 | 20 | public: 21 | // Registration API (called by components) 22 | void RegisterAutopathSource(UAeonixAutopathComponent* Component); 23 | void UnregisterAutopathSource(UAeonixAutopathComponent* Component); 24 | void RegisterAutopathTarget(UAeonixAutopathTargetComponent* Component); 25 | void UnregisterAutopathTarget(UAeonixAutopathTargetComponent* Component); 26 | 27 | // Access to registered components 28 | const TArray& GetRegisteredSources() const { return RegisteredSources; } 29 | UAeonixAutopathTargetComponent* GetRegisteredTarget() const { return RegisteredTarget; } 30 | 31 | // UTickableWorldSubsystem interface 32 | virtual void Initialize(FSubsystemCollectionBase& Collection) override; 33 | virtual void Deinitialize() override; 34 | virtual void Tick(float DeltaTime) override; 35 | virtual TStatId GetStatId() const override; 36 | 37 | virtual bool IsTickable() const override; 38 | virtual bool IsTickableInEditor() const override; 39 | virtual bool IsTickableWhenPaused() const override; 40 | 41 | protected: 42 | virtual bool DoesSupportWorldType(const EWorldType::Type WorldType) const override; 43 | 44 | private: 45 | UPROPERTY(Transient) 46 | TArray RegisteredSources{}; 47 | 48 | /** The single registered target (only one allowed) */ 49 | UPROPERTY(Transient) 50 | UAeonixAutopathTargetComponent* RegisteredTarget = nullptr; 51 | 52 | // Track last positions for threshold comparison 53 | // Not UPROPERTY - we manually clean up stale actor keys in ProcessAutopathSources() 54 | TMap SourceLastPositionMap; 55 | 56 | FVector TargetLastPosition = FVector::ZeroVector; 57 | bool bTargetPositionInitialized = false; 58 | 59 | /** Process all autopath sources and trigger pathfinding as needed */ 60 | void ProcessAutopathSources(); 61 | 62 | /** Check if an actor has moved beyond threshold since last check */ 63 | bool HasMovedBeyondThreshold(AActor* Actor, TMap& LastPositionMap, float Threshold); 64 | 65 | /** Request async pathfinding for a source component */ 66 | void RequestAsyncPathfinding(UAeonixAutopathComponent* Source); 67 | }; 68 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Data/AeonixOctreeData.h" 4 | #include "Data/AeonixGenerationParameters.h" 5 | 6 | #include "AeonixData.generated.h" 7 | 8 | class IAeonixCollisionQueryInterface; 9 | class IAeonixDebugDrawInterface; 10 | struct FAeonixGenerationParamaters; 11 | 12 | USTRUCT() 13 | struct AEONIXNAVIGATION_API FAeonixData 14 | { 15 | GENERATED_BODY() 16 | 17 | // SVO data 18 | UPROPERTY() 19 | FAeonixOctreeData OctreeData; 20 | 21 | public: 22 | void SetExtents(const FVector& Origin, const FVector& Extents); 23 | void SetDebugPosition(const FVector& DebugPosition); 24 | 25 | void ResetForGeneration(); 26 | void UpdateGenerationParameters(const FAeonixGenerationParameters& Params); 27 | const FAeonixGenerationParameters& GetParams() const; 28 | void Generate(UWorld& World, const IAeonixCollisionQueryInterface& CollisionInterface, const IAeonixDebugDrawInterface& DebugInterface); 29 | void RegenerateDynamicRegions(const IAeonixCollisionQueryInterface& CollisionInterface, const IAeonixDebugDrawInterface& DebugInterface); 30 | void RegenerateDynamicRegions(const TSet& RegionIds, const IAeonixCollisionQueryInterface& CollisionInterface, const IAeonixDebugDrawInterface& DebugInterface); 31 | 32 | bool GetLinkPosition(const AeonixLink& aLink, FVector& oPosition) const; 33 | bool GetNodePosition(layerindex_t aLayer, mortoncode_t aCode, FVector& oPosition) const; 34 | float GetVoxelSize(layerindex_t aLayer) const; 35 | 36 | //~ Begin UObject 37 | //void Serialize(FArchive& Ar) override; 38 | //~ End UObject 39 | 40 | private: 41 | FAeonixGenerationParameters GenerationParameters; 42 | int32 GetNumNodesInLayer(layerindex_t aLayer) const; 43 | int32 GetNumNodesPerSide(layerindex_t aLayer) const; 44 | 45 | bool IsBlocked(const FVector& aPosition, const float aSize) const; 46 | bool IsInDebugRange(const FVector& aPosition) const; 47 | bool IsAnyMemberBlocked(layerindex_t aLayer, mortoncode_t aCode) const; 48 | bool GetIndexForCode(layerindex_t aLayer, mortoncode_t aCode, nodeindex_t& oIndex) const; 49 | 50 | void BuildNeighbourLinks(layerindex_t aLayer, const IAeonixDebugDrawInterface& DebugInterface); 51 | bool FindLinkInDirection(layerindex_t aLayer, const nodeindex_t aNodeIndex, uint8 aDir, AeonixLink& oLinkToUpdate, FVector& aStartPosForDebug, const IAeonixDebugDrawInterface& DebugInterface); 52 | 53 | void RasterizeLeafNode(FVector& aOrigin, nodeindex_t aLeafIndex, const IAeonixCollisionQueryInterface& CollisionInterface, const IAeonixDebugDrawInterface& DebugInterface); 54 | void RasteriseLayer(layerindex_t aLayer, const IAeonixCollisionQueryInterface& CollisionInterface, const IAeonixDebugDrawInterface& DebugInterface); 55 | 56 | void FirstPassRasterise(const IAeonixCollisionQueryInterface& CollisionInterface); 57 | }; 58 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Actor/AeonixModifierVolume.h: -------------------------------------------------------------------------------- 1 | // Copyright Chris Kang. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "GameFramework/Volume.h" 7 | #include "AeonixModifierVolume.generated.h" 8 | 9 | /** 10 | * Modifier type flags for AeonixModifierVolume 11 | */ 12 | UENUM(BlueprintType, meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = "true")) 13 | enum class EAeonixModifierType : uint8 14 | { 15 | None = 0, 16 | DebugFilter = 1 << 0 UMETA(DisplayName = "Debug Filter", ToolTip = "Only leaf voxels inside this volume will be debug rendered"), 17 | DynamicRegion = 1 << 1 UMETA(DisplayName = "Dynamic Region", ToolTip = "Voxels in this region can be updated at runtime without full regeneration"), 18 | // Future modifiers can be added here 19 | }; 20 | ENUM_CLASS_FLAGS(EAeonixModifierType) 21 | 22 | /** 23 | * Volume that modifies Aeonix navigation behavior within its bounds. 24 | * Supports multiple modifier types through flags: 25 | * - DebugFilter: Filters leaf voxel debug rendering to only show voxels inside this volume 26 | * - DynamicRegion: Marks voxels for runtime updates without full regeneration 27 | * 28 | * Multiple modifier volumes can be used per bounding volume. 29 | * Modifier volumes should be placed before generation for proper leaf node allocation. 30 | */ 31 | UCLASS(hidecategories = (Tags, Cooking, Actor, HLOD, Mobile, LOD)) 32 | class AEONIXNAVIGATION_API AAeonixModifierVolume : public AVolume 33 | { 34 | GENERATED_BODY() 35 | 36 | public: 37 | AAeonixModifierVolume(const FObjectInitializer& ObjectInitializer); 38 | 39 | //~ Begin UObject Interface 40 | virtual void PostLoad() override; 41 | //~ End UObject Interface 42 | 43 | //~ Begin AActor Interface 44 | virtual void BeginPlay() override; 45 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 46 | virtual void OnConstruction(const FTransform& Transform) override; 47 | virtual void Destroyed() override; 48 | #if WITH_EDITOR 49 | virtual void PostEditMove(bool bFinished) override; 50 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 51 | #endif 52 | //~ End AActor Interface 53 | 54 | /** Modifier types active in this volume */ 55 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix", meta = (Bitmask, BitmaskEnum = "/Script/AeonixNavigation.EAeonixModifierType")) 56 | int32 ModifierTypes = static_cast(EAeonixModifierType::None); 57 | 58 | /** Unique ID for this dynamic region (used for selective regeneration) */ 59 | UPROPERTY() 60 | FGuid DynamicRegionId; 61 | 62 | private: 63 | /** Register this volume with bounding volumes it's inside */ 64 | void RegisterWithBoundingVolumes(); 65 | 66 | /** Unregister this volume from bounding volumes */ 67 | void UnregisterFromBoundingVolumes(); 68 | }; 69 | -------------------------------------------------------------------------------- /Source/AeonixGameUtils/Private/Component/AeonixAutopathTargetComponent.cpp: -------------------------------------------------------------------------------- 1 | #include "Component/AeonixAutopathTargetComponent.h" 2 | #include "Subsystem/AeonixAutopathSubsystem.h" 3 | #include "AeonixNavigation.h" 4 | 5 | #include "GameFramework/Actor.h" 6 | 7 | UAeonixAutopathTargetComponent::UAeonixAutopathTargetComponent(const FObjectInitializer& ObjectInitializer) 8 | : Super(ObjectInitializer) 9 | { 10 | // Component does not tick - subsystem handles all autopath ticking 11 | PrimaryComponentTick.bCanEverTick = false; 12 | } 13 | 14 | void UAeonixAutopathTargetComponent::OnRegister() 15 | { 16 | Super::OnRegister(); 17 | 18 | // Register with subsystem (works in editor and at runtime) 19 | RegisterWithSubsystem(); 20 | } 21 | 22 | void UAeonixAutopathTargetComponent::OnUnregister() 23 | { 24 | // Unregister from subsystem 25 | UnregisterFromSubsystem(); 26 | 27 | Super::OnUnregister(); 28 | } 29 | 30 | void UAeonixAutopathTargetComponent::BeginPlay() 31 | { 32 | Super::BeginPlay(); 33 | 34 | // Reset state from editor world - PIE creates new world with new actor instances 35 | bRegisteredWithSubsystem = false; 36 | 37 | RegisterWithSubsystem(); 38 | } 39 | 40 | void UAeonixAutopathTargetComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) 41 | { 42 | // Unregister from subsystem 43 | UnregisterFromSubsystem(); 44 | 45 | Super::EndPlay(EndPlayReason); 46 | } 47 | 48 | void UAeonixAutopathTargetComponent::RegisterWithSubsystem() 49 | { 50 | if (bRegisteredWithSubsystem) 51 | { 52 | return; // Already registered 53 | } 54 | 55 | // Get reference to the Autopath subsystem 56 | if (UWorld* World = GetWorld()) 57 | { 58 | UAeonixAutopathSubsystem* Subsystem = World->GetSubsystem(); 59 | if (!Subsystem) 60 | { 61 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("AutopathTarget %s: No AeonixAutopathSubsystem found"), *GetName()); 62 | return; 63 | } 64 | 65 | // Register with the subsystem 66 | Subsystem->RegisterAutopathTarget(this); 67 | bRegisteredWithSubsystem = true; 68 | 69 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("AutopathTarget %s: Registered with subsystem"), *GetName()); 70 | } 71 | } 72 | 73 | void UAeonixAutopathTargetComponent::UnregisterFromSubsystem() 74 | { 75 | if (!bRegisteredWithSubsystem) 76 | { 77 | return; // Not registered 78 | } 79 | 80 | // Unregister from subsystem 81 | UWorld* World = GetWorld(); 82 | if (!World) 83 | { 84 | bRegisteredWithSubsystem = false; 85 | return; 86 | } 87 | 88 | UAeonixAutopathSubsystem* Subsystem = World->GetSubsystem(); 89 | if (!Subsystem) 90 | { 91 | bRegisteredWithSubsystem = false; 92 | return; 93 | } 94 | 95 | Subsystem->UnregisterAutopathTarget(this); 96 | bRegisteredWithSubsystem = false; 97 | 98 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("AutopathTarget %s: Unregistered from subsystem"), *GetName()); 99 | } 100 | -------------------------------------------------------------------------------- /Source/AeonixNavigationTests/Private/AeonixNavigationBasicTest.cpp: -------------------------------------------------------------------------------- 1 | #include "Data/AeonixData.h" 2 | #include "Engine/World.h" 3 | #include "Engine/EngineTypes.h" 4 | #include "Interface/AeonixCollisionQueryInterface.h" 5 | #include "Interface/AeonixDebugDrawInterface.h" 6 | #include "Misc/AutomationTest.h" 7 | 8 | // Mock implementation of IAeonixCollisionQueryInterface 9 | class FMockCollisionQueryInterface : public IAeonixCollisionQueryInterface 10 | { 11 | public: 12 | virtual bool IsBlocked(const FVector& Position, const float VoxelSize, ECollisionChannel CollisionChannel, const float AgentRadius) const override 13 | { 14 | // For testing, always return false (not blocked) 15 | return false; 16 | } 17 | 18 | virtual bool IsLeafBlocked(const FVector& Position, const float LeafSize, ECollisionChannel CollisionChannel, const float AgentRadius) const override 19 | { 20 | // For testing, always return false (not blocked) 21 | return false; 22 | } 23 | }; 24 | 25 | // Mock implementation of IAeonixDebugDrawInterface 26 | class FMockDebugDrawInterface : public IAeonixDebugDrawInterface 27 | { 28 | public: 29 | virtual void AeonixDrawDebugString(const FVector& Position, const FString& String, const FColor& Color) const override {} 30 | virtual void AeonixDrawDebugBox(const FVector& Position, const float Size, const FColor& Color) const override {} 31 | virtual void AeonixDrawDebugLine(const FVector& Start, const FVector& End, const FColor& Color, float Thickness = 0.0f) const override {} 32 | virtual void AeonixDrawDebugDirectionalArrow(const FVector& Start, const FVector& End, const FColor& Color, float ArrowSize = 0.0f) const override {} 33 | }; 34 | 35 | IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAeonixNavigation_GenerateDataTest, "AeonixNavigation.GenerateData.Basic", EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) 36 | 37 | bool FAeonixNavigation_GenerateDataTest::RunTest(const FString& Parameters) 38 | { 39 | // Arrange 40 | FMockCollisionQueryInterface MockCollision; 41 | FMockDebugDrawInterface MockDebug; 42 | FAeonixData NavData; 43 | 44 | // Use dummy world (nullptr) for now; in a more advanced test, you can mock or spawn a test world 45 | UWorld* DummyWorld = nullptr; 46 | 47 | // Setup minimal generation parameters 48 | FAeonixGenerationParameters Params; 49 | Params.Origin = FVector::ZeroVector; 50 | Params.Extents = FVector(1000, 1000, 1000); 51 | Params.OctreeDepth = 3; 52 | Params.CollisionChannel = ECollisionChannel::ECC_WorldStatic; 53 | Params.AgentRadius = 34.f; 54 | NavData.UpdateGenerationParameters(Params); 55 | 56 | // Act & Assert: Should not crash 57 | // Only call Generate if DummyWorld is valid (in a real test, you'd want a valid world) 58 | bool bGenerateCalled = false; 59 | NavData.Generate(*DummyWorld, MockCollision, MockDebug); 60 | bGenerateCalled = true; 61 | 62 | 63 | TestTrue(TEXT("Test ran without crashing (Generate not called if world is null)"), true); 64 | return true; 65 | } 66 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Component/AeonixPathFollowingComponent.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "Navigation/PathFollowingComponent.h" 6 | #include "CoreMinimal.h" 7 | #include "AeonixPathFollowingComponent.generated.h" 8 | 9 | struct FAeonixNavigationPath; 10 | struct FAeonixPathPoint; 11 | 12 | USTRUCT(BlueprintType) 13 | struct AEONIXNAVIGATION_API FAeonixFlightSettings 14 | { 15 | GENERATED_BODY() 16 | 17 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Flight") 18 | float MaxSpeed = 1200.0f; 19 | 20 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Flight") 21 | float TurnRate = 180.0f; 22 | 23 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Flight") 24 | float AcceptanceRadius = 100.0f; 25 | }; 26 | 27 | UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) 28 | class AEONIXNAVIGATION_API UAeonixPathFollowingComponent : public UPathFollowingComponent 29 | { 30 | GENERATED_BODY() 31 | 32 | public: 33 | UAeonixPathFollowingComponent(); 34 | virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override; 35 | 36 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix Flight") 37 | FAeonixFlightSettings FlightSettings; 38 | 39 | protected: 40 | virtual void BeginPlay() override; 41 | 42 | // Override path following methods 43 | virtual void FollowPathSegment(float DeltaTime) override; 44 | virtual void UpdatePathSegment() override; 45 | virtual void SetMoveSegment(int32 SegmentStartIndex) override; 46 | virtual bool HasReached(const FVector& TestPoint, float AcceptanceRadiusOverride = DefaultAcceptanceRadius, bool bExactSpot = false) const; 47 | 48 | // Shadow base class inline method to return Aeonix path target (can't override FORCEINLINE) 49 | FORCEINLINE FVector GetCurrentTargetLocation() const { return GetTargetLocation(); } 50 | 51 | // Aeonix-specific path following 52 | virtual void FollowAeonixPath(); 53 | virtual FVector GetTargetLocation() const; 54 | virtual void UpdateMovement(float DeltaTime); 55 | virtual void UpdateRotation(float DeltaTime); 56 | 57 | private: 58 | // Current path state 59 | int32 CurrentWaypointIndex; 60 | FVector LastVelocity; 61 | 62 | // Cached path reference 63 | const FAeonixNavigationPath* CurrentAeonixPath; 64 | 65 | // Prevent double-processing per frame 66 | bool bProcessingMovementThisFrame; 67 | uint64 LastProcessedFrameNumber; 68 | 69 | // Initialization retry system 70 | bool bInitializationComplete; 71 | float InitializationRetryTimer; 72 | int32 InitializationRetryCount; 73 | static constexpr float INITIALIZATION_RETRY_INTERVAL = 0.5f; 74 | static constexpr int32 MAX_INITIALIZATION_RETRIES = 10; 75 | 76 | // Helper functions 77 | const FAeonixPathPoint* GetCurrentWaypoint() const; 78 | const FAeonixPathPoint* GetNextWaypoint() const; 79 | bool IsValidWaypointIndex(int32 Index) const; 80 | void AdvanceToNextWaypoint(); 81 | 82 | // Initialization helpers 83 | void TryInitializeNavigation(); 84 | bool IsValidForNavigation() const; 85 | }; -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/AeonixBatchTestActor.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Runtime/Engine/Classes/GameFramework/Actor.h" 5 | #include "../Public/AeonixPerformanceTypes.h" 6 | #include "AeonixNavigation/Public/Pathfinding/AeonixNavigationPath.h" 7 | #include "AeonixBatchTestActor.generated.h" 8 | 9 | class AAeonixBoundingVolume; 10 | class UAeonixNavAgentComponent; 11 | class UAeonixSubsystem; 12 | 13 | UCLASS() 14 | class AEONIXEDITOR_API AAeonixBatchTestActor : public AActor 15 | { 16 | GENERATED_BODY() 17 | 18 | public: 19 | AAeonixBatchTestActor(); 20 | 21 | virtual void BeginPlay() override; 22 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 23 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 24 | 25 | #if WITH_EDITOR 26 | virtual void PostEditMove(bool bFinished) override; 27 | virtual void OnConstruction(const FTransform& Transform) override; 28 | #endif 29 | 30 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix Navigation") 31 | FAeonixPerformanceTestSettings TestSettings; 32 | 33 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Aeonix Navigation") 34 | EAeonixPerformanceTestStatus CurrentStatus = EAeonixPerformanceTestStatus::NotStarted; 35 | 36 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Aeonix Navigation") 37 | FAeonixPerformanceTestSummary LastTestSummary; 38 | 39 | UFUNCTION(BlueprintCallable, Category = "Aeonix Navigation") 40 | void StartBatchTest(); 41 | 42 | UFUNCTION(BlueprintCallable, Category = "Aeonix Navigation") 43 | void CancelBatchTest(); 44 | 45 | UFUNCTION(BlueprintCallable, Category = "Aeonix Navigation") 46 | void ClearResults(); 47 | 48 | UFUNCTION(CallInEditor, Category = "Aeonix Navigation") 49 | void RunTestInEditor(); 50 | 51 | UFUNCTION(CallInEditor, Category = "Aeonix Navigation") 52 | void ClearResultsInEditor(); 53 | 54 | protected: 55 | UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Aeonix Navigation") 56 | TObjectPtr NavAgentComponent; 57 | 58 | UPROPERTY(Transient) 59 | FRandomStream RandomStream; 60 | 61 | UPROPERTY(Transient) 62 | int32 CurrentTestIndex = 0; 63 | 64 | UPROPERTY(Transient) 65 | float TestStartTime = 0.0f; 66 | 67 | UPROPERTY(Transient) 68 | TArray CurrentResults; 69 | 70 | UPROPERTY(Transient) 71 | TObjectPtr AeonixSubsystem; 72 | 73 | virtual void RunSynchronousTests(); 74 | 75 | virtual bool GenerateRandomNavigablePoint(const AAeonixBoundingVolume* Volume, FVector& OutPoint); 76 | virtual bool GenerateRandomEndPoint(FVector& OutEnd); 77 | 78 | virtual void ExecuteSingleTest(const FVector& Start, const FVector& End, FAeonixPerformanceTestResult& OutResult); 79 | 80 | virtual void OnTestCompleted(); 81 | 82 | virtual void VisualizeTestResults(); 83 | virtual void ClearVisualization(); 84 | 85 | private: 86 | TArray VisualizationPoints; 87 | TArray VisualizationPaths; 88 | TArray> FailedPathVisualizationPoints; 89 | AAeonixBoundingVolume* GetTargetVolume() const; 90 | TArray GetAllVolumes() const; 91 | void InitializeSubsystemIfNeeded(); 92 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Interface/AeonixSubsystemInterface.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Data/AeonixTypes.h" 4 | 5 | #include "UObject/Interface.h" 6 | 7 | #include "AeonixSubsystemInterface.generated.h" 8 | 9 | class AAeonixModifierVolume; 10 | class AAeonixBoundingVolume; 11 | class UAeonixNavAgentComponent; 12 | class UAeonixDynamicObstacleComponent; 13 | struct FAeonixNavigationPath; 14 | 15 | /** Delegate broadcast when navigation regeneration completes (full or dynamic regions) */ 16 | DECLARE_MULTICAST_DELEGATE_OneParam(FOnNavigationRegenCompleted, AAeonixBoundingVolume*); 17 | 18 | /** Delegate broadcast when registration changes (volumes, modifiers, or obstacles added/removed) */ 19 | DECLARE_MULTICAST_DELEGATE(FOnRegistrationChanged); 20 | 21 | UINTERFACE(MinimalAPI, NotBlueprintable) 22 | class UAeonixSubsystemInterface : public UInterface 23 | { 24 | GENERATED_BODY() 25 | }; 26 | 27 | /** 28 | * Interface for interacting with the main navigation subsystem 29 | */ 30 | class AEONIXNAVIGATION_API IAeonixSubsystemInterface 31 | { 32 | GENERATED_BODY() 33 | 34 | public: 35 | UFUNCTION() 36 | virtual void RegisterVolume(AAeonixBoundingVolume* Volume) = 0; 37 | UFUNCTION() 38 | virtual void UnRegisterVolume(AAeonixBoundingVolume* Volume) = 0; 39 | UFUNCTION() 40 | virtual void RegisterModifierVolume(AAeonixModifierVolume* ModifierVolume) = 0; 41 | UFUNCTION() 42 | virtual void UnRegisterModifierVolume(AAeonixModifierVolume* ModifierVolume) = 0; 43 | UFUNCTION() 44 | virtual void RegisterNavComponent(UAeonixNavAgentComponent* NavComponent) = 0; 45 | UFUNCTION() 46 | virtual void UnRegisterNavComponent(UAeonixNavAgentComponent* NavComponent) = 0; 47 | UFUNCTION() 48 | virtual void RegisterDynamicObstacle(UAeonixDynamicObstacleComponent* ObstacleComponent) = 0; 49 | UFUNCTION() 50 | virtual void UnRegisterDynamicObstacle(UAeonixDynamicObstacleComponent* ObstacleComponent) = 0; 51 | UFUNCTION() 52 | virtual const AAeonixBoundingVolume* GetVolumeForPosition(const FVector& Position) = 0; 53 | 54 | UFUNCTION() 55 | virtual const AAeonixBoundingVolume* GetVolumeForAgent(const UAeonixNavAgentComponent* NavigationComponent) = 0; 56 | UFUNCTION() 57 | virtual AAeonixBoundingVolume* GetMutableVolumeForAgent(const UAeonixNavAgentComponent* NavigationComponent) = 0; 58 | 59 | UFUNCTION(BlueprintCallable, Category="Aeonix") 60 | virtual FAeonixPathFindRequestCompleteDelegate& FindPathAsyncAgent(UAeonixNavAgentComponent* NavAgentComponent, const FVector& End, FAeonixNavigationPath& OutPath) = 0; 61 | 62 | UFUNCTION(BlueprintCallable, Category="Aeonix") 63 | virtual bool FindPathImmediateAgent(UAeonixNavAgentComponent* NavigationComponent, const FVector& End, FAeonixNavigationPath& OutPath) = 0; 64 | 65 | UFUNCTION() 66 | virtual void UpdateComponents() = 0; 67 | 68 | /** Get delegate for navigation regeneration completion notifications */ 69 | virtual FOnNavigationRegenCompleted& GetOnNavigationRegenCompleted() = 0; 70 | 71 | /** Get delegate for registration change notifications */ 72 | virtual FOnRegistrationChanged& GetOnRegistrationChanged() = 0; 73 | 74 | /** Request debug path update for a specific nav agent component */ 75 | virtual void RequestDebugPathUpdate(UAeonixNavAgentComponent* NavComponent) = 0; 76 | 77 | }; 78 | -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/AenoixEditorDebugSubsystem.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include "Pathfinding/AeonixNavigationPath.h" 5 | 6 | #include "EditorSubsystem.h" 7 | 8 | #include "AenoixEditorDebugSubsystem.generated.h" 9 | 10 | USTRUCT() 11 | struct FAeonixFailedPath 12 | { 13 | GENERATED_BODY() 14 | 15 | UPROPERTY() 16 | FVector StartPoint; 17 | 18 | UPROPERTY() 19 | FVector EndPoint; 20 | 21 | FAeonixFailedPath() {} 22 | FAeonixFailedPath(const FVector& InStart, const FVector& InEnd) 23 | : StartPoint(InStart), EndPoint(InEnd) {} 24 | }; 25 | 26 | /** 27 | * A subsystem that provides debug functionality for the Aenoix Editor. 28 | * 29 | * @see UEditorSubsystem 30 | */ 31 | UCLASS() 32 | class AEONIXEDITOR_API UAenoixEditorDebugSubsystem : public UEditorSubsystem, public FTickableGameObject 33 | { 34 | GENERATED_BODY() 35 | 36 | UPROPERTY(Transient) 37 | TSoftObjectPtr StartDebugActor{nullptr}; 38 | UPROPERTY(Transient) 39 | TSoftObjectPtr EndDebugActor{nullptr}; 40 | UPROPERTY(Transient) 41 | FAeonixNavigationPath CurrentDebugPath{}; 42 | UPROPERTY(Transient) 43 | FAeonixNavigationPath CachedDebugPath{}; 44 | UPROPERTY(Transient) 45 | TSoftObjectPtr CurrentDebugVolume{nullptr}; 46 | 47 | bool bIsPathPending{false}; 48 | bool bHasValidCachedPath{false}; 49 | bool bNeedsRedraw{true}; 50 | bool bBatchPathsNeedRedraw{false}; 51 | bool bFailedPathsNeedRedraw{false}; 52 | 53 | // Batch run paths for visualization 54 | UPROPERTY(Transient) 55 | TArray BatchRunPaths; 56 | 57 | // Failed batch run paths for visualization 58 | UPROPERTY(Transient) 59 | TArray FailedBatchRunPaths; 60 | 61 | // Mutex to protect path data. Prevent writing to path while debug drawing it! 62 | FCriticalSection PathMutex; 63 | 64 | // Delegate subscription methods for automatic path update on navigation regeneration 65 | void BindToBoundingVolumes(); 66 | void UnbindFromBoundingVolumes(); 67 | void OnBoundingVolumeRegenerated(AAeonixBoundingVolume* Volume); 68 | 69 | public: 70 | UFUNCTION(BlueprintCallable, Category="Aeonix") 71 | void UpdateDebugActor(AAeonixPathDebugActor* DebugActor); 72 | 73 | UFUNCTION(BlueprintCallable, Category="Aeonix") 74 | void ClearDebugActor(AAeonixPathDebugActor* ActorToRemove); 75 | 76 | UFUNCTION() 77 | void OnPathFindComplete(EAeonixPathFindStatus Status); 78 | 79 | UFUNCTION(BlueprintCallable, Category="Aeonix") 80 | void SetBatchRunPaths(const TArray& Paths); 81 | 82 | UFUNCTION(BlueprintCallable, Category="Aeonix") 83 | void ClearBatchRunPaths(); 84 | 85 | void SetFailedBatchRunPaths(const TArray>& FailedPaths); 86 | 87 | UFUNCTION(BlueprintCallable, Category="Aeonix") 88 | void ClearFailedBatchRunPaths(); 89 | 90 | UFUNCTION(BlueprintCallable, Category="Aeonix") 91 | void ClearCachedPath(); 92 | 93 | virtual void Tick(float DeltaTime) override; 94 | virtual TStatId GetStatId() const override; 95 | virtual bool IsTickable() const override; 96 | virtual bool IsTickableInEditor() const override; 97 | virtual bool IsTickableWhenPaused() const override; 98 | }; 99 | 100 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Settings/AeonixSettings.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Engine/DeveloperSettings.h" 7 | #include "AeonixSettings.generated.h" 8 | 9 | /** 10 | * Aeonix Navigation Plugin Settings 11 | */ 12 | UCLASS(config = Game, defaultconfig, meta = (DisplayName = "Aeonix Navigation")) 13 | class AEONIXNAVIGATION_API UAeonixSettings : public UDeveloperSettings 14 | { 15 | GENERATED_BODY() 16 | 17 | public: 18 | UAeonixSettings(); 19 | 20 | // Override to place settings in the correct category 21 | virtual FName GetCategoryName() const override { return FName(TEXT("Plugins")); } 22 | 23 | //~ Pathfinding Settings 24 | 25 | /** 26 | * Number of worker threads for async pathfinding operations. 27 | * Recommended: 2-8 threads depending on CPU core count. 28 | */ 29 | UPROPERTY(config, EditAnywhere, Category = "Pathfinding", meta = (ClampMin = "1", ClampMax = "16", UIMin = "1", UIMax = "8")) 30 | int32 PathfindingWorkerThreads = 2; 31 | 32 | /** 33 | * Maximum number of concurrent pathfinding requests allowed in the queue. 34 | * Prevents memory issues when overwhelmed with pathfinding requests. 35 | */ 36 | UPROPERTY(config, EditAnywhere, Category = "Pathfinding", meta = (ClampMin = "4", ClampMax = "200", UIMin = "8", UIMax = "100", 37 | Tooltip = "Maximum pending pathfinding requests. Higher values allow more buffering but use more memory.")) 38 | int32 MaxConcurrentPathfinds = 8; 39 | 40 | //~ Dynamic Regeneration Settings 41 | 42 | /** 43 | * Time budget per frame for applying dynamic regeneration results (milliseconds). 44 | * Lower values spread work across more frames, reducing spikes but increasing total time. 45 | */ 46 | UPROPERTY(config, EditAnywhere, Category = "Dynamic Regeneration", meta = (ClampMin = "1.0", ClampMax = "50.0", UIMin = "1.0", UIMax = "20.0")) 47 | float DynamicRegenTimeBudgetMs = 5.0f; 48 | 49 | /** 50 | * Number of leaves to process in each async chunk (for physics lock management). 51 | * Higher values = fewer lock acquisitions but longer hold times. 52 | */ 53 | UPROPERTY(config, EditAnywhere, Category = "Dynamic Regeneration", meta = (ClampMin = "10", ClampMax = "500", UIMin = "25", UIMax = "200")) 54 | int32 AsyncChunkSize = 75; 55 | 56 | /** 57 | * Minimum time between dynamic region regenerations (seconds). 58 | * Prevents excessive regeneration when obstacles move frequently. 59 | */ 60 | UPROPERTY(config, EditAnywhere, Category = "Dynamic Regeneration", meta = (ClampMin = "0.0", ClampMax = "5.0", UIMin = "0.1", UIMax = "2.0")) 61 | float DynamicRegenCooldown = 0.5f; 62 | 63 | /** 64 | * Delay after marking a region dirty before processing it at runtime (seconds). 65 | * Allows physics to settle before regenerating navigation. 66 | */ 67 | UPROPERTY(config, EditAnywhere, Category = "Dynamic Regeneration", meta = (ClampMin = "0.0", ClampMax = "2.0", UIMin = "0.0", UIMax = "1.0")) 68 | float DirtyRegionProcessDelay = 0.25f; 69 | 70 | /** 71 | * Delay after marking a region dirty before processing it in editor (seconds). 72 | * Longer than runtime delay to allow for editor overhead. 73 | */ 74 | UPROPERTY(config, EditAnywhere, Category = "Dynamic Regeneration", meta = (ClampMin = "0.0", ClampMax = "10.0", UIMin = "0.5", UIMax = "5.0")) 75 | float EditorDirtyRegionProcessDelay = 1.0f; 76 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixGenerationParameters.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "UObject/ObjectMacros.h" 4 | 5 | #include "AeonixGenerationParameters.generated.h" 6 | 7 | UENUM(BlueprintType) 8 | enum class ESVOGenerationStrategy : uint8 9 | { 10 | UseBaked UMETA(DisplayName = "Use Baked"), 11 | GenerateOnBeginPlay UMETA(DisplayName = "Generate OnBeginPlay") 12 | }; 13 | 14 | USTRUCT(BlueprintType) 15 | struct AEONIXNAVIGATION_API FAeonixGenerationParameters 16 | { 17 | GENERATED_BODY() 18 | 19 | // Debug Parameters 20 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation") 21 | float DebugDistance{5000.f}; 22 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation") 23 | bool ShowVoxels{false}; 24 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation") 25 | bool ShowLeafVoxels{false}; 26 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation") 27 | bool ShowMortonCodes{false}; 28 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation") 29 | bool ShowNeighbourLinks{false}; 30 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation") 31 | bool ShowParentChildLinks{false}; 32 | 33 | // Generation parameters 34 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation", meta = (ToolTip = "Controls octree subdivision depth. Higher values create more voxels for finer detail but much longer generation and pathfinding. Creates OctreeDepth+1 hierarchical layers. Layer 0 has the smallest voxels.")) 35 | int32 OctreeDepth{3}; 36 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation") 37 | TEnumAsByte CollisionChannel{ECollisionChannel::ECC_MAX}; 38 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation") 39 | float AgentRadius = 0.f; 40 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "SVO Navigation") 41 | ESVOGenerationStrategy GenerationStrategy = ESVOGenerationStrategy::UseBaked; 42 | 43 | // Transient data used during generation 44 | FVector Origin{FVector::ZeroVector}; 45 | FVector Extents{FVector::ZeroVector}; 46 | FVector DebugPosition{FVector::ZeroVector}; 47 | FBox DebugFilterBox{ForceInit}; 48 | bool bUseDebugFilterBox{false}; 49 | 50 | // Dynamic region support - voxels in these regions get pre-allocated leaf nodes for runtime updates 51 | // Key = unique GUID for each region, Value = bounding box 52 | // This data is now serialized with the bounding volume to persist dynamic regions across level loads 53 | UPROPERTY() 54 | TMap DynamicRegionBoxes; 55 | 56 | /** Add a dynamic region with a unique ID */ 57 | void AddDynamicRegion(const FGuid& RegionId, const FBox& RegionBox) 58 | { 59 | DynamicRegionBoxes.Add(RegionId, RegionBox); 60 | } 61 | 62 | /** Remove a dynamic region by ID */ 63 | void RemoveDynamicRegion(const FGuid& RegionId) 64 | { 65 | DynamicRegionBoxes.Remove(RegionId); 66 | } 67 | 68 | /** Get a dynamic region by ID (returns nullptr if not found) */ 69 | const FBox* GetDynamicRegion(const FGuid& RegionId) const 70 | { 71 | return DynamicRegionBoxes.Find(RegionId); 72 | } 73 | 74 | /** Get all region IDs */ 75 | TArray GetAllRegionIds() const 76 | { 77 | TArray RegionIds; 78 | DynamicRegionBoxes.GetKeys(RegionIds); 79 | return RegionIds; 80 | } 81 | 82 | /** Get the total number of hierarchical layers in the octree (OctreeDepth + 1) */ 83 | int32 GetNumLayers() const 84 | { 85 | return OctreeDepth + 1; 86 | } 87 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Component/AeonixDynamicObstacleComponent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Interface/AeonixSubsystemInterface.h" 4 | #include "Components/ActorComponent.h" 5 | #include "Actor/AeonixBoundingVolume.h" 6 | 7 | #include "AeonixDynamicObstacleComponent.generated.h" 8 | 9 | /** 10 | * Component that tracks dynamic obstacles and triggers navigation regeneration 11 | * when the obstacle moves significantly or crosses dynamic region boundaries. 12 | * 13 | * The component does not tick itself - instead the UAeonixSubsystem ticks all 14 | * registered obstacles and checks for transform changes each frame. 15 | */ 16 | UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent)) 17 | class AEONIXNAVIGATION_API UAeonixDynamicObstacleComponent : public UActorComponent 18 | { 19 | GENERATED_BODY() 20 | 21 | public: 22 | UAeonixDynamicObstacleComponent(const FObjectInitializer& ObjectInitializer); 23 | 24 | /** Position threshold in cm - triggers regeneration when moved beyond this distance */ 25 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix|Obstacle", meta = (ClampMin = "0.0")) 26 | float PositionThreshold = 50.0f; 27 | 28 | /** Rotation threshold in degrees - triggers regeneration when rotated beyond this angle */ 29 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix|Obstacle", meta = (ClampMin = "0.0", ClampMax = "180.0")) 30 | float RotationThreshold = 15.0f; 31 | 32 | /** Whether this obstacle should trigger navigation regeneration (can be disabled temporarily) */ 33 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix|Obstacle") 34 | bool bEnableNavigationRegen = true; 35 | 36 | /** 37 | * Manually trigger navigation regeneration for all regions this obstacle is currently inside. 38 | * Bypasses position/rotation thresholds. 39 | */ 40 | UFUNCTION(BlueprintCallable, CallInEditor, Category = "Aeonix|Obstacle") 41 | void TriggerNavigationRegen(); 42 | 43 | /** 44 | * Get the set of dynamic region IDs this obstacle is currently inside. 45 | */ 46 | const TSet& GetCurrentRegionIds() const { return CurrentDynamicRegionIds; } 47 | 48 | /** 49 | * Set the current region IDs (called by subsystem). 50 | */ 51 | void SetCurrentRegionIds(const TSet& NewRegionIds) { CurrentDynamicRegionIds = NewRegionIds; } 52 | 53 | /** 54 | * Get the bounding volume this obstacle is currently inside (can be null). 55 | */ 56 | AAeonixBoundingVolume* GetCurrentBoundingVolume() const { return CurrentBoundingVolume.Get(); } 57 | 58 | /** 59 | * Set the current bounding volume (called by subsystem). 60 | */ 61 | void SetCurrentBoundingVolume(AAeonixBoundingVolume* Volume) { CurrentBoundingVolume = Volume; } 62 | 63 | protected: 64 | virtual void OnRegister() override; 65 | virtual void OnUnregister() override; 66 | virtual void BeginPlay() override; 67 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 68 | 69 | #if WITH_EDITOR 70 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 71 | #endif 72 | 73 | private: 74 | /** Whether we're currently registered with the subsystem */ 75 | bool bRegisteredWithSubsystem = false; 76 | 77 | /** Initialize and register with subsystem */ 78 | void RegisterWithSubsystem(); 79 | 80 | /** Unregister from subsystem */ 81 | void UnregisterFromSubsystem(); 82 | 83 | /** Set of dynamic region IDs this obstacle is currently inside */ 84 | TSet CurrentDynamicRegionIds; 85 | 86 | /** Bounding volume this obstacle is currently inside */ 87 | TWeakObjectPtr CurrentBoundingVolume; 88 | }; 89 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Library/libmorton/README.md: -------------------------------------------------------------------------------- 1 | # Libmorton 2 | [![Build Status](https://travis-ci.org/Forceflow/libmorton.svg?branch=master)](https://travis-ci.org/Forceflow/libmorton) [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://opensource.org/licenses/MIT) 3 | 4 | * Libmorton is a **C++ header-only library** with methods to efficiently encode/decode 64, 32 and 16-bit Morton codes and coordinates, in 2D and 3D. *Morton order* is also known as *Z-order* or *[the Z-order curve](https://en.wikipedia.org/wiki/Z-order_curve)*. 5 | * Libmorton is a **lightweight and portable** library - in its most basic form it only depends on standard C++ headers. Architecture-specific optimizations are implemented incrementally. 6 | * This library is under active development. SHIT WILL BREAK. 7 | * More info and some benchmarks in these blogposts: [*Morton encoding*](http://www.forceflow.be/2013/10/07/morton-encodingdecoding-through-bit-interleaving-implementations/), [*Libmorton*](http://www.forceflow.be/2016/01/18/libmorton-a-library-for-morton-order-encoding-decoding/) and [*BMI2 instruction set*](http://www.forceflow.be/2016/11/25/using-the-bmi2-instruction-set-to-encode-decode-morton-codes/) 8 | 9 | ## Usage 10 | Just include *libmorton/morton.h*. This will always have functions that point to the most efficient way to encode/decode Morton codes. If you want to test out alternative (and possibly slower) methods, you can find them in *libmorton/morton2D.h* and *libmorton/morton3D.h*. 11 | 12 |
13 | // ENCODING 2D / 3D morton codes, of length 32 and 64 bits
14 | inline uint_fast32_t morton2D_32_encode(const uint_fast16_t x, const uint_fast16_t y);
15 | inline uint_fast64_t morton2D_64_encode(const uint_fast32_t x, const uint_fast32_t y);
16 | inline uint_fast32_t morton3D_32_encode(const uint_fast16_t x, const uint_fast16_t y, const uint_fast16_t z);
17 | inline uint_fast64_t morton3D_64_encode(const uint_fast32_t x, const uint_fast32_t y, const uint_fast32_t z);
18 | // DECODING 2D / 3D morton codes, of length 32 and 64 bits
19 | inline void morton2D_32_decode(const uint_fast32_t morton, uint_fast16_t& x, uint_fast16_t& y);
20 | inline void morton2D_64_decode(const uint_fast64_t morton, uint_fast32_t& x, uint_fast32_t& y);
21 | inline void morton3D_32_decode(const uint_fast32_t morton, uint_fast16_t& x, uint_fast16_t& y, uint_fast16_t& z);
22 | inline void morton3D_64_decode(const uint_fast64_t morton, uint_fast32_t& x, uint_fast32_t& y, uint_fast32_t& z);
23 | 
24 | 25 | If you want to take advantage of the BMI2 instruction set (only available on Intel Haswell processors and newer), make sure `__BMI2__` is defined before you include `morton.h`. 26 | 27 | ## Testing 28 | The *test* folder contains tools I use to test correctness and performance of the libmorton implementation. This section is under heavy re-writing, but might contain some useful code for advanced usage. 29 | 30 | ## Citation 31 | If you use libmorton in your paper or work, please reference it, for example as follows: 32 |
33 | @Misc{libmorton18,
34 | author = "Jeroen Baert",
35 | title = "Libmorton: C++ Morton Encoding/Decoding Library",
36 | howpublished = "\url{https://github.com/Forceflow/libmorton}",
37 | year = "2018"}
38 | 
39 | 40 | ## Thanks 41 | * To [@gnzlbg](https://github.com/gnzlbg) and his Rust implementation [bitwise](https://github.com/gnzlbg) for finding bugs in the Magicbits code 42 | * Everyone making comments and suggestions on the [original blogpost](http://www.forceflow.be/2013/10/07/morton-encodingdecoding-through-bit-interleaving-implementations/) 43 | 44 | ## TODO 45 | * Write better test suite (with L1/L2 trashing, better tests, ...) 46 | 47 | * A better naming system for the functions, because m3D_e_sLUT_shifted? That escalated quickly. 48 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixAsyncRegen.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Data/AeonixTypes.h" 7 | #include "Data/AeonixDefines.h" 8 | #include "Data/AeonixGenerationParameters.h" 9 | 10 | // Forward declarations 11 | class AAeonixBoundingVolume; 12 | class IAeonixCollisionQueryInterface; 13 | class FPhysScene_Chaos; 14 | 15 | /** 16 | * Data structure for async dynamic region regeneration batch 17 | */ 18 | struct FAeonixAsyncRegenBatch 19 | { 20 | /** Leaf node indices that need to be processed */ 21 | TArray LeafIndicesToProcess; 22 | 23 | /** Leaf node coordinates for processing */ 24 | TArray LeafCoordinates; 25 | 26 | /** Leaf origins (corner positions) for rasterization */ 27 | TArray LeafOrigins; 28 | 29 | /** Pointer to physics scene for collision queries */ 30 | FPhysScene_Chaos* PhysicsScenePtr = nullptr; 31 | 32 | /** Generation parameters (collision channel, agent radius, voxel power, etc.) */ 33 | FAeonixGenerationParameters GenParams; 34 | 35 | /** Weak pointer to the volume being regenerated */ 36 | TWeakObjectPtr VolumePtr; 37 | 38 | /** Region IDs to process (empty set means all regions) */ 39 | TSet RegionIdsToProcess; 40 | 41 | /** Chunk size for lock management (number of leaves to process before releasing lock) */ 42 | int32 ChunkSize = 75; 43 | 44 | FAeonixAsyncRegenBatch() = default; 45 | }; 46 | 47 | /** 48 | * Result of a single leaf node rasterization 49 | */ 50 | struct FAeonixLeafRasterResult 51 | { 52 | /** Morton code index of the leaf */ 53 | mortoncode_t LeafIndex; 54 | 55 | /** Index into the LeafNodes array */ 56 | nodeindex_t LeafNodeArrayIndex; 57 | 58 | /** 64-bit voxel bitmask for this leaf */ 59 | uint64 VoxelBitmask; 60 | 61 | FAeonixLeafRasterResult() 62 | : LeafIndex(0) 63 | , LeafNodeArrayIndex(0) 64 | , VoxelBitmask(0) 65 | {} 66 | 67 | FAeonixLeafRasterResult(mortoncode_t InLeafIndex, nodeindex_t InLeafNodeArrayIndex, uint64 InVoxelBitmask) 68 | : LeafIndex(InLeafIndex) 69 | , LeafNodeArrayIndex(InLeafNodeArrayIndex) 70 | , VoxelBitmask(InVoxelBitmask) 71 | {} 72 | }; 73 | 74 | /** 75 | * Namespace for async dynamic region regeneration functions 76 | */ 77 | namespace AeonixAsyncRegen 78 | { 79 | /** 80 | * Execute async dynamic region regeneration on background thread with chunked physics scene locking 81 | * @param Batch The batch data containing all information needed for regeneration 82 | */ 83 | void ExecuteAsyncRegen(const FAeonixAsyncRegenBatch& Batch); 84 | 85 | /** 86 | * Process a single chunk of leaves with physics scene read lock held 87 | * @param Batch The batch data 88 | * @param ChunkStart Starting index in the LeafIndicesToProcess array 89 | * @param ChunkEnd Ending index (exclusive) 90 | * @param OutResults Array to append results to 91 | */ 92 | void ProcessLeafChunk( 93 | const FAeonixAsyncRegenBatch& Batch, 94 | int32 ChunkStart, 95 | int32 ChunkEnd, 96 | TArray& OutResults); 97 | 98 | /** 99 | * Rasterize a single leaf node with two-pass optimization 100 | * @param LeafOrigin Corner position of the leaf 101 | * @param LeafIndex Morton code of the leaf 102 | * @param LeafNodeArrayIndex Index into the LeafNodes array 103 | * @param GenParams Generation parameters 104 | * @param CollisionInterface Collision query interface 105 | * @return Voxel bitmask for this leaf (0 if all clear) 106 | */ 107 | uint64 RasterizeLeafNodeAsync( 108 | const FVector& LeafOrigin, 109 | mortoncode_t LeafIndex, 110 | nodeindex_t LeafNodeArrayIndex, 111 | const FAeonixGenerationParameters& GenParams, 112 | const IAeonixCollisionQueryInterface& CollisionInterface); 113 | } 114 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Pathfinding/AeonixPathfindBenchmark.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "Data/AeonixLink.h" 5 | 6 | struct FAeonixData; 7 | struct FAeonixPathFinderSettings; 8 | 9 | /** 10 | * Result data for a single pathfinding benchmark run 11 | */ 12 | struct AEONIXNAVIGATION_API FAeonixPathfindBenchmarkResult 13 | { 14 | /** Number of A* iterations used */ 15 | int32 Iterations = 0; 16 | 17 | /** Time elapsed in seconds */ 18 | double TimeSeconds = 0.0; 19 | 20 | /** Whether the path was found successfully */ 21 | bool bSuccess = false; 22 | 23 | /** Total path length (0 if path not found) */ 24 | float PathLength = 0.0f; 25 | 26 | /** Start position used for this run */ 27 | FVector StartPos = FVector::ZeroVector; 28 | 29 | /** End position used for this run */ 30 | FVector EndPos = FVector::ZeroVector; 31 | 32 | /** Direct distance between start and end */ 33 | float DirectDistance = 0.0f; 34 | }; 35 | 36 | /** 37 | * Summary statistics for a complete benchmark run 38 | */ 39 | struct AEONIXNAVIGATION_API FAeonixPathfindBenchmarkSummary 40 | { 41 | /** Random seed used for this benchmark */ 42 | int32 Seed = 0; 43 | 44 | /** Total number of pathfind attempts */ 45 | int32 TotalRuns = 0; 46 | 47 | /** Number of successful pathfinds */ 48 | int32 SuccessfulRuns = 0; 49 | 50 | /** Number of failed pathfinds */ 51 | int32 FailedRuns = 0; 52 | 53 | // Iteration statistics (for successful runs only) 54 | double AvgIterations = 0.0; 55 | int32 MinIterations = 0; 56 | int32 MaxIterations = 0; 57 | double StdDevIterations = 0.0; 58 | 59 | // Time statistics in milliseconds (for successful runs only) 60 | double AvgTimeMs = 0.0; 61 | double MinTimeMs = 0.0; 62 | double MaxTimeMs = 0.0; 63 | double StdDevTimeMs = 0.0; 64 | double TotalTimeMs = 0.0; 65 | 66 | // Path length statistics (for successful runs only) 67 | float AvgPathLength = 0.0f; 68 | float MinPathLength = 0.0f; 69 | float MaxPathLength = 0.0f; 70 | 71 | // Distance statistics 72 | float AvgDirectDistance = 0.0f; 73 | 74 | /** All individual results */ 75 | TArray Results; 76 | 77 | /** Get success rate as percentage */ 78 | float GetSuccessRate() const 79 | { 80 | return TotalRuns > 0 ? (SuccessfulRuns * 100.0f / TotalRuns) : 0.0f; 81 | } 82 | 83 | /** Log summary to output */ 84 | void LogSummary() const; 85 | }; 86 | 87 | /** 88 | * Benchmark runner for pathfinding performance testing 89 | * 90 | * Usage: 91 | * FAeonixPathfindBenchmark Benchmark; 92 | * FAeonixPathfindBenchmarkSummary Summary = Benchmark.RunBenchmark( 93 | * 12345, // Seed for reproducibility 94 | * 100, // Number of runs 95 | * NavData, // Navigation data 96 | * PathSettings // Pathfinder settings 97 | * ); 98 | * Summary.LogSummary(); 99 | */ 100 | class AEONIXNAVIGATION_API FAeonixPathfindBenchmark 101 | { 102 | public: 103 | /** 104 | * Run the benchmark with specified parameters 105 | * 106 | * @param Seed Random seed for deterministic position selection 107 | * @param NumRuns Number of pathfinding attempts to perform 108 | * @param NavData Navigation data to use for pathfinding 109 | * @param PathSettings Settings for the pathfinder 110 | * @return Summary of benchmark results 111 | */ 112 | FAeonixPathfindBenchmarkSummary RunBenchmark( 113 | int32 Seed, 114 | int32 NumRuns, 115 | FAeonixData& NavData, 116 | const FAeonixPathFinderSettings& PathSettings 117 | ); 118 | 119 | private: 120 | /** 121 | * Collect all navigable node links from the navigation data 122 | * Only includes nodes that can be used as pathfinding targets 123 | */ 124 | void CollectNavigableNodes(FAeonixData& NavData, TArray& OutNodes); 125 | 126 | /** 127 | * Calculate summary statistics from individual results 128 | */ 129 | void CalculateSummary(FAeonixPathfindBenchmarkSummary& Summary); 130 | }; 131 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Library/libmorton/morton_LUT_generators.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "morton2D.h" 4 | #include "morton3D.h" 5 | #include 6 | 7 | template 8 | void printTable(const element* table, size_t howmany, unsigned int splitat){ 9 | for (size_t i = 0; i < howmany; i++){ 10 | if (i % splitat == 0){ cout << endl; } 11 | printf("%u ,", static_cast(table[i])); 12 | } 13 | cout << endl; 14 | } 15 | 16 | void generate2D_EncodeLUT(size_t how_many_bits, uint_fast16_t*& x_table, uint_fast16_t*& y_table, bool print_tables){ 17 | size_t total = 1 << how_many_bits; 18 | x_table = (uint_fast16_t*)malloc(total * sizeof(uint_fast16_t)); 19 | y_table = (uint_fast16_t*)malloc(total * sizeof(uint_fast16_t)); 20 | 21 | for (uint_fast32_t i = 0; i < total; i++){ 22 | x_table[i] = (uint_fast16_t) m2D_e_magicbits(i, 0); 23 | y_table[i] = (uint_fast16_t)m2D_e_magicbits(0, i); 24 | } 25 | 26 | if (print_tables){ 27 | cout << "X Table " << endl; 28 | printTable(x_table, total, 8); 29 | cout << "Y Table " << endl; 30 | printTable(y_table, total, 8); 31 | } 32 | } 33 | 34 | void generate2D_DecodeLUT(size_t how_many_bits, uint_fast8_t*& x_table, uint_fast8_t*& y_table, bool print_tables){ 35 | size_t total = 1 << how_many_bits; 36 | x_table = (uint_fast8_t*)malloc(total * sizeof(uint_fast8_t)); 37 | y_table = (uint_fast8_t*)malloc(total * sizeof(uint_fast8_t)); 38 | 39 | //generate tables 40 | for (size_t i = 0; i < total; i++) { 41 | m2D_d_for(i, x_table[i], y_table[i]); 42 | } 43 | 44 | if (print_tables) { 45 | cout << "X Table " << endl; 46 | printTable(x_table, total, 16); 47 | cout << "Y Table " << endl; 48 | printTable(y_table, total, 16); 49 | } 50 | } 51 | 52 | void generate3D_EncodeLUT(size_t how_many_bits, uint_fast32_t*& x_table, uint_fast32_t*& y_table, uint_fast32_t*& z_table, bool print_tables){ 53 | // how many items 54 | size_t total = 1 << how_many_bits; 55 | x_table = (uint_fast32_t*)malloc(total * sizeof(uint_fast32_t)); 56 | y_table = (uint_fast32_t*)malloc(total * sizeof(uint_fast32_t)); 57 | z_table = (uint_fast32_t*)malloc(total * sizeof(uint_fast32_t)); 58 | 59 | for (uint_fast32_t i = 0; i < total; i++){ 60 | x_table[i] = (uint_fast32_t) m3D_e_magicbits(i, 0, 0); 61 | y_table[i] = (uint_fast32_t) m3D_e_magicbits(0, i, 0); 62 | z_table[i] = (uint_fast32_t) m3D_e_magicbits(0, 0, i); 63 | } 64 | 65 | if (print_tables){ 66 | cout << "X Table " << endl; 67 | printTable(x_table, total, 8); 68 | cout << "Y Table " << endl; 69 | printTable(y_table, total, 8); 70 | cout << "Z Table " << endl; 71 | printTable(z_table, total, 8); 72 | } 73 | } 74 | 75 | // Generate a decode tables for 3D morton code 76 | // how_many_bits should be a multiple of three, to ensure you can shift results together 77 | void generate3D_DecodeLUT(size_t how_many_bits, uint_fast8_t*& x_table, uint_fast8_t*& y_table, uint_fast8_t*& z_table, bool print_tables){ 78 | size_t total = 1 << how_many_bits; 79 | x_table = (uint_fast8_t*) malloc(total * sizeof(uint_fast8_t)); 80 | y_table = (uint_fast8_t*) malloc(total * sizeof(uint_fast8_t)); 81 | z_table = (uint_fast8_t*) malloc(total * sizeof(uint_fast8_t)); 82 | 83 | //generate tables 84 | for (size_t i = 0; i < total; i++){ 85 | x_table[i] = morton3D_GetThirdBits(i); 86 | y_table[i] = morton3D_GetThirdBits(i >> 1); 87 | z_table[i] = morton3D_GetThirdBits(i >> 2); 88 | } 89 | 90 | if (print_tables){ 91 | cout << "X Table " << endl; 92 | printTable(x_table, total, 16); 93 | cout << "Y Table " << endl; 94 | printTable(y_table, total, 16); 95 | cout << "Z Table " << endl; 96 | printTable(z_table, total, 16); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Actor/AeonixModifierVolume.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Chris Kang. All Rights Reserved. 2 | 3 | #include "Actor/AeonixModifierVolume.h" 4 | #include "Actor/AeonixBoundingVolume.h" 5 | #include "Subsystem/AeonixSubsystem.h" 6 | #include "Components/BrushComponent.h" 7 | #include "AeonixNavigation.h" 8 | 9 | AAeonixModifierVolume::AAeonixModifierVolume(const FObjectInitializer& ObjectInitializer) 10 | : Super(ObjectInitializer) 11 | { 12 | GetBrushComponent()->Mobility = EComponentMobility::Static; 13 | GetBrushComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision); 14 | BrushColor = FColor::Cyan; 15 | bColored = true; 16 | 17 | // Don't generate GUID in constructor - wait for serialization to load it first 18 | // GUID generation happens in PostLoad (for loaded actors) or OnConstruction (for new actors) 19 | } 20 | 21 | void AAeonixModifierVolume::PostLoad() 22 | { 23 | Super::PostLoad(); 24 | 25 | // Generate GUID only if not loaded from serialization 26 | if (!DynamicRegionId.IsValid()) 27 | { 28 | DynamicRegionId = FGuid::NewGuid(); 29 | UE_LOG(LogAeonixNavigation, Warning, TEXT("ModifierVolume %s: Generated NEW GUID in PostLoad %s"), 30 | *GetName(), *DynamicRegionId.ToString()); 31 | } 32 | else 33 | { 34 | UE_LOG(LogAeonixNavigation, Log, TEXT("ModifierVolume %s: Loaded serialized GUID %s"), 35 | *GetName(), *DynamicRegionId.ToString()); 36 | } 37 | } 38 | 39 | void AAeonixModifierVolume::OnConstruction(const FTransform& Transform) 40 | { 41 | Super::OnConstruction(Transform); 42 | 43 | // Generate GUID for newly placed actors (not loaded from disk) 44 | // Loaded actors get their GUID in PostLoad 45 | if (!DynamicRegionId.IsValid()) 46 | { 47 | DynamicRegionId = FGuid::NewGuid(); 48 | UE_LOG(LogAeonixNavigation, Log, TEXT("ModifierVolume %s: Generated NEW GUID in OnConstruction %s"), 49 | *GetName(), *DynamicRegionId.ToString()); 50 | } 51 | 52 | RegisterWithBoundingVolumes(); 53 | } 54 | 55 | void AAeonixModifierVolume::BeginPlay() 56 | { 57 | Super::BeginPlay(); 58 | RegisterWithBoundingVolumes(); 59 | } 60 | 61 | void AAeonixModifierVolume::EndPlay(const EEndPlayReason::Type EndPlayReason) 62 | { 63 | UnregisterFromBoundingVolumes(); 64 | Super::EndPlay(EndPlayReason); 65 | } 66 | 67 | void AAeonixModifierVolume::Destroyed() 68 | { 69 | UnregisterFromBoundingVolumes(); 70 | Super::Destroyed(); 71 | } 72 | 73 | #if WITH_EDITOR 74 | void AAeonixModifierVolume::PostEditMove(bool bFinished) 75 | { 76 | Super::PostEditMove(bFinished); 77 | 78 | if (bFinished) 79 | { 80 | // Re-register when movement is finished 81 | UnregisterFromBoundingVolumes(); 82 | RegisterWithBoundingVolumes(); 83 | } 84 | } 85 | 86 | void AAeonixModifierVolume::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) 87 | { 88 | Super::PostEditChangeProperty(PropertyChangedEvent); 89 | 90 | // Re-register when properties change (like brush being resized) 91 | UnregisterFromBoundingVolumes(); 92 | RegisterWithBoundingVolumes(); 93 | } 94 | #endif 95 | 96 | void AAeonixModifierVolume::RegisterWithBoundingVolumes() 97 | { 98 | if (UWorld* World = GetWorld()) 99 | { 100 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("ModifierVolume %s: Registering with subsystem - ModifierTypes=%d"), 101 | *GetName(), ModifierTypes); 102 | 103 | // Simply register with the subsystem - it will handle spatial relationships 104 | if (UAeonixSubsystem* Subsystem = World->GetSubsystem()) 105 | { 106 | Subsystem->RegisterModifierVolume(this); 107 | } 108 | } 109 | } 110 | 111 | void AAeonixModifierVolume::UnregisterFromBoundingVolumes() 112 | { 113 | if (UWorld* World = GetWorld()) 114 | { 115 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("ModifierVolume %s: Unregistering from subsystem"), 116 | *GetName()); 117 | 118 | // Simply unregister from the subsystem - it will handle cleanup 119 | if (UAeonixSubsystem* Subsystem = World->GetSubsystem()) 120 | { 121 | Subsystem->UnRegisterModifierVolume(this); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aeonix Navigation 2 | 3 | A voxel-based 3D pathfinding plugin for Unreal Engine 5.5, 5.6, and 5.7. 4 | 5 | ![GitHub last commit](https://img.shields.io/github/last-commit/midgen/AeonixNavigation?style=for-the-badge&logo=unrealengine)![GitHub branch status](https://img.shields.io/github/checks-status/midgen/AeonixNavigation/main?style=for-the-badge&logo=unrealengine) 6 | 7 | Build validation via TeamCity CI/CD using the [Aeonix Demo](https://github.com/midgen/AeonixDemo) project. 8 | 9 | ## What is it? 10 | 11 | Aeonix Navigation provides 3d navigation generation and pathfinding for Unreal Engine 5.6+ 12 | 13 | Main Features: 14 | - Sparse Voxel Octree for excelent memory efficient in sparse environment 15 | - A* pathfinding with various heuristics for optimising queries 16 | - A number of post-processing functions for smooth paths 17 | - *EXPERIMENTAL* Dynamic modifier regions that provide runtime updates of selected regions with no re-allocation of navigation data 18 | - Editor tools for visualization and debugging 19 | 20 | ## Installation 21 | 22 | 1. Place the plugin in your project's `Plugins/AeonixNavigation` directory 23 | 2. Regenerate project files and build 24 | 3. Enable the plugin in the Plugins menu 25 | 26 | ## Getting Started 27 | 28 | ### 1. Create Navigation Volume 29 | 30 | - Place an **AeonixBoundingVolume** in your level (Place Actors → Aeonix Bounding Volume) 31 | - Scale it to encompass your desired navigable space 32 | - Select it and configure in the Details panel: 33 | - **Octree Depth**: Controls octree subdivision depth. Higher values create finer detail. Use 5-6 for human-sized characters 34 | - **CollisionChannel**: Which geometry blocks navigation (usually ECC_WorldStatic) 35 | - **GenerationStrategy**: "Use Baked" (pre-generate) or "Generate OnBeginPlay" (runtime) 36 | - (Optional) Check 'Debug Leaf Voxels' to render blocked voxels (may need to increase the debug render distance) 37 | - Click **Generate** button to create navigation data 38 | 39 | ### 2. Add Component to AI 40 | 41 | - Open your AI Controller or Character Blueprint 42 | - Add **AeonixNavAgentComponent** 43 | - Configure pathfinder settings 44 | 45 | ### 3. Use in Behavior Tree 46 | 47 | - Add **BTTask_AeonixMoveTo** task to your Behavior Tree 48 | - Set the **BlackboardKey** to your target location (Vector or Actor) 49 | - Configure **AcceptableRadius** (how close AI needs to get, typically 100-200cm) 50 | 51 | ### 4. Testing 52 | 53 | Use **AAeonixPathDebugActor** (editor-only) to test pathfinding: 54 | - Place two AeonixPathDebugActors in your level 55 | - Set one to START, one to END 56 | - Move them around to visualize paths in real-time 57 | 58 | ## Editor Tools 59 | 60 | **Aeonix Navigation Panel**: Window → Aeonix Navigation 61 | - View and manage all navigation volumes in the level 62 | - Access volume settings and regeneration controls 63 | 64 | **Bounding Volume Details**: Custom details panel when selecting a volume 65 | - Quick access to generation parameters 66 | - Generate button for creating/updating navigation 67 | - Visualization toggles for debugging 68 | 69 | ## Module Structure 70 | 71 | - **AeonixNavigation**: Core runtime module (pathfinding, octree, subsystem) 72 | - **AeonixEditor**: Editor integration (tools, visualization, debug actors) 73 | - **AeonixNavigationTests**: Unit and integration tests 74 | 75 | ## Key Classes 76 | 77 | - `AAeonixBoundingVolume`: Defines navigable 3D space 78 | - `UAeonixNavAgentComponent`: Enables AI to use navigation 79 | - `UAeonixSubsystem`: World subsystem managing navigation and pathfinding 80 | - `AeonixPathFinder`: A* pathfinding implementation 81 | - `UBTTask_AeonixMoveTo`: Behavior Tree movement task 82 | - `AAeonixPathDebugActor`: Editor debug actor for testing 83 | 84 | ## Documentation 85 | 86 | More indepth documentation to come. Please feel free to raise issues on github. 87 | 88 | ## Testing 89 | 90 | Unit tests are in `Source/AeonixNavigationTests`. Example levels are in [AeonixDemo](https://github.com/midgen/AeonixDemo) project. 91 | 92 | ## Credits 93 | 94 | Based on [UESVON](https://github.com/midgen/uesvon) by midgen (me). 95 | 96 | ## License 97 | 98 | See LICENSE file. 99 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Library/libmorton/morton.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // This file will always contain #define clauses to the fastest Morton encoding/decoding implementation 4 | // IF you just want to use the fastest method to encode/decode morton codes, include this header. 5 | 6 | #include "morton2D.h" 7 | #include "morton3D.h" 8 | #include "morton_BMI.h" 9 | 10 | //// ENCODE 11 | //inline uint_fast32_t morton2D_32_encode(const uint_fast16_t x, const uint_fast16_t y); 12 | //inline uint_fast64_t morton2D_64_encode(const uint_fast32_t x, const uint_fast32_t y); 13 | //inline uint_fast32_t morton3D_32_encode(const uint_fast16_t x, const uint_fast16_t y, const uint_fast16_t z); 14 | //inline uint_fast64_t morton3D_64_encode(const uint_fast32_t x, const uint_fast32_t y, const uint_fast32_t z); 15 | // 16 | //// DECODE 17 | //inline void morton2D_32_decode(const uint_fast32_t morton, uint_fast16_t& x, uint_fast16_t& y); 18 | //inline void morton2D_64_decode(const uint_fast64_t morton, uint_fast32_t& x, uint_fast32_t& y); 19 | //inline void morton3D_32_decode(const uint_fast32_t morton, uint_fast16_t& x, uint_fast16_t& y, uint_fast16_t& z); 20 | //inline void morton3D_64_decode(const uint_fast64_t morton, uint_fast32_t& x, uint_fast32_t& y, uint_fast32_t& z); 21 | 22 | // Functions under this are stubs which will always point to fastest implementation at the moment 23 | //----------------------------------------------------------------------------------------------- 24 | 25 | // ENCODING 26 | #if defined(__BMI2__)// || __AVX2__ 27 | inline uint_fast32_t morton2D_32_encode(const uint_fast16_t x, const uint_fast16_t y) { 28 | return m2D_e_BMI(x, y); 29 | } 30 | inline uint_fast64_t morton2D_64_encode(const uint_fast32_t x, const uint_fast32_t y) { 31 | return m2D_e_BMI(x, y); 32 | } 33 | inline uint_fast32_t morton3D_32_encode(const uint_fast16_t x, const uint_fast16_t y, const uint_fast16_t z) { 34 | return m3D_e_BMI(x, y, z); 35 | } 36 | inline uint_fast64_t morton3D_64_encode(const uint_fast32_t x, const uint_fast32_t y, const uint_fast32_t z) { 37 | return m3D_e_BMI(x, y, z); 38 | } 39 | #else 40 | inline uint_fast32_t morton2D_32_encode(const uint_fast16_t x, const uint_fast16_t y) { 41 | return m2D_e_sLUT(x, y); 42 | } 43 | inline uint_fast64_t morton2D_64_encode(const uint_fast32_t x, const uint_fast32_t y) { 44 | return m2D_e_sLUT(x, y); 45 | } 46 | inline uint_fast32_t morton3D_32_encode(const uint_fast16_t x, const uint_fast16_t y, const uint_fast16_t z) { 47 | return m3D_e_sLUT(x, y, z); 48 | } 49 | inline uint_fast64_t morton3D_64_encode(const uint_fast32_t x, const uint_fast32_t y, const uint_fast32_t z) { 50 | return m3D_e_sLUT(x, y, z); 51 | } 52 | #endif 53 | 54 | // DECODING 55 | #if defined(__BMI2__)// || __AVX2__ 56 | inline void morton2D_32_decode(const uint_fast32_t morton, uint_fast16_t& x, uint_fast16_t& y) { 57 | m2D_d_BMI(morton, x, y); 58 | } 59 | inline void morton2D_64_decode(const uint_fast64_t morton, uint_fast32_t& x, uint_fast32_t& y) { 60 | m2D_d_BMI(morton, x, y); 61 | } 62 | inline void morton3D_32_decode(const uint_fast32_t morton, uint_fast16_t& x, uint_fast16_t& y, uint_fast16_t& z) { 63 | m3D_d_BMI(morton, x, y, z); 64 | } 65 | inline void morton3D_64_decode(const uint_fast64_t morton, uint_fast32_t& x, uint_fast32_t& y, uint_fast32_t& z) { 66 | m3D_d_BMI(morton, x, y, z); 67 | } 68 | #else 69 | inline void morton2D_32_decode(const uint_fast32_t morton, uint_fast16_t& x, uint_fast16_t& y) { 70 | m2D_d_sLUT(morton, x, y); 71 | } 72 | inline void morton2D_64_decode(const uint_fast64_t morton, uint_fast32_t& x, uint_fast32_t& y) { 73 | m2D_d_sLUT(morton, x, y); 74 | } 75 | inline void morton3D_32_decode(const uint_fast32_t morton, uint_fast16_t& x, uint_fast16_t& y, uint_fast16_t& z) { 76 | m3D_d_sLUT(morton, x, y, z); 77 | } 78 | inline void morton3D_64_decode(const uint_fast64_t morton, uint_fast32_t& x, uint_fast32_t& y, uint_fast32_t& z) { 79 | m3D_d_sLUT(morton, x, y, z); 80 | } 81 | #endif -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Pathfinding/AeonixNavigationPath.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "AeonixNavigationPath.generated.h" 4 | 5 | struct FAeonixData; 6 | 7 | // Delegate for path invalidation notification 8 | DECLARE_MULTICAST_DELEGATE_OneParam(FOnPathInvalidated, FAeonixNavigationPath*); 9 | 10 | UENUM(BlueprintType) 11 | enum class EAeonixPathPointType : uint8 12 | { 13 | NODE_CENTER UMETA(ToolTip="Path points are in the center of each traversed node"), 14 | INTERMEDIATE UMETA(ToolTip="Path points are halfway between the center of each traversed node edge"), 15 | }; 16 | 17 | UENUM(BlueprintType) 18 | enum class EAeonixPathHeuristicType : uint8 19 | { 20 | EUCLIDEAN, 21 | VELOCITY 22 | }; 23 | 24 | struct FNavigationPath; 25 | class AAeonixBoundingVolume; 26 | 27 | USTRUCT(BlueprintType) 28 | struct AEONIXNAVIGATION_API FAeonixPathPoint 29 | { 30 | GENERATED_BODY() 31 | FAeonixPathPoint(){} 32 | 33 | FAeonixPathPoint(const FVector& aPosition, int aLayer) 34 | : Position(aPosition) 35 | , Layer(aLayer) 36 | , bCullFlag(false) 37 | 38 | { 39 | } 40 | 41 | UPROPERTY(BlueprintReadOnly, Category = "Aeonix|Path") 42 | FVector Position{}; // Position of the point 43 | 44 | UPROPERTY(BlueprintReadOnly, Category = "Aeonix|Path") 45 | int Layer{-1}; // Layer that the point came from (so we can infer it's volume) 46 | bool bCullFlag{false}; 47 | #if WITH_EDITOR 48 | FVector NodePosition; // Position of the node that this point came from, for debug display 49 | #endif 50 | 51 | }; 52 | 53 | #if WITH_EDITOR 54 | // Structure to store debug information about the original path nodes 55 | struct FDebugVoxelInfo 56 | { 57 | FVector Position; // Original position of the voxel 58 | int32 Layer; // Layer of the voxel in the octree 59 | bool bWasEmptyLeaf; // True if this node used the empty leaf optimization (skipped 64-voxel subdivision) 60 | 61 | FDebugVoxelInfo() : Position(FVector::ZeroVector), Layer(-1), bWasEmptyLeaf(false) {} 62 | 63 | FDebugVoxelInfo(const FVector& InPosition, int32 InLayer, bool bInWasEmptyLeaf = false) 64 | : Position(InPosition), Layer(InLayer), bWasEmptyLeaf(bInWasEmptyLeaf) 65 | { 66 | } 67 | }; 68 | #endif 69 | 70 | USTRUCT(BlueprintType) 71 | struct AEONIXNAVIGATION_API FAeonixNavigationPath 72 | { 73 | GENERATED_BODY() 74 | public: 75 | void AddPoint(const FAeonixPathPoint& aPoint); 76 | void ResetForRepath(); 77 | 78 | void DebugDraw(UWorld* World, const FAeonixData& Data); 79 | void DebugDrawLite(UWorld* World, const FColor& LineColor = FColor::Cyan, float LifeTime = 60.0f) const; 80 | 81 | const TArray& GetPathPoints() const 82 | { 83 | return myPoints; 84 | }; 85 | 86 | TArray& GetPathPoints() { return myPoints; } 87 | 88 | int32 GetNumPoints() const { return myPoints.Num(); } 89 | 90 | #if WITH_EDITOR 91 | // Sets the debug voxel information, to be called before any path optimizations 92 | void SetDebugVoxelInfo(const TArray& InVoxelInfo) 93 | { 94 | myDebugVoxelInfo = InVoxelInfo; 95 | } 96 | 97 | // Creates debug voxel information from the current path points 98 | void StoreOriginalPathForDebug() 99 | { 100 | myDebugVoxelInfo.Empty(myPoints.Num()); 101 | for (const FAeonixPathPoint& Point : myPoints) 102 | { 103 | myDebugVoxelInfo.Add(FDebugVoxelInfo(Point.Position, Point.Layer)); 104 | } 105 | } 106 | #endif 107 | 108 | bool IsReady() const { return myIsReady; }; 109 | void SetIsReady(bool aIsReady) { myIsReady = aIsReady; } 110 | 111 | // Copy the path positions into a standard navigation path 112 | void CreateNavPath(FNavigationPath& aOutPath); 113 | 114 | // Path invalidation support 115 | void AddTraversedRegion(const FGuid& RegionId); 116 | bool CheckInvalidation(const TSet& RegeneratedRegions) const; 117 | void MarkInvalid(); 118 | bool IsValid() const { return bIsValid; } 119 | const TSet& GetTraversedRegionIds() const { return TraversedRegionIds; } 120 | 121 | // Delegate broadcast when path is invalidated 122 | FOnPathInvalidated OnPathInvalidated; 123 | 124 | protected: 125 | bool myIsReady; 126 | TArray myPoints; 127 | #if WITH_EDITOR 128 | TArray myDebugVoxelInfo; 129 | #endif 130 | 131 | // Path invalidation tracking 132 | TSet TraversedRegionIds; 133 | bool bIsValid = true; 134 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Task/BTTask_AeonixMoveTo.h: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "BehaviorTree/Tasks/BTTask_BlackboardBase.h" 6 | 7 | #include "BTTask_AeonixMoveTo.generated.h" 8 | 9 | class UAITask_AeonixMoveTo; 10 | class UBlackboardComponent; 11 | 12 | struct FBTAeonixMoveToTaskMemory 13 | { 14 | /** Move request ID */ 15 | FAIRequestID MoveRequestID; 16 | 17 | FDelegateHandle BBObserverDelegateHandle; 18 | FVector PreviousGoalLocation; 19 | 20 | TWeakObjectPtr Task; 21 | 22 | uint8 bWaitingForPath : 1; 23 | uint8 bObserverCanFinishTask : 1; 24 | }; 25 | 26 | /** 27 | * Move To task node. 28 | * Moves the AI pawn toward the specified Actor or Location blackboard entry using the navigation system. 29 | */ 30 | UCLASS(config = Game) 31 | class AEONIXNAVIGATION_API UBTTask_AeonixMoveTo : public UBTTask_BlackboardBase 32 | { 33 | GENERATED_BODY() 34 | 35 | UBTTask_AeonixMoveTo(const FObjectInitializer& ObjectInitializer); 36 | 37 | /** fixed distance added to threshold between AI and goal location in destination reach test */ 38 | UPROPERTY(config, Category = Node, EditAnywhere, meta = (ClampMin = "0.0", UIMin = "0.0")) 39 | float AcceptableRadius; 40 | 41 | /** if task is expected to react to changes to location represented by BB key 42 | * this property can be used to tweak sensitivity of the mechanism. Value is 43 | * recommended to be less then AcceptableRadius */ 44 | UPROPERTY(Category = Blackboard, EditAnywhere, AdvancedDisplay, meta = (ClampMin = "1", UIMin = "1", EditCondition = "bObserveBlackboardValue")) 45 | float ObservedBlackboardValueTolerance; 46 | 47 | /** if move goal in BB changes the move will be redirected to new location */ 48 | UPROPERTY() 49 | uint32 bObserveBlackboardValue : 1; 50 | 51 | /** if set, path to goal actor will update itself when actor moves */ 52 | UPROPERTY(Category = Node, EditAnywhere, AdvancedDisplay) 53 | uint32 bTrackMovingGoal : 1; 54 | 55 | /** if set, radius of AI's capsule will be added to threshold between AI and goal location in destination reach test */ 56 | UPROPERTY(Category = Node, EditAnywhere) 57 | uint32 bReachTestIncludesAgentRadius : 1; 58 | 59 | /** if set, radius of goal's capsule will be added to threshold between AI and goal location in destination reach test */ 60 | UPROPERTY(Category = Node, EditAnywhere) 61 | uint32 bReachTestIncludesGoalRadius : 1; 62 | 63 | /** if set, radius of AI's capsule will be added to threshold between AI and goal location in destination reach test */ 64 | UPROPERTY(Category = Node, EditAnywhere) 65 | bool bUseAsyncPathfinding; 66 | 67 | /** if true, automatically repath when path is invalidated by dynamic region changes. If false, abort the move. */ 68 | UPROPERTY(Category = Node, EditAnywhere, AdvancedDisplay, meta = (DisplayName = "Repath On Invalidation")) 69 | bool bRepathOnInvalidation = true; 70 | 71 | virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; 72 | virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override; 73 | virtual void OnTaskFinished(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTNodeResult::Type TaskResult) override; 74 | virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override; 75 | virtual uint16 GetInstanceMemorySize() const override; 76 | virtual void PostLoad() override; 77 | 78 | virtual void OnGameplayTaskDeactivated(UGameplayTask& Task) override; 79 | virtual void OnMessage(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, FName Message, int32 RequestID, bool bSuccess) override; 80 | EBlackboardNotificationResult OnBlackboardValueChange(const UBlackboardComponent& Blackboard, FBlackboard::FKey ChangedKeyID); 81 | 82 | virtual void DescribeRuntimeValues(const UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, EBTDescriptionVerbosity::Type Verbosity, TArray& Values) const override; 83 | virtual FString GetStaticDescription() const override; 84 | 85 | #if WITH_EDITOR 86 | virtual FName GetNodeIconName() const override; 87 | virtual void OnNodeCreated() override; 88 | #endif // WITH_EDITOR 89 | 90 | protected: 91 | 92 | EBTNodeResult::Type PerformMoveTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory); 93 | 94 | /** prepares move task for activation */ 95 | virtual UAITask_AeonixMoveTo* PrepareMoveTask(UBehaviorTreeComponent& OwnerComp, UAITask_AeonixMoveTo* ExistingTask, FAIMoveRequest& MoveRequest); 96 | }; 97 | -------------------------------------------------------------------------------- /Source/AeonixEditor/Private/SAeonixNavigationTreeView.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Widgets/SCompoundWidget.h" 7 | #include "Widgets/Views/STreeView.h" 8 | 9 | class AAeonixBoundingVolume; 10 | class AAeonixModifierVolume; 11 | class UAeonixDynamicObstacleComponent; 12 | class UAeonixSubsystem; 13 | 14 | /** 15 | * Tree item types for the navigation hierarchy 16 | */ 17 | enum class EAeonixTreeItemType : uint8 18 | { 19 | World, 20 | BoundingVolume, 21 | ModifierVolume, 22 | DynamicComponent 23 | }; 24 | 25 | /** 26 | * Data structure representing a single item in the navigation tree view 27 | */ 28 | struct FAeonixTreeItem : TSharedFromThis 29 | { 30 | FAeonixTreeItem() = default; 31 | 32 | FAeonixTreeItem(EAeonixTreeItemType InType, const FString& InDisplayName) 33 | : Type(InType) 34 | , DisplayName(InDisplayName) 35 | {} 36 | 37 | EAeonixTreeItemType Type = EAeonixTreeItemType::World; 38 | FString DisplayName; 39 | 40 | // Object references based on type 41 | TWeakObjectPtr BoundingVolume; 42 | TWeakObjectPtr ModifierVolume; 43 | TWeakObjectPtr DynamicComponent; 44 | 45 | // Tree structure 46 | TWeakPtr Parent; 47 | TArray> Children; 48 | 49 | bool IsValid() const; 50 | FString GetIconName() const; 51 | AActor* GetActor() const; 52 | }; 53 | 54 | typedef TSharedPtr FAeonixTreeItemPtr; 55 | 56 | /** 57 | * Slate widget that displays a tree view of Aeonix navigation elements: 58 | * World -> Bounding Volumes -> Modifier Volumes / Dynamic Components 59 | */ 60 | class SAeonixNavigationTreeView : public SCompoundWidget 61 | { 62 | public: 63 | SLATE_BEGIN_ARGS(SAeonixNavigationTreeView) {} 64 | SLATE_END_ARGS() 65 | 66 | void Construct(const FArguments& InArgs); 67 | virtual ~SAeonixNavigationTreeView(); 68 | 69 | private: 70 | // Tree view generation 71 | TSharedRef OnGenerateRow(FAeonixTreeItemPtr Item, const TSharedRef& OwnerTable); 72 | void OnGetChildren(FAeonixTreeItemPtr Item, TArray& OutChildren); 73 | void OnSelectionChanged(FAeonixTreeItemPtr Item, ESelectInfo::Type SelectInfo); 74 | void OnItemDoubleClick(FAeonixTreeItemPtr Item); 75 | 76 | // Data management 77 | void RefreshTreeData(); 78 | void PopulateTreeFromWorld(UWorld* World); 79 | UAeonixSubsystem* GetSubsystem() const; 80 | UWorld* GetTargetWorld() const; 81 | 82 | // PIE auto-refresh timer 83 | EActiveTimerReturnType UpdateDuringPIE(double InCurrentTime, float InDeltaTime); 84 | 85 | // Button callbacks 86 | FReply OnRefreshClicked(); 87 | FReply OnTerminatePathfindsClicked(); 88 | FReply OnExpandAllClicked(); 89 | FReply OnCollapseAllClicked(); 90 | 91 | // Item action callbacks 92 | FReply OnRegenerateVolumeClicked(FAeonixTreeItemPtr Item); 93 | FReply OnRegenerateModifierClicked(FAeonixTreeItemPtr Item); 94 | 95 | // Registration change callback 96 | void OnRegistrationChanged(); 97 | 98 | // Status text 99 | FText GetPathfindMetricsText() const; 100 | FText GetWorkerPoolStatusText() const; 101 | FText GetGenerationMetricsText() const; 102 | 103 | // Tree state management 104 | void ExpandAllItems(); 105 | void CollapseAllItems(); 106 | void ExpandItemRecursive(FAeonixTreeItemPtr Item); 107 | 108 | private: 109 | TSharedPtr> TreeView; 110 | TArray RootItems; 111 | TSet ExpandedItems; 112 | 113 | // Active timer handle for PIE auto-refresh 114 | TSharedPtr PIERefreshTimerHandle; 115 | 116 | // Track PIE state to detect transitions 117 | bool bWasInPIE = false; 118 | 119 | // Blocked voxel visualization 120 | TWeakObjectPtr BlockedVizActiveVolume; 121 | int32 MaxBlockedVoxels = 5000; 122 | float BlockedVizRange = 500.0f; 123 | FVector LastCameraPosition = FVector::ZeroVector; 124 | 125 | // Blocked voxel visualization handlers 126 | ECheckBoxState IsBlockedVizEnabled(FAeonixTreeItemPtr Item) const; 127 | void OnBlockedVizToggled(ECheckBoxState NewState, FAeonixTreeItemPtr Item); 128 | void UpdateBlockedVoxelVisualization(); 129 | 130 | // Max voxels control 131 | int32 GetMaxBlockedVoxelsValue() const; 132 | void OnMaxBlockedVoxelsChanged(int32 NewValue); 133 | 134 | // Range control 135 | float GetBlockedVizRangeValue() const; 136 | void OnBlockedVizRangeChanged(float NewValue); 137 | }; 138 | -------------------------------------------------------------------------------- /Source/AeonixEditor/Public/AeonixPerformanceTypes.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "CoreMinimal.h" 4 | #include "AeonixPerformanceTypes.generated.h" 5 | 6 | class AAeonixBoundingVolume; 7 | 8 | UENUM(BlueprintType) 9 | enum class EAeonixPerformanceTestStatus : uint8 10 | { 11 | NotStarted, 12 | Running, 13 | Completed, 14 | Failed, 15 | Cancelled 16 | }; 17 | 18 | USTRUCT(BlueprintType) 19 | struct AEONIXEDITOR_API FAeonixPerformanceTestSettings 20 | { 21 | GENERATED_BODY() 22 | 23 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test Configuration") 24 | int32 NumberOfTests = 100; 25 | 26 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test Configuration") 27 | int32 RandomSeed = 12345; 28 | 29 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test Configuration") 30 | float MinPathDistance = 100.0f; 31 | 32 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test Configuration") 33 | float MaxPathDistance = 2000.0f; 34 | 35 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Test Configuration") 36 | bool bVisualizeResults = true; 37 | 38 | }; 39 | 40 | USTRUCT(BlueprintType) 41 | struct AEONIXEDITOR_API FAeonixPerformanceTestResult 42 | { 43 | GENERATED_BODY() 44 | 45 | UPROPERTY(BlueprintReadOnly, Category = "Results") 46 | FVector StartPosition = FVector::ZeroVector; 47 | 48 | UPROPERTY(BlueprintReadOnly, Category = "Results") 49 | FVector EndPosition = FVector::ZeroVector; 50 | 51 | UPROPERTY(BlueprintReadOnly, Category = "Results") 52 | bool bPathFound = false; 53 | 54 | UPROPERTY(BlueprintReadOnly, Category = "Results") 55 | float PathfindingTime = 0.0f; 56 | 57 | UPROPERTY(BlueprintReadOnly, Category = "Results") 58 | float PathLength = 0.0f; 59 | 60 | UPROPERTY(BlueprintReadOnly, Category = "Results") 61 | int32 NodesExplored = 0; 62 | 63 | UPROPERTY(BlueprintReadOnly, Category = "Results") 64 | int32 PathPoints = 0; 65 | }; 66 | 67 | USTRUCT(BlueprintType) 68 | struct AEONIXEDITOR_API FAeonixPerformanceTestSummary 69 | { 70 | GENERATED_BODY() 71 | 72 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 73 | int32 TotalTests = 0; 74 | 75 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 76 | int32 SuccessfulPaths = 0; 77 | 78 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 79 | int32 FailedPaths = 0; 80 | 81 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 82 | float SuccessRate = 0.0f; 83 | 84 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 85 | float MinPathfindingTime = 0.0f; 86 | 87 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 88 | float MaxPathfindingTime = 0.0f; 89 | 90 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 91 | float AveragePathfindingTime = 0.0f; 92 | 93 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 94 | float MedianPathfindingTime = 0.0f; 95 | 96 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 97 | float AveragePathLength = 0.0f; 98 | 99 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 100 | float AverageNodesExplored = 0.0f; 101 | 102 | UPROPERTY(BlueprintReadOnly, Category = "Summary") 103 | float TotalTestTime = 0.0f; 104 | 105 | TArray Results; 106 | 107 | void CalculateSummary() 108 | { 109 | TotalTests = Results.Num(); 110 | SuccessfulPaths = 0; 111 | FailedPaths = 0; 112 | 113 | if (TotalTests == 0) 114 | { 115 | return; 116 | } 117 | 118 | TArray PathfindingTimes; 119 | float TotalPathfindingTime = 0.0f; 120 | float TotalPathLength = 0.0f; 121 | float TotalNodesExplored = 0.0f; 122 | 123 | for (const FAeonixPerformanceTestResult& Result : Results) 124 | { 125 | if (Result.bPathFound) 126 | { 127 | SuccessfulPaths++; 128 | PathfindingTimes.Add(Result.PathfindingTime); 129 | TotalPathfindingTime += Result.PathfindingTime; 130 | TotalPathLength += Result.PathLength; 131 | TotalNodesExplored += Result.NodesExplored; 132 | } 133 | else 134 | { 135 | FailedPaths++; 136 | } 137 | } 138 | 139 | SuccessRate = (float)SuccessfulPaths / (float)TotalTests * 100.0f; 140 | 141 | if (SuccessfulPaths > 0) 142 | { 143 | AveragePathfindingTime = TotalPathfindingTime / SuccessfulPaths; 144 | AveragePathLength = TotalPathLength / SuccessfulPaths; 145 | AverageNodesExplored = TotalNodesExplored / SuccessfulPaths; 146 | 147 | PathfindingTimes.Sort(); 148 | MinPathfindingTime = PathfindingTimes[0]; 149 | MaxPathfindingTime = PathfindingTimes.Last(); 150 | 151 | if (PathfindingTimes.Num() % 2 == 0) 152 | { 153 | int32 Mid = PathfindingTimes.Num() / 2; 154 | MedianPathfindingTime = (PathfindingTimes[Mid - 1] + PathfindingTimes[Mid]) * 0.5f; 155 | } 156 | else 157 | { 158 | MedianPathfindingTime = PathfindingTimes[PathfindingTimes.Num() / 2]; 159 | } 160 | } 161 | } 162 | }; -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Component/AeonixDynamicObstacleComponent.cpp: -------------------------------------------------------------------------------- 1 | // Copyright Chris Kang. All Rights Reserved. 2 | 3 | #include "Component/AeonixDynamicObstacleComponent.h" 4 | #include "Actor/AeonixBoundingVolume.h" 5 | #include "Subsystem/AeonixSubsystem.h" 6 | #include "AeonixNavigation.h" 7 | 8 | #include "GameFramework/Actor.h" 9 | 10 | UAeonixDynamicObstacleComponent::UAeonixDynamicObstacleComponent(const FObjectInitializer& ObjectInitializer) 11 | : Super(ObjectInitializer) 12 | { 13 | // Component does not tick - subsystem handles all obstacle ticking 14 | PrimaryComponentTick.bCanEverTick = false; 15 | } 16 | 17 | void UAeonixDynamicObstacleComponent::OnRegister() 18 | { 19 | Super::OnRegister(); 20 | 21 | // Register with subsystem (works in editor and at runtime) 22 | RegisterWithSubsystem(); 23 | } 24 | 25 | void UAeonixDynamicObstacleComponent::OnUnregister() 26 | { 27 | // Unregister from subsystem 28 | UnregisterFromSubsystem(); 29 | 30 | Super::OnUnregister(); 31 | } 32 | 33 | void UAeonixDynamicObstacleComponent::BeginPlay() 34 | { 35 | Super::BeginPlay(); 36 | 37 | // Reset state from editor world - PIE creates new world with new actor instances 38 | CurrentBoundingVolume = nullptr; 39 | CurrentDynamicRegionIds.Empty(); 40 | bRegisteredWithSubsystem = false; 41 | 42 | RegisterWithSubsystem(); 43 | } 44 | 45 | void UAeonixDynamicObstacleComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) 46 | { 47 | // Unregister from subsystem 48 | UnregisterFromSubsystem(); 49 | 50 | Super::EndPlay(EndPlayReason); 51 | } 52 | 53 | void UAeonixDynamicObstacleComponent::RegisterWithSubsystem() 54 | { 55 | if (bRegisteredWithSubsystem) 56 | { 57 | return; // Already registered 58 | } 59 | 60 | // Get reference to the Aeonix subsystem 61 | if (UWorld* World = GetWorld()) 62 | { 63 | UAeonixSubsystem* Subsystem = World->GetSubsystem(); 64 | if (!Subsystem) 65 | { 66 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("DynamicObstacle %s: No AeonixSubsystem found"), *GetName()); 67 | return; 68 | } 69 | 70 | // Register with the subsystem - it handles all transform tracking and region detection 71 | Subsystem->RegisterDynamicObstacle(this); 72 | bRegisteredWithSubsystem = true; 73 | 74 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("DynamicObstacle %s: Registered with subsystem"), *GetName()); 75 | } 76 | } 77 | 78 | void UAeonixDynamicObstacleComponent::UnregisterFromSubsystem() 79 | { 80 | if (!bRegisteredWithSubsystem) 81 | { 82 | return; // Not registered 83 | } 84 | 85 | // Unregister from subsystem 86 | UWorld* World = GetWorld(); 87 | if (!World) 88 | { 89 | bRegisteredWithSubsystem = false; 90 | return; 91 | } 92 | 93 | UAeonixSubsystem* Subsystem = World->GetSubsystem(); 94 | if (!Subsystem) 95 | { 96 | bRegisteredWithSubsystem = false; 97 | return; 98 | } 99 | 100 | Subsystem->UnRegisterDynamicObstacle(this); 101 | bRegisteredWithSubsystem = false; 102 | 103 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("DynamicObstacle %s: Unregistered from subsystem"), *GetName()); 104 | } 105 | 106 | #if WITH_EDITOR 107 | void UAeonixDynamicObstacleComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) 108 | { 109 | Super::PostEditChangeProperty(PropertyChangedEvent); 110 | 111 | // Properties changed - the subsystem will detect movement in its next tick 112 | } 113 | #endif 114 | 115 | void UAeonixDynamicObstacleComponent::TriggerNavigationRegen() 116 | { 117 | if (!bEnableNavigationRegen) 118 | { 119 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("DynamicObstacle %s: Manual trigger ignored (disabled)"), *GetName()); 120 | return; 121 | } 122 | 123 | if (!CurrentBoundingVolume.IsValid()) 124 | { 125 | UE_LOG(LogAeonixNavigation, Warning, TEXT("DynamicObstacle %s: Manual trigger ignored (not inside any bounding volume)"), *GetName()); 126 | return; 127 | } 128 | 129 | if (CurrentDynamicRegionIds.Num() == 0) 130 | { 131 | UE_LOG(LogAeonixNavigation, Warning, TEXT("DynamicObstacle %s: Manual trigger ignored (not inside any dynamic regions)"), *GetName()); 132 | return; 133 | } 134 | 135 | // Request regeneration for all regions we're currently inside 136 | for (const FGuid& RegionId : CurrentDynamicRegionIds) 137 | { 138 | CurrentBoundingVolume->RequestDynamicRegionRegen(RegionId); 139 | } 140 | 141 | // In editor, immediately process dirty regions for instant feedback 142 | // At runtime, the subsystem tick will process them with throttling 143 | #if WITH_EDITOR 144 | if (!GetWorld()->IsGameWorld()) 145 | { 146 | CurrentBoundingVolume->TryProcessDirtyRegions(); 147 | } 148 | #endif 149 | 150 | UE_LOG(LogAeonixNavigation, Display, TEXT("DynamicObstacle %s: Manually triggered regen for %d regions"), 151 | *GetName(), CurrentDynamicRegionIds.Num()); 152 | } 153 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Util/AeonixMediator.cpp: -------------------------------------------------------------------------------- 1 | #include "Util/AeonixMediator.h" 2 | #include "Data/AeonixLink.h" 3 | #include "Actor/AeonixBoundingVolume.h" 4 | 5 | #include "DrawDebugHelpers.h" 6 | 7 | bool AeonixMediator::GetLinkFromPosition(const FVector& aPosition, const AAeonixBoundingVolume& aVolume, AeonixLink& oLink) 8 | { 9 | // Position is outside the volume, no can do 10 | if (!aVolume.IsPointInside(aPosition)) 11 | { 12 | return false; 13 | } 14 | 15 | if (!aVolume.HasData()) 16 | { 17 | return false; 18 | } 19 | 20 | // Use cached bounds from NavigationData instead of recalculating GetComponentsBoundingBox 21 | const FAeonixGenerationParameters& Params = aVolume.GetNavData().GetParams(); 22 | const FVector& origin = Params.Origin; 23 | const FVector& extent = Params.Extents; 24 | // The z-order origin of the volume (where code == 0) 25 | FVector zOrigin = origin - extent; 26 | // The local position of the point in volume space 27 | FVector localPos = aPosition - zOrigin; 28 | 29 | int layerIndex = aVolume.GetNavData().OctreeData.GetNumLayers() - 1; 30 | nodeindex_t nodeIndex = 0; 31 | while (layerIndex >= 0 && layerIndex < aVolume.GetNavData().OctreeData.GetNumLayers()) 32 | { 33 | // Get the layer and voxel size 34 | 35 | const TArray& layer = aVolume.GetNavData().OctreeData.GetLayer(layerIndex); 36 | // Calculate the XYZ coordinates 37 | 38 | // TODO: compiler probably tidies this up, but we can do better 39 | FIntVector voxel; 40 | GetVolumeXYZ(aPosition, aVolume, layerIndex, voxel); 41 | uint_fast32_t x, y, z; 42 | x = voxel.X; 43 | y = voxel.Y; 44 | z = voxel.Z; 45 | 46 | // Get the morton code we want for this layer 47 | mortoncode_t code = morton3D_64_encode(x, y, z); 48 | 49 | for (nodeindex_t j = nodeIndex; j < layer.Num(); j++) 50 | { 51 | const AeonixNode& node = layer[j]; 52 | // This is the node we are in 53 | if (node.Code == code) 54 | { 55 | // There are no child nodes, so this is our nav position 56 | if (!node.FirstChild.IsValid()) 57 | { 58 | oLink.LayerIndex = layerIndex; 59 | oLink.NodeIndex = j; 60 | oLink.SubnodeIndex = 0; 61 | return true; 62 | } 63 | 64 | // If this is a leaf node, we need to find our subnode 65 | if (layerIndex == 0) 66 | { 67 | const AeonixLeafNode& leaf = aVolume.GetNavData().OctreeData.GetLeafNode(node.FirstChild.NodeIndex); 68 | // We need to calculate the node local position to get the morton code for the leaf 69 | float voxelSize = aVolume.GetNavData().GetVoxelSize(layerIndex); 70 | // The world position of the 0 node 71 | FVector nodePosition; 72 | aVolume.GetNavData().GetNodePosition(layerIndex, node.Code, nodePosition); 73 | // The morton origin of the node 74 | FVector nodeOrigin = nodePosition - FVector(voxelSize * 0.5f); 75 | // The requested position, relative to the node origin 76 | FVector nodeLocalPos = aPosition - nodeOrigin; 77 | // Now get our voxel coordinates 78 | FIntVector coord; 79 | coord.X = FMath::FloorToInt((nodeLocalPos.X / (voxelSize * 0.25f))); 80 | coord.Y = FMath::FloorToInt((nodeLocalPos.Y / (voxelSize * 0.25f))); 81 | coord.Z = FMath::FloorToInt((nodeLocalPos.Z / (voxelSize * 0.25f))); 82 | 83 | // So our link is.....*drum roll* 84 | oLink.LayerIndex = 0; // Layer 0 (leaf) 85 | oLink.NodeIndex = j; // This index 86 | 87 | mortoncode_t leafIndex = morton3D_64_encode(coord.X, coord.Y, coord.Z); // This morton code is our key into the 64-bit leaf node 88 | 89 | if (leaf.GetNode(leafIndex)) 90 | { 91 | return false; // This voxel is blocked, oops! 92 | } 93 | 94 | oLink.SubnodeIndex = leafIndex; 95 | 96 | return true; 97 | } 98 | 99 | // If we've got here, the current node has a child, and isn't a leaf, so lets go down... 100 | layerIndex = layer[j].FirstChild.GetLayerIndex(); 101 | nodeIndex = layer[j].FirstChild.GetNodeIndex(); 102 | 103 | break; //stop iterating this layer 104 | } 105 | } 106 | } 107 | 108 | return false; 109 | } 110 | 111 | void AeonixMediator::GetVolumeXYZ(const FVector& aPosition, const AAeonixBoundingVolume& aVolume, const int aLayer, FIntVector& oXYZ) 112 | { 113 | // Use cached bounds from NavigationData instead of recalculating GetComponentsBoundingBox 114 | const FAeonixGenerationParameters& Params = aVolume.GetNavData().GetParams(); 115 | const FVector& origin = Params.Origin; 116 | const FVector& extent = Params.Extents; 117 | // The z-order origin of the volume (where code == 0) 118 | FVector zOrigin = origin - extent; 119 | // The local position of the point in volume space 120 | FVector localPos = aPosition - zOrigin; 121 | 122 | int layerIndex = aLayer; 123 | 124 | // Get the layer and voxel size 125 | float voxelSize = aVolume.GetNavData().GetVoxelSize(layerIndex); 126 | 127 | // Calculate the XYZ coordinates 128 | 129 | oXYZ.X = FMath::FloorToInt((localPos.X / voxelSize)); 130 | oXYZ.Y = FMath::FloorToInt((localPos.Y / voxelSize)); 131 | oXYZ.Z = FMath::FloorToInt((localPos.Z / voxelSize)); 132 | } 133 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/EQS/AeonixEQSFloodFillGenerator.cpp: -------------------------------------------------------------------------------- 1 | #include "EQS/AeonixEQSFloodFillGenerator.h" 2 | #include "EnvironmentQuery/Items/EnvQueryItemType_Point.h" 3 | #include "EnvironmentQuery/Contexts/EnvQueryContext_Querier.h" 4 | #include "NavigationSystem.h" 5 | #include "Data/AeonixData.h" 6 | #include "Subsystem/AeonixSubsystem.h" 7 | #include "Actor/AeonixBoundingVolume.h" 8 | #include "Util/AeonixMediator.h" 9 | #include "Data/AeonixLink.h" 10 | 11 | UAeonixEQSFloodFillGenerator::UAeonixEQSFloodFillGenerator(const FObjectInitializer& ObjectInitializer) 12 | { 13 | Context = UEnvQueryContext_Querier::StaticClass(); 14 | ItemType = UEnvQueryItemType_Point::StaticClass(); 15 | FloodRadius.DefaultValue = 1000.0f; 16 | FloodStepsMax.DefaultValue = 1000; 17 | NavAgentIndex.DefaultValue = 0; 18 | MinPointSpacing.DefaultValue = 0.0f; 19 | } 20 | 21 | void UAeonixEQSFloodFillGenerator::GenerateItems(FEnvQueryInstance& QueryInstance) const 22 | { 23 | UObject* Owner = QueryInstance.Owner.Get(); 24 | if (!Owner) return; 25 | 26 | UWorld* World = GEngine->GetWorldFromContextObject(Owner, EGetWorldErrorMode::ReturnNull); 27 | if (!World) return; 28 | 29 | TArray ContextLocations; 30 | QueryInstance.PrepareContext(Context, ContextLocations); 31 | if (ContextLocations.Num() == 0) return; 32 | FVector Origin = ContextLocations[0]; 33 | 34 | UAeonixSubsystem* AeonixSubsystem = World->GetSubsystem(); 35 | if (!AeonixSubsystem) return; 36 | 37 | float Radius = FloodRadius.GetValue(); 38 | int32 MaxSteps = FloodStepsMax.GetValue(); 39 | int32 AgentIdx = NavAgentIndex.GetValue(); 40 | float MinSpacing = MinPointSpacing.GetValue(); 41 | 42 | // Set up spatial bucket filtering if MinSpacing is enabled 43 | const bool bUseDensityFiltering = MinSpacing > 0.0f; 44 | const float BucketSize = bUseDensityFiltering ? MinSpacing * 0.866f : 0.0f; // Optimal sphere packing 45 | const float InvBucketSize = bUseDensityFiltering ? 1.0f / BucketSize : 0.0f; 46 | TSet OccupiedBuckets; 47 | 48 | // Get nav volume and data 49 | const AAeonixBoundingVolume* NavVolume = AeonixSubsystem->GetVolumeForPosition(Origin); 50 | if (!NavVolume || !NavVolume->HasData()) return; 51 | 52 | // Convert start position to AeonixLink 53 | AeonixLink StartLink; 54 | if (!AeonixMediator::GetLinkFromPosition(Origin, *NavVolume, StartLink)) 55 | return; 56 | 57 | // CRITICAL: Acquire read lock for thread-safe octree access 58 | // EQS queries can run on background threads while dynamic regeneration 59 | // is modifying the octree on the game thread 60 | FReadScopeLock ReadLock(NavVolume->GetOctreeDataLock()); 61 | const FAeonixData& NavData = NavVolume->GetNavData(); 62 | 63 | TSet Visited; 64 | TQueue Queue; 65 | Queue.Enqueue(StartLink); 66 | Visited.Add(StartLink); 67 | 68 | FVector StartPos; 69 | NavData.GetLinkPosition(StartLink, StartPos); 70 | 71 | int32 StepCount = 0; 72 | while (!Queue.IsEmpty() && StepCount < MaxSteps) 73 | { 74 | AeonixLink CurrentLink; 75 | Queue.Dequeue(CurrentLink); 76 | StepCount++; 77 | FVector CurrentPos; 78 | if (!NavData.GetLinkPosition(CurrentLink, CurrentPos)) 79 | continue; 80 | if (FVector::DistSquared(CurrentPos, Origin) > Radius * Radius) 81 | continue; 82 | 83 | // Apply density filtering if enabled 84 | bool bAddPoint = true; 85 | if (bUseDensityFiltering) 86 | { 87 | // Calculate spatial bucket for this point 88 | const FIntVector Bucket( 89 | FMath::FloorToInt(CurrentPos.X * InvBucketSize), 90 | FMath::FloorToInt(CurrentPos.Y * InvBucketSize), 91 | FMath::FloorToInt(CurrentPos.Z * InvBucketSize) 92 | ); 93 | 94 | // Check if bucket is already occupied 95 | if (OccupiedBuckets.Contains(Bucket)) 96 | { 97 | bAddPoint = false; 98 | } 99 | else 100 | { 101 | OccupiedBuckets.Add(Bucket); 102 | } 103 | } 104 | 105 | if (bAddPoint) 106 | { 107 | QueryInstance.AddItemData(CurrentPos); 108 | } 109 | 110 | // Get neighbors 111 | TArray Neighbors; 112 | const AeonixNode& CurrentNode = NavData.OctreeData.GetNode(CurrentLink); 113 | if (CurrentLink.GetLayerIndex() == 0 && CurrentNode.FirstChild.IsValid()) 114 | { 115 | NavData.OctreeData.GetLeafNeighbours(CurrentLink, Neighbors); 116 | } 117 | else 118 | { 119 | NavData.OctreeData.GetNeighbours(CurrentLink, Neighbors); 120 | } 121 | for (const AeonixLink& Neighbor : Neighbors) 122 | { 123 | if (!Neighbor.IsValid() || Visited.Contains(Neighbor)) 124 | continue; 125 | FVector NeighborPos; 126 | if (!NavData.GetLinkPosition(Neighbor, NeighborPos)) 127 | continue; 128 | if (FVector::DistSquared(NeighborPos, Origin) > Radius * Radius) 129 | continue; 130 | Visited.Add(Neighbor); 131 | Queue.Enqueue(Neighbor); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Library/libmorton/morton2D_LUTs.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | // Magicbits masks 6 | static uint_fast32_t magicbit2D_masks32[6] = { 0xFFFFFFFF, 0x0000FFFF, 0x00FF00FF, 0x0F0F0F0F, 0x33333333, 0x55555555 }; 7 | static uint_fast64_t magicbit2D_masks64[6] = { 0x00000000FFFFFFFF, 0x0000FFFF0000FFFF, 0x00FF00FF00FF00FF, 0x0F0F0F0F0F0F0F0F, 0x3333333333333333, 0x5555555555555555 }; 8 | 9 | static const uint_fast16_t Morton2D_encode_x_256[256] = 10 | { 11 | 0, 1, 4, 5, 16, 17, 20, 21, 12 | 64, 65, 68, 69, 80, 81, 84, 85, 13 | 256, 257, 260, 261, 272, 273, 276, 277, 14 | 320, 321, 324, 325, 336, 337, 340, 341, 15 | 1024, 1025, 1028, 1029, 1040, 1041, 1044, 1045, 16 | 1088, 1089, 1092, 1093, 1104, 1105, 1108, 1109, 17 | 1280, 1281, 1284, 1285, 1296, 1297, 1300, 1301, 18 | 1344, 1345, 1348, 1349, 1360, 1361, 1364, 1365, 19 | 4096, 4097, 4100, 4101, 4112, 4113, 4116, 4117, 20 | 4160, 4161, 4164, 4165, 4176, 4177, 4180, 4181, 21 | 4352, 4353, 4356, 4357, 4368, 4369, 4372, 4373, 22 | 4416, 4417, 4420, 4421, 4432, 4433, 4436, 4437, 23 | 5120, 5121, 5124, 5125, 5136, 5137, 5140, 5141, 24 | 5184, 5185, 5188, 5189, 5200, 5201, 5204, 5205, 25 | 5376, 5377, 5380, 5381, 5392, 5393, 5396, 5397, 26 | 5440, 5441, 5444, 5445, 5456, 5457, 5460, 5461, 27 | 16384, 16385, 16388, 16389, 16400, 16401, 16404, 16405, 28 | 16448, 16449, 16452, 16453, 16464, 16465, 16468, 16469, 29 | 16640, 16641, 16644, 16645, 16656, 16657, 16660, 16661, 30 | 16704, 16705, 16708, 16709, 16720, 16721, 16724, 16725, 31 | 17408, 17409, 17412, 17413, 17424, 17425, 17428, 17429, 32 | 17472, 17473, 17476, 17477, 17488, 17489, 17492, 17493, 33 | 17664, 17665, 17668, 17669, 17680, 17681, 17684, 17685, 34 | 17728, 17729, 17732, 17733, 17744, 17745, 17748, 17749, 35 | 20480, 20481, 20484, 20485, 20496, 20497, 20500, 20501, 36 | 20544, 20545, 20548, 20549, 20560, 20561, 20564, 20565, 37 | 20736, 20737, 20740, 20741, 20752, 20753, 20756, 20757, 38 | 20800, 20801, 20804, 20805, 20816, 20817, 20820, 20821, 39 | 21504, 21505, 21508, 21509, 21520, 21521, 21524, 21525, 40 | 21568, 21569, 21572, 21573, 21584, 21585, 21588, 21589, 41 | 21760, 21761, 21764, 21765, 21776, 21777, 21780, 21781, 42 | 21824, 21825, 21828, 21829, 21840, 21841, 21844, 21845 43 | }; 44 | 45 | static const uint_fast16_t Morton2D_encode_y_256[256] = 46 | { 47 | 0, 2, 8, 10, 32, 34, 40, 42, 48 | 128, 130, 136, 138, 160, 162, 168, 170, 49 | 512, 514, 520, 522, 544, 546, 552, 554, 50 | 640, 642, 648, 650, 672, 674, 680, 682, 51 | 2048, 2050, 2056, 2058, 2080, 2082, 2088, 2090, 52 | 2176, 2178, 2184, 2186, 2208, 2210, 2216, 2218, 53 | 2560, 2562, 2568, 2570, 2592, 2594, 2600, 2602, 54 | 2688, 2690, 2696, 2698, 2720, 2722, 2728, 2730, 55 | 8192, 8194, 8200, 8202, 8224, 8226, 8232, 8234, 56 | 8320, 8322, 8328, 8330, 8352, 8354, 8360, 8362, 57 | 8704, 8706, 8712, 8714, 8736, 8738, 8744, 8746, 58 | 8832, 8834, 8840, 8842, 8864, 8866, 8872, 8874, 59 | 10240, 10242, 10248, 10250, 10272, 10274, 10280, 10282, 60 | 10368, 10370, 10376, 10378, 10400, 10402, 10408, 10410, 61 | 10752, 10754, 10760, 10762, 10784, 10786, 10792, 10794, 62 | 10880, 10882, 10888, 10890, 10912, 10914, 10920, 10922, 63 | 32768, 32770, 32776, 32778, 32800, 32802, 32808, 32810, 64 | 32896, 32898, 32904, 32906, 32928, 32930, 32936, 32938, 65 | 33280, 33282, 33288, 33290, 33312, 33314, 33320, 33322, 66 | 33408, 33410, 33416, 33418, 33440, 33442, 33448, 33450, 67 | 34816, 34818, 34824, 34826, 34848, 34850, 34856, 34858, 68 | 34944, 34946, 34952, 34954, 34976, 34978, 34984, 34986, 69 | 35328, 35330, 35336, 35338, 35360, 35362, 35368, 35370, 70 | 35456, 35458, 35464, 35466, 35488, 35490, 35496, 35498, 71 | 40960, 40962, 40968, 40970, 40992, 40994, 41000, 41002, 72 | 41088, 41090, 41096, 41098, 41120, 41122, 41128, 41130, 73 | 41472, 41474, 41480, 41482, 41504, 41506, 41512, 41514, 74 | 41600, 41602, 41608, 41610, 41632, 41634, 41640, 41642, 75 | 43008, 43010, 43016, 43018, 43040, 43042, 43048, 43050, 76 | 43136, 43138, 43144, 43146, 43168, 43170, 43176, 43178, 77 | 43520, 43522, 43528, 43530, 43552, 43554, 43560, 43562, 78 | 43648, 43650, 43656, 43658, 43680, 43682, 43688, 43690 79 | }; 80 | 81 | static const uint_fast8_t Morton2D_decode_x_256[256] = { 82 | 0,1,0,1,2,3,2,3,0,1,0,1,2,3,2,3, 83 | 4,5,4,5,6,7,6,7,4,5,4,5,6,7,6,7, 84 | 0,1,0,1,2,3,2,3,0,1,0,1,2,3,2,3, 85 | 4,5,4,5,6,7,6,7,4,5,4,5,6,7,6,7, 86 | 8,9,8,9,10,11,10,11,8,9,8,9,10,11,10,11, 87 | 12,13,12,13,14,15,14,15,12,13,12,13,14,15,14,15, 88 | 8,9,8,9,10,11,10,11,8,9,8,9,10,11,10,11, 89 | 12,13,12,13,14,15,14,15,12,13,12,13,14,15,14,15, 90 | 0,1,0,1,2,3,2,3,0,1,0,1,2,3,2,3, 91 | 4,5,4,5,6,7,6,7,4,5,4,5,6,7,6,7, 92 | 0,1,0,1,2,3,2,3,0,1,0,1,2,3,2,3, 93 | 4,5,4,5,6,7,6,7,4,5,4,5,6,7,6,7, 94 | 8,9,8,9,10,11,10,11,8,9,8,9,10,11,10,11, 95 | 12,13,12,13,14,15,14,15,12,13,12,13,14,15,14,15, 96 | 8,9,8,9,10,11,10,11,8,9,8,9,10,11,10,11, 97 | 12,13,12,13,14,15,14,15,12,13,12,13,14,15,14,15 98 | }; 99 | 100 | static const uint_fast8_t Morton2D_decode_y_256[256] = { 101 | 0,0,1,1,0,0,1,1,2,2,3,3,2,2,3,3, 102 | 0,0,1,1,0,0,1,1,2,2,3,3,2,2,3,3, 103 | 4,4,5,5,4,4,5,5,6,6,7,7,6,6,7,7, 104 | 4,4,5,5,4,4,5,5,6,6,7,7,6,6,7,7, 105 | 0,0,1,1,0,0,1,1,2,2,3,3,2,2,3,3, 106 | 0,0,1,1,0,0,1,1,2,2,3,3,2,2,3,3, 107 | 4,4,5,5,4,4,5,5,6,6,7,7,6,6,7,7, 108 | 4,4,5,5,4,4,5,5,6,6,7,7,6,6,7,7, 109 | 8,8,9,9,8,8,9,9,10,10,11,11,10,10,11,11, 110 | 8,8,9,9,8,8,9,9,10,10,11,11,10,10,11,11, 111 | 12,12,13,13,12,12,13,13,14,14,15,15,14,14,15,15, 112 | 12,12,13,13,12,12,13,13,14,14,15,15,14,14,15,15, 113 | 8,8,9,9,8,8,9,9,10,10,11,11,10,10,11,11, 114 | 8,8,9,9,8,8,9,9,10,10,11,11,10,10,11,11, 115 | 12,12,13,13,12,12,13,13,14,14,15,15,14,14,15,15, 116 | 12,12,13,13,12,12,13,13,14,14,15,15,14,14,15,15 117 | }; 118 | -------------------------------------------------------------------------------- /Source/AeonixNavigationTests/Private/AeonixNavigationWallSplitTest.cpp: -------------------------------------------------------------------------------- 1 | #include "Data/AeonixData.h" 2 | #include "Pathfinding/AeonixPathFinder.h" 3 | #include "Pathfinding/AeonixNavigationPath.h" 4 | #include "Data/AeonixLink.h" 5 | #include "Engine/World.h" 6 | #include "Engine/EngineTypes.h" 7 | #include "Misc/AutomationTest.h" 8 | #include "../Public/AeonixNavigationTestMocks.h" 9 | 10 | IMPLEMENT_SIMPLE_AUTOMATION_TEST(FAeonixNavigation_WallSplitPathfindingTest, 11 | "AeonixNavigation.Pathfinding.WallSplitBug", 12 | EAutomationTestFlags::EditorContext | EAutomationTestFlags::ProductFilter) 13 | 14 | bool FAeonixNavigation_WallSplitPathfindingTest::RunTest(const FString& Parameters) 15 | { 16 | // Setup 17 | FTestWallCollisionQueryInterface WallCollision; 18 | FTestDebugDrawInterface DebugDraw; 19 | FAeonixData NavData; 20 | 21 | // Setup generation parameters for a volume split by a wall 22 | FAeonixGenerationParameters Params; 23 | Params.Origin = FVector::ZeroVector; 24 | Params.Extents = FVector(500, 500, 500); // 1000x1000x1000 volume centered at origin 25 | Params.OctreeDepth = 4; // 16x16x16 voxels at layer 0 26 | Params.CollisionChannel = ECollisionChannel::ECC_WorldStatic; 27 | Params.AgentRadius = 34.f; 28 | Params.ShowLeafVoxels = true; // Enable to see blocked voxels 29 | Params.ShowMortonCodes = false; 30 | 31 | NavData.UpdateGenerationParameters(Params); 32 | 33 | // Generate navigation data with the wall 34 | // Note: World parameter is still required but not used for collision after our fix 35 | UWorld* DummyWorld = nullptr; 36 | NavData.Generate(*DummyWorld, WallCollision, DebugDraw); 37 | 38 | UE_LOG(LogTemp, Display, TEXT("Navigation generation complete. Blocked voxels: %d/%d"), 39 | DebugDraw.BlockedVoxelCount, DebugDraw.TotalVoxelCount); 40 | 41 | // Setup pathfinding test 42 | FAeonixPathFinderSettings PathSettings; 43 | PathSettings.MaxIterations = 10000; 44 | PathSettings.bUseUnitCost = false; 45 | PathSettings.bOptimizePath = true; 46 | PathSettings.bUseStringPulling = true; 47 | PathSettings.HeuristicSettings.EuclideanWeight = 1.0f; 48 | PathSettings.HeuristicSettings.GlobalWeight = 10.0f; 49 | 50 | AeonixPathFinder PathFinder(NavData, PathSettings); 51 | 52 | // Define start and end positions on opposite sides of the wall 53 | FVector StartPos(-200, -200, 0); // One side of the wall (Y < 0) 54 | FVector EndPos(200, 200, 0); // Other side of the wall (Y > 0) 55 | 56 | UE_LOG(LogTemp, Display, TEXT("Testing path from %s to %s (wall at Y=0)"), 57 | *StartPos.ToString(), *EndPos.ToString()); 58 | 59 | // Find links for start and end positions 60 | AeonixLink StartLink, EndLink; 61 | FString StartLogMsg, EndLogMsg; 62 | 63 | bool bFoundStart = FAeonixNavigationTestUtils::FindLinkAtPosition(NavData, StartPos, StartLink, StartLogMsg); 64 | bool bFoundEnd = FAeonixNavigationTestUtils::FindLinkAtPosition(NavData, EndPos, EndLink, EndLogMsg); 65 | 66 | if (bFoundStart) 67 | UE_LOG(LogTemp, Display, TEXT("Start: %s"), *StartLogMsg); 68 | if (bFoundEnd) 69 | UE_LOG(LogTemp, Display, TEXT("End: %s"), *EndLogMsg); 70 | 71 | // Verify we found valid navigation links 72 | TestTrue(TEXT("Found valid start navigation link"), bFoundStart); 73 | TestTrue(TEXT("Found valid end navigation link"), bFoundEnd); 74 | 75 | if (!bFoundStart || !bFoundEnd) 76 | { 77 | UE_LOG(LogTemp, Error, TEXT("Could not find navigation links for test positions")); 78 | return false; 79 | } 80 | 81 | // Attempt to find a path 82 | FAeonixNavigationPath Path; 83 | bool bPathFound = PathFinder.FindPath(StartLink, EndLink, StartPos, EndPos, Path); 84 | 85 | // Log the result 86 | if (bPathFound) 87 | { 88 | const TArray& PathPoints = Path.GetPathPoints(); 89 | UE_LOG(LogTemp, Error, TEXT("BUG DEMONSTRATED: Path found through wall! Path has %d points:"), 90 | PathPoints.Num()); 91 | 92 | for (int32 i = 0; i < PathPoints.Num(); ++i) 93 | { 94 | const FAeonixPathPoint& Point = PathPoints[i]; 95 | UE_LOG(LogTemp, Error, TEXT(" Point %d: %s (Layer: %d)"), 96 | i, *Point.Position.ToString(), Point.Layer); 97 | 98 | // Check if any path point crosses the wall 99 | if (i > 0) 100 | { 101 | const FAeonixPathPoint& PrevPoint = PathPoints[i - 1]; 102 | 103 | // Check if we crossed from negative Y to positive Y (or vice versa) 104 | if ((PrevPoint.Position.Y < -WallCollision.WallThickness * 0.5f && 105 | Point.Position.Y > WallCollision.WallThickness * 0.5f) || 106 | (PrevPoint.Position.Y > WallCollision.WallThickness * 0.5f && 107 | Point.Position.Y < -WallCollision.WallThickness * 0.5f)) 108 | { 109 | UE_LOG(LogTemp, Error, TEXT(" >>> Path crosses wall between points %d and %d!"), i-1, i); 110 | } 111 | } 112 | } 113 | } 114 | else 115 | { 116 | UE_LOG(LogTemp, Display, TEXT("No path found (expected behavior - wall blocks the path)")); 117 | } 118 | 119 | // TEST EXPECTATION: There should be NO valid path through the wall 120 | // This test will FAIL initially, demonstrating the bug 121 | TestFalse(TEXT("No path should exist through the wall"), bPathFound); 122 | 123 | if (bPathFound) 124 | { 125 | AddError(TEXT("Pathfinder incorrectly found a path through a solid wall that splits the navigation volume!")); 126 | return false; 127 | } 128 | 129 | return true; 130 | } -------------------------------------------------------------------------------- /Source/AeonixGameUtils/Public/Component/AeonixAutopathComponent.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Pathfinding/AeonixNavigationPath.h" 4 | #include "Data/AeonixTypes.h" 5 | #include "Components/ActorComponent.h" 6 | 7 | #include "AeonixAutopathComponent.generated.h" 8 | 9 | class UAeonixAutopathSubsystem; 10 | class UAeonixNavAgentComponent; 11 | 12 | /** Delegate broadcast when path is updated (after async pathfinding completes) */ 13 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnAutopathUpdated, bool, bSuccess); 14 | 15 | /** 16 | * Component that automatically pathfinds to a target actor. 17 | * The component does not tick itself - instead the UAeonixAutopathSubsystem 18 | * tracks all registered sources and triggers pathfinding when source or target moves. 19 | * 20 | * Requires a UAeonixNavAgentComponent on the same actor or its controller for pathfinding offsets. 21 | */ 22 | UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent)) 23 | class AEONIXGAMEUTILS_API UAeonixAutopathComponent : public UActorComponent 24 | { 25 | GENERATED_BODY() 26 | 27 | public: 28 | UAeonixAutopathComponent(const FObjectInitializer& ObjectInitializer); 29 | 30 | /** Position threshold in cm - triggers pathfinding when source moved beyond this distance */ 31 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix|Autopath", meta = (ClampMin = "0.0")) 32 | float PositionThreshold = 50.0f; 33 | 34 | /** Acceptance radius in cm - advances to next waypoint when within this distance */ 35 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix|Autopath", meta = (ClampMin = "0.0")) 36 | float AcceptanceRadius = 50.0f; 37 | 38 | /** Whether this autopath source is active */ 39 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix|Autopath") 40 | bool bEnableAutopath = true; 41 | 42 | /** Event broadcast when path is updated (after async pathfinding completes) */ 43 | UPROPERTY(BlueprintAssignable, Category = "Aeonix|Autopath") 44 | FOnAutopathUpdated OnPathUpdated; 45 | 46 | /** 47 | * Get the full path as an array of positions. 48 | * Returns empty array if no path is available. 49 | */ 50 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Aeonix|Autopath") 51 | TArray GetPathPoints() const; 52 | 53 | /** 54 | * Get the next path point to navigate towards. 55 | * Automatically progresses through the path as the agent moves. 56 | * Returns ZeroVector if no path is available. 57 | */ 58 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Aeonix|Autopath") 59 | FVector GetNextPathPoint() const; 60 | 61 | /** 62 | * Check if the agent has reached the final destination. 63 | */ 64 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Aeonix|Autopath") 65 | bool HasReachedDestination() const; 66 | 67 | /** 68 | * Get the current waypoint index in the path. 69 | */ 70 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Aeonix|Autopath") 71 | int32 GetCurrentPathIndex() const { return CurrentPathIndex; } 72 | 73 | /** 74 | * Check if a valid path exists. 75 | */ 76 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Aeonix|Autopath") 77 | bool HasValidPath() const; 78 | 79 | /** 80 | * Get the number of points in the current path. 81 | */ 82 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Aeonix|Autopath") 83 | int32 GetNumPathPoints() const; 84 | 85 | /** 86 | * Get the path point at a specific index. 87 | * Returns false if index is out of bounds. 88 | */ 89 | UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Aeonix|Autopath") 90 | bool GetPathPointAtIndex(int32 Index, FVector& OutPosition) const; 91 | 92 | /** 93 | * Get the navigation path (internal use and subsystem access). 94 | */ 95 | FAeonixNavigationPath& GetNavigationPath() { return CurrentPath; } 96 | const FAeonixNavigationPath& GetNavigationPath() const { return CurrentPath; } 97 | 98 | /** 99 | * Get the nav agent component on this actor (for offset access). 100 | * Can be null if no nav agent component exists. 101 | */ 102 | UAeonixNavAgentComponent* GetNavAgentComponent() const { return CachedNavAgentComponent.Get(); } 103 | 104 | /** Called by subsystem when async pathfinding completes */ 105 | UFUNCTION() 106 | void OnPathFindComplete(EAeonixPathFindStatus Status); 107 | 108 | /** Mark that a path request is pending (called by subsystem) */ 109 | void SetPathRequestPending(bool bPending) { bPathRequestPending = bPending; } 110 | 111 | /** Check if a path request is pending */ 112 | bool IsPathRequestPending() const { return bPathRequestPending; } 113 | 114 | /** Called by subsystem to update path progression based on current position */ 115 | void UpdatePathProgression(const FVector& CurrentPosition); 116 | 117 | /** Reset path index to start of path */ 118 | void ResetPathIndex(); 119 | 120 | protected: 121 | virtual void OnRegister() override; 122 | virtual void OnUnregister() override; 123 | virtual void BeginPlay() override; 124 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 125 | 126 | private: 127 | /** Whether we're currently registered with the subsystem */ 128 | bool bRegisteredWithSubsystem = false; 129 | 130 | /** Current navigation path */ 131 | FAeonixNavigationPath CurrentPath{}; 132 | 133 | /** Current waypoint index in the path */ 134 | int32 CurrentPathIndex = 0; 135 | 136 | /** Cached reference to nav agent component on same actor */ 137 | TWeakObjectPtr CachedNavAgentComponent; 138 | 139 | /** Initialize and register with subsystem */ 140 | void RegisterWithSubsystem(); 141 | 142 | /** Unregister from subsystem */ 143 | void UnregisterFromSubsystem(); 144 | 145 | /** Cache the nav agent component reference */ 146 | void CacheNavAgentComponent(); 147 | 148 | /** Whether a path request is currently pending */ 149 | bool bPathRequestPending = false; 150 | }; 151 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Task/AITask_AeonixMoveTo.h: -------------------------------------------------------------------------------- 1 | // Copyright 1998-2018 Epic Games, Inc. All Rights Reserved. 2 | #pragma once 3 | 4 | #include "Subsystem/AeonixSubsystem.h" 5 | 6 | #include "Data/AeonixDefines.h" 7 | 8 | #include "Navigation/PathFollowingComponent.h" 9 | #include "Tasks/AITask.h" 10 | #include "HAL/ThreadSafeBool.h" 11 | 12 | #include "AITask_AeonixMoveTo.generated.h" 13 | 14 | class AAIController; 15 | class UAeonixNavAgentComponent; 16 | struct FAeonixNavigationPath; 17 | 18 | DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FAeonixMoveTaskCompletedSignature, TEnumAsByte, Result, AAIController*, AIController); 19 | 20 | UCLASS() 21 | class AEONIXNAVIGATION_API UAITask_AeonixMoveTo : public UAITask 22 | { 23 | GENERATED_BODY() 24 | 25 | public: 26 | UAITask_AeonixMoveTo(const FObjectInitializer& ObjectInitializer); 27 | 28 | /** tries to start move request and handles retry timer */ 29 | void ConditionalPerformMove(); 30 | 31 | /** prepare move task for activation */ 32 | void SetUp(AAIController* Controller, const FAIMoveRequest& InMoveRequest, bool aUseAsyncPathfinding); 33 | 34 | EPathFollowingResult::Type GetMoveResult() const { return MoveResult; } 35 | bool WasMoveSuccessful() const { return MoveResult == EPathFollowingResult::Success; } 36 | 37 | /** If true, automatically repath when path is invalidated by dynamic region changes. If false, abort the move. */ 38 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI|Aeonix") 39 | bool bRepathOnInvalidation = true; 40 | 41 | UFUNCTION(BlueprintCallable, Category = "AI|Tasks", meta = (AdvancedDisplay = "AcceptanceRadius,StopOnOverlap,AcceptPartialPath,bUsePathfinding,bUseContinuosGoalTracking,bInRepathOnInvalidation", DefaultToSelf = "Controller", BlueprintInternalUseOnly = "TRUE", DisplayName = "Aeonix Move To Location or Actor")) 42 | static UAITask_AeonixMoveTo* AeonixAIMoveTo(AAIController* Controller, FVector GoalLocation, bool aUseAsyncPathfinding, AActor* GoalActor = nullptr, 43 | float AcceptanceRadius = -1.f, EAIOptionFlag::Type StopOnOverlap = EAIOptionFlag::Default, bool bLockAILogic = true, bool bUseContinuosGoalTracking = false, bool bInRepathOnInvalidation = true); 44 | 45 | UE_DEPRECATED(4.12, "This function is now depreacted, please use version with FAIMoveRequest parameter") 46 | void SetUp(AAIController* Controller, FVector GoalLocation, AActor* GoalActor = nullptr, float AcceptanceRadius = -1.f, EAIOptionFlag::Type StopOnOverlap = EAIOptionFlag::Default); 47 | 48 | /** Allows custom move request tweaking. Note that all MoveRequest need to 49 | * be performed before PerformMove is called. */ 50 | FAIMoveRequest& GetMoveRequestRef() { return MoveRequest; } 51 | 52 | /** Switch task into continuous tracking mode: keep restarting move toward goal actor. Only pathfinding failure or external cancel will be able to stop this task. */ 53 | void SetContinuousGoalTracking(bool bEnable); 54 | 55 | void TickTask(float DeltaTime) override; 56 | 57 | UFUNCTION() 58 | void OnPathFindComplete(EAeonixPathFindStatus Status); 59 | 60 | protected: 61 | void LogPathHelper(); 62 | 63 | bool bUseAsyncPathfinding; 64 | 65 | UPROPERTY(BlueprintAssignable) 66 | FGenericGameplayTaskDelegate OnRequestFailed; 67 | 68 | UPROPERTY(BlueprintAssignable) 69 | FAeonixMoveTaskCompletedSignature OnMoveFinished; 70 | 71 | /** parameters of move request */ 72 | UPROPERTY() 73 | FAIMoveRequest MoveRequest; 74 | 75 | /** handle of path following's OnMoveFinished delegate */ 76 | FDelegateHandle PathFinishDelegateHandle; 77 | 78 | /** handle of path's update event delegate */ 79 | FDelegateHandle PathUpdateDelegateHandle; 80 | 81 | /** handle of path's invalidation delegate */ 82 | FDelegateHandle PathInvalidationDelegateHandle; 83 | 84 | /** handle of active ConditionalPerformMove timer */ 85 | FTimerHandle MoveRetryTimerHandle; 86 | 87 | /** handle of active ConditionalUpdatePath timer */ 88 | FTimerHandle PathRetryTimerHandle; 89 | 90 | /** request ID of path following's request */ 91 | FAIRequestID MoveRequestID; 92 | 93 | /** currently followed path */ 94 | FNavPathSharedPtr Path; 95 | 96 | FAeonixNavigationPath* AeonixPath; 97 | 98 | TScriptInterface AeonixSubsystem; 99 | 100 | TEnumAsByte MoveResult; 101 | bool bUseContinuousTracking{false}; 102 | 103 | virtual void Activate() override; 104 | virtual void OnDestroy(bool bOwnerFinished) override; 105 | 106 | virtual void Pause() override; 107 | virtual void Resume() override; 108 | 109 | /** finish task */ 110 | void FinishMoveTask(EPathFollowingResult::Type InResult); 111 | 112 | /** stores path and starts observing its events */ 113 | void SetObservedPath(FNavPathSharedPtr InPath); 114 | 115 | //FPathFollowingRequestResult myResult; 116 | 117 | FAeonixPathfindingRequestResult Result; 118 | 119 | UPROPERTY() 120 | TObjectPtr NavComponent; 121 | 122 | void CheckPathPreConditions(); 123 | 124 | void RequestPathSynchronous(); 125 | void RequestPathAsync(); 126 | 127 | void RequestMove(); 128 | 129 | void ResetPaths(); 130 | 131 | /** remove all delegates */ 132 | virtual void ResetObservers(); 133 | 134 | /** remove all timers */ 135 | virtual void ResetTimers(); 136 | 137 | /** tries to update invalidated path and handles retry timer */ 138 | void ConditionalUpdatePath(); 139 | 140 | /** start move request */ 141 | virtual void PerformMove(); 142 | 143 | /** event from followed path */ 144 | virtual void OnPathEvent(FNavigationPath* InPath, ENavPathEvent::Type Event); 145 | 146 | /** event from path invalidation (dynamic region regeneration) */ 147 | virtual void OnPathInvalidated(FAeonixNavigationPath* InvalidatedPath); 148 | 149 | /** event from path following */ 150 | virtual void OnRequestFinished(FAIRequestID RequestID, const FPathFollowingResult& Result); 151 | }; 152 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Data/AeonixThreading.h: -------------------------------------------------------------------------------- 1 | // Copyright 2024 Chris Ashworth 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "HAL/Runnable.h" 7 | #include "HAL/RunnableThread.h" 8 | #include "HAL/ThreadSafeBool.h" 9 | #include "Containers/Queue.h" 10 | #include "Misc/ScopeLock.h" 11 | #include 12 | 13 | #include "AeonixThreading.generated.h" 14 | 15 | class UAeonixSubsystem; 16 | 17 | /** 18 | * Priority levels for pathfinding requests 19 | */ 20 | UENUM() 21 | enum class EAeonixRequestPriority : uint8 22 | { 23 | Critical = 0 UMETA(DisplayName = "Critical"), // Player character, high-priority AI 24 | High = 1 UMETA(DisplayName = "High"), // Important AI 25 | Normal = 2 UMETA(DisplayName = "Normal"), // Regular AI 26 | Low = 3 UMETA(DisplayName = "Low") // Background/speculative paths 27 | }; 28 | 29 | /** 30 | * Load metrics for pathfinding and regeneration systems 31 | */ 32 | struct AEONIXNAVIGATION_API FAeonixLoadMetrics 33 | { 34 | std::atomic PendingPathfinds{0}; 35 | std::atomic ActivePathfinds{0}; 36 | std::atomic PendingRegenRegions{0}; 37 | std::atomic ActiveWriteLocks{0}; 38 | std::atomic CompletedPathfindsTotal{0}; 39 | std::atomic FailedPathfindsTotal{0}; 40 | std::atomic CancelledPathfindsTotal{0}; 41 | std::atomic InvalidatedPathsTotal{0}; 42 | 43 | TAtomic AveragePathfindTimeMs{0.0f}; 44 | TAtomic AverageRegenTimeMs{0.0f}; 45 | 46 | // Running average calculation helpers 47 | std::atomic PathfindSampleCount{0}; 48 | std::atomic RegenSampleCount{0}; 49 | 50 | /** Check if system is under heavy load */ 51 | bool ShouldThrottleNewRequests() const 52 | { 53 | return PendingPathfinds > 100 || ActiveWriteLocks > 0; 54 | } 55 | 56 | /** Get recommended delay before processing new requests */ 57 | float GetRecommendedDelay() const 58 | { 59 | if (PendingPathfinds > 50) return 0.1f; // High load 60 | if (PendingPathfinds > 20) return 0.05f; // Medium load 61 | return 0.0f; // Normal 62 | } 63 | 64 | /** Update average pathfind time with exponential moving average */ 65 | void UpdatePathfindTime(float NewTimeMs) 66 | { 67 | const float Alpha = 0.1f; // Smoothing factor 68 | float CurrentAvg = AveragePathfindTimeMs.Load(); 69 | float NewAvg = (Alpha * NewTimeMs) + ((1.0f - Alpha) * CurrentAvg); 70 | AveragePathfindTimeMs.Store(NewAvg); 71 | PathfindSampleCount.fetch_add(1); 72 | } 73 | 74 | /** Update average regen time with exponential moving average */ 75 | void UpdateRegenTime(float NewTimeMs) 76 | { 77 | const float Alpha = 0.1f; // Smoothing factor 78 | float CurrentAvg = AverageRegenTimeMs.Load(); 79 | float NewAvg = (Alpha * NewTimeMs) + ((1.0f - Alpha) * CurrentAvg); 80 | AverageRegenTimeMs.Store(NewAvg); 81 | RegenSampleCount.fetch_add(1); 82 | } 83 | 84 | /** Reset all counters */ 85 | void Reset() 86 | { 87 | PendingPathfinds = 0; 88 | ActivePathfinds = 0; 89 | PendingRegenRegions = 0; 90 | ActiveWriteLocks = 0; 91 | CompletedPathfindsTotal = 0; 92 | FailedPathfindsTotal = 0; 93 | CancelledPathfindsTotal = 0; 94 | InvalidatedPathsTotal = 0; 95 | AveragePathfindTimeMs = 0.0f; 96 | AverageRegenTimeMs = 0.0f; 97 | PathfindSampleCount = 0; 98 | RegenSampleCount = 0; 99 | } 100 | }; 101 | 102 | /** 103 | * Pathfinding worker thread that processes work from its own SPSC queue. 104 | * Each worker has a dedicated queue to avoid multi-consumer race conditions. 105 | */ 106 | class AEONIXNAVIGATION_API FAeonixPathfindWorker : public FRunnable 107 | { 108 | public: 109 | FAeonixPathfindWorker( 110 | TQueue, EQueueMode::Spsc>* InWorkQueue, // SPSC - single consumer per worker! 111 | FEvent* InWorkAvailableEvent, 112 | FThreadSafeBool* InShuttingDown, 113 | int32 InWorkerIndex); 114 | 115 | virtual ~FAeonixPathfindWorker(); 116 | 117 | // FRunnable interface 118 | virtual bool Init() override; 119 | virtual uint32 Run() override; 120 | virtual void Stop() override; 121 | virtual void Exit() override; 122 | 123 | private: 124 | TQueue, EQueueMode::Spsc>* WorkQueue; // SPSC - single consumer! 125 | FEvent* WorkAvailableEvent; 126 | FThreadSafeBool* bShuttingDown; 127 | int32 WorkerIndex; 128 | FThreadSafeBool bStopRequested; 129 | }; 130 | 131 | // Forward declaration 132 | struct FAeonixWorkerContext; 133 | 134 | /** 135 | * Context for a single pathfinding worker (queue + thread + event). 136 | * Each worker has its own SPSC queue to avoid the MPSC multi-consumer race condition. 137 | */ 138 | struct FAeonixWorkerContext 139 | { 140 | TUniquePtr Worker; 141 | FRunnableThread* Thread = nullptr; // Raw pointer - Kill() handles cleanup, destructor would double-free 142 | TQueue, EQueueMode::Spsc> WorkQueue; // SPSC - single consumer per worker! 143 | FEvent* WorkAvailableEvent = nullptr; 144 | }; 145 | 146 | /** 147 | * Worker pool with per-worker SPSC queues. 148 | * Game thread round-robins work distribution across workers. 149 | * This design fixes the MPSC race condition where multiple workers consumed from one queue. 150 | */ 151 | class AEONIXNAVIGATION_API FAeonixPathfindWorkerPool 152 | { 153 | public: 154 | FAeonixPathfindWorkerPool(); 155 | ~FAeonixPathfindWorkerPool(); 156 | 157 | /** Initialize the worker pool with a specific number of threads */ 158 | void Initialize(int32 NumThreads = 4); 159 | 160 | /** Enqueue work to be processed by worker threads (round-robin distribution) */ 161 | void EnqueueWork(TFunction&& Work); 162 | 163 | /** Shutdown the worker pool */ 164 | void Shutdown(); 165 | 166 | /** Check if pool is initialized */ 167 | bool IsInitialized() const { return bInitialized; } 168 | 169 | /** Get number of worker threads */ 170 | int32 GetNumWorkers() const { return WorkerContexts.Num(); } 171 | 172 | private: 173 | TArray WorkerContexts; 174 | std::atomic NextWorkerIndex{0}; // Round-robin counter for work distribution 175 | FThreadSafeBool bShuttingDown; 176 | bool bInitialized = false; 177 | }; 178 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Public/Actor/AeonixBoundingVolume.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "Data/AeonixData.h" 4 | #include "Data/AeonixAsyncRegen.h" 5 | #include "Interface/AeonixDebugDrawInterface.h" 6 | #include "Interface/AeonixSubsystemInterface.h" 7 | 8 | #include "GameFramework/Volume.h" 9 | 10 | #include "AeonixBoundingVolume.generated.h" 11 | 12 | // Forward declarations 13 | class AAeonixBoundingVolume; 14 | 15 | /** Delegate broadcast when navigation is regenerated (full or dynamic regions) */ 16 | DECLARE_MULTICAST_DELEGATE_OneParam(FOnNavigationRegenerated, AAeonixBoundingVolume*); 17 | 18 | /** 19 | * AeonixVolume is a bounding volume that forms a navigable area 20 | */ 21 | UCLASS(hidecategories = (Tags, Cooking, Actor, HLOD, Mobile, LOD)) 22 | class AEONIXNAVIGATION_API AAeonixBoundingVolume : public AVolume, public IAeonixDebugDrawInterface 23 | { 24 | GENERATED_BODY() 25 | 26 | public: 27 | 28 | AAeonixBoundingVolume(const FObjectInitializer& ObjectInitializer); 29 | 30 | //~ Begin AActor Interface 31 | void BeginPlay() override; 32 | void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 33 | bool ShouldTickIfViewportsOnly() const override { return true; } 34 | virtual void OnConstruction(const FTransform& Transform) override; 35 | virtual void Destroyed() override; 36 | //~ End AActor Interface 37 | 38 | //~ Begin UObject 39 | void Serialize(FArchive& Ar) override; 40 | //~ End UObject 41 | 42 | void UpdateBounds(); 43 | bool Generate(); 44 | void RegenerateDynamicRegions(); 45 | void RegenerateDynamicRegionsAsync(); 46 | void RegenerateDynamicRegion(const FGuid& RegionId); 47 | void RegenerateDynamicRegionAsync(const FGuid& RegionId); 48 | void RegenerateDynamicRegionsAsync(const TSet& RegionIds); 49 | bool HasData() const; 50 | void ClearData(); 51 | 52 | // Called by editor subsystem to set the debug filter box 53 | void SetDebugFilterBox(const FBox& FilterBox); 54 | void ClearDebugFilterBox(); 55 | 56 | // Called by modifier volumes to register dynamic regions 57 | void AddDynamicRegion(const FGuid& RegionId, const FBox& RegionBox); 58 | void RemoveDynamicRegion(const FGuid& RegionId); 59 | void ClearDynamicRegions(); 60 | 61 | // Validate that loaded dynamic regions match modifier volumes in the level 62 | void ValidateDynamicRegions(); 63 | 64 | // Called by dynamic obstacles to request regeneration (throttled) 65 | void RequestDynamicRegionRegen(const FGuid& RegionId); 66 | void TryProcessDirtyRegions(); 67 | 68 | // Called by async regen to enqueue results for time-budgeted processing 69 | void EnqueueRegenResults(TArray&& Results, int32 TotalLeaves); 70 | 71 | // Called by subsystem to process pending regeneration results with time budget 72 | void ProcessPendingRegenResults(float DeltaTime); 73 | 74 | const FAeonixData& GetNavData() const { return NavigationData; } 75 | FAeonixData& GetMutableNavData() { return NavigationData; } 76 | 77 | /** Check if a point is inside this volume using bounding box (more reliable than EncompassesPoint in PIE) */ 78 | bool IsPointInside(const FVector& Point) const; 79 | 80 | /** Get the read-write lock for thread-safe octree access */ 81 | FRWLock& GetOctreeDataLock() const { return OctreeDataLock; } 82 | 83 | /** Delegate broadcast when navigation is regenerated (full or dynamic regions) */ 84 | FOnNavigationRegenerated OnNavigationRegenerated; 85 | 86 | UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Aeonix") 87 | FAeonixGenerationParameters GenerationParameters; 88 | 89 | /** Minimum time between dynamic region regenerations (seconds) */ 90 | UPROPERTY(EditAnywhere, Category = "Aeonix|Dynamic", meta = (ClampMin = "0.0")) 91 | float DynamicRegenCooldown = 0.5f; 92 | 93 | /** Delay after marking a region dirty before processing it at runtime (allows physics to settle) */ 94 | UPROPERTY(EditAnywhere, Category = "Aeonix|Dynamic", meta = (ClampMin = "0.0", ClampMax = "2.0")) 95 | float DirtyRegionProcessDelay = 0.25f; 96 | 97 | /** Delay after marking a region dirty before processing it in editor (longer to allow editor overhead) */ 98 | UPROPERTY(EditAnywhere, Category = "Aeonix|Dynamic", meta = (ClampMin = "0.0", ClampMax = "5.0")) 99 | float EditorDirtyRegionProcessDelay = 1.0f; 100 | 101 | bool bIsReadyForNavigation{false}; 102 | 103 | private: 104 | // Flag to indicate that old format baked data was loaded and needs bounds update in BeginPlay 105 | bool bNeedsLegacyBoundsUpdate{false}; 106 | 107 | /** Read-write lock for thread-safe access to octree data during async pathfinding */ 108 | mutable FRWLock OctreeDataLock; 109 | 110 | /** Dirty regions awaiting regeneration (throttling) */ 111 | TSet DirtyRegionIds; 112 | 113 | /** Time when each region was marked dirty (used for processing delay) */ 114 | TMap DirtyRegionTimestamps; 115 | 116 | /** Time of last dynamic region regeneration */ 117 | double LastDynamicRegenTime = 0.0; 118 | 119 | /** Pending regeneration results awaiting time-budgeted application */ 120 | TArray PendingRegenResults; 121 | 122 | /** Index of next result to process from queue */ 123 | int32 NextResultIndexToProcess = 0; 124 | 125 | /** Total number of leaves in current regeneration batch (for progress tracking) */ 126 | int32 CurrentRegenTotalLeaves = 0; 127 | 128 | /** Start time of current async regeneration (for metrics tracking) */ 129 | double AsyncRegenStartTime = 0.0; 130 | 131 | /** Regions that are currently being regenerated (for path invalidation) */ 132 | TSet CurrentlyRegeneratingRegions; 133 | 134 | protected: 135 | FAeonixData NavigationData; 136 | 137 | UPROPERTY(Transient) 138 | TScriptInterface AeonixSubsystemInterface; 139 | UPROPERTY(Transient) 140 | TScriptInterface CollisionQueryInterface; 141 | 142 | // IAeonixDebugDrawInterface 143 | void AeonixDrawDebugString(const FVector& Position, const FString& String, const FColor& Color) const override; 144 | void AeonixDrawDebugBox(const FVector& Position, const float Size, const FColor& Color) const override; 145 | void AeonixDrawDebugLine(const FVector& Start, const FVector& End, const FColor& Color, float Thickness = 0.0f) const override; 146 | void AeonixDrawDebugDirectionalArrow(const FVector& Start, const FVector& End, const FColor& Color, float ArrowSize = 0.0f) const override; 147 | }; 148 | -------------------------------------------------------------------------------- /Source/AeonixNavigation/Private/Pathfinding/AeonixNavigationPath.cpp: -------------------------------------------------------------------------------- 1 | #include "Pathfinding/AeonixNavigationPath.h" 2 | 3 | #include "Debug/DebugDrawService.h" 4 | #include "Debug/AeonixDebugDrawManager.h" 5 | #include "DrawDebugHelpers.h" 6 | #include "NavigationData.h" 7 | #include "Actor/AeonixBoundingVolume.h" 8 | #include "AeonixNavigation.h" 9 | 10 | void FAeonixNavigationPath::AddPoint(const FAeonixPathPoint& aPoint) 11 | { 12 | myPoints.Add(aPoint); 13 | } 14 | 15 | void FAeonixNavigationPath::ResetForRepath() 16 | { 17 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("AeonixNavigationPath: ResetForRepath called, clearing %d points"), myPoints.Num()); 18 | myPoints.Empty(); 19 | myIsReady = false; 20 | } 21 | 22 | void FAeonixNavigationPath::DebugDraw(UWorld* World, const FAeonixData& Data) 23 | { 24 | UAeonixDebugDrawManager* DebugManager = World->GetSubsystem(); 25 | if (!DebugManager) 26 | { 27 | return; 28 | } 29 | 30 | // Log path endpoints for debugging 31 | if (myPoints.Num() > 0) 32 | { 33 | const FVector StartPoint = myPoints[0].Position; 34 | const FVector EndPoint = myPoints[myPoints.Num() - 1].Position; 35 | UE_LOG(LogAeonixNavigation, Log, TEXT("AeonixNavigationPath: Start=%s, End=%s, NumPoints=%d"), 36 | *StartPoint.ToCompactString(), *EndPoint.ToCompactString(), myPoints.Num()); 37 | } 38 | 39 | // Draw the final optimized path with blue spheres and connecting lines 40 | for (int i = 0; i < myPoints.Num(); i++) 41 | { 42 | FAeonixPathPoint& point = myPoints[i]; 43 | 44 | FColor PointColor = FColor::Blue; 45 | if (i == 0) 46 | { 47 | PointColor = FColor::Green; 48 | } 49 | else if (i == myPoints.Num() - 1) 50 | { 51 | PointColor = FColor::Red; 52 | } 53 | 54 | // Draw sphere at the actual path point position (which may be smoothed) 55 | DebugManager->AddSphere(point.Position, 30.f, 20, PointColor, EAeonixDebugCategory::Paths); 56 | 57 | // Draw lines connecting path points 58 | if (i < myPoints.Num() - 1) 59 | { 60 | DebugManager->AddLine(point.Position, myPoints[i+1].Position, FColor::Cyan, 10.f, EAeonixDebugCategory::Paths); 61 | } 62 | } 63 | 64 | #if WITH_EDITOR 65 | // Draw the original voxel positions from the debug array 66 | if (myDebugVoxelInfo.Num() > 0) 67 | { 68 | // Log extents and parameters for debugging rendering issues 69 | const FAeonixGenerationParameters& Params = Data.GetParams(); 70 | UE_LOG(LogAeonixNavigation, Log, TEXT("AeonixNavigationPath Debug Voxels: Origin=%s, Extents=%s, OctreeDepth=%d"), 71 | *Params.Origin.ToCompactString(), *Params.Extents.ToCompactString(), Params.OctreeDepth); 72 | 73 | // Cache the array size to avoid issues if array changes during iteration 74 | const int32 NumVoxels = myDebugVoxelInfo.Num(); 75 | 76 | for (int32 i = 0; i < NumVoxels; i++) 77 | { 78 | // Bounds check to prevent crashes 79 | if (!myDebugVoxelInfo.IsValidIndex(i)) 80 | { 81 | UE_LOG(LogAeonixNavigation, Warning, TEXT("DebugDraw: Invalid index %d for debug voxel info array of size %d"), i, myDebugVoxelInfo.Num()); 82 | break; 83 | } 84 | 85 | const FDebugVoxelInfo& voxelInfo = myDebugVoxelInfo[i]; 86 | 87 | // Determine color based on position in the array 88 | FColor boxColor; 89 | if (i == 0) // First voxel (target position) 90 | { 91 | boxColor = FColor::Yellow; 92 | } 93 | else if (i == NumVoxels - 1) // Last voxel (start position) 94 | { 95 | boxColor = FColor::Green; 96 | } 97 | else // Intermediate voxels 98 | { 99 | // Validate layer index before using it (myLinkColors has 8 elements) 100 | if (voxelInfo.Layer >= 0 && voxelInfo.Layer < 8) 101 | { 102 | boxColor = voxelInfo.Layer > 0 ? AeonixStatics::myLinkColors[voxelInfo.Layer] : FColor::Red; 103 | } 104 | else 105 | { 106 | boxColor = FColor::Red; // Default to red for invalid layers 107 | } 108 | } 109 | 110 | // Calculate size with bounds checking on layer 111 | float size = 50.0f; // Default size 112 | if (voxelInfo.Layer >= 0) 113 | { 114 | const float rawVoxelSize = Data.GetVoxelSize(voxelInfo.Layer); 115 | size = voxelInfo.Layer == 0 ? 116 | rawVoxelSize * 0.125f : 117 | rawVoxelSize * 0.25f; 118 | 119 | // Log voxel size information for the first few voxels to avoid spam 120 | if (i < 3) 121 | { 122 | UE_LOG(LogAeonixNavigation, Log, TEXT(" Voxel[%d]: Layer=%d, RawSize=%.2f, RenderSize=%.2f, Position=%s"), 123 | i, voxelInfo.Layer, rawVoxelSize, size, *voxelInfo.Position.ToCompactString()); 124 | } 125 | } 126 | 127 | // Draw a box representing the original voxel 128 | DebugManager->AddBox(voxelInfo.Position, FVector(size), FQuat::Identity, boxColor, EAeonixDebugCategory::Paths); 129 | } 130 | } 131 | #endif 132 | } 133 | 134 | void FAeonixNavigationPath::DebugDrawLite(UWorld* World, const FColor& LineColor, float LifeTime) const 135 | { 136 | if (!World || myPoints.Num() < 2) 137 | { 138 | return; 139 | } 140 | 141 | UAeonixDebugDrawManager* DebugManager = World->GetSubsystem(); 142 | if (!DebugManager) 143 | { 144 | return; 145 | } 146 | 147 | // Draw simple lines connecting all path points 148 | for (int32 i = 0; i < myPoints.Num() - 1; i++) 149 | { 150 | DebugManager->AddLine(myPoints[i].Position, myPoints[i + 1].Position, LineColor, 2.0f, EAeonixDebugCategory::Paths); 151 | } 152 | } 153 | 154 | void FAeonixNavigationPath::CreateNavPath(FNavigationPath& aOutPath) 155 | { 156 | for (const FAeonixPathPoint& point : myPoints) 157 | { 158 | aOutPath.GetPathPoints().Add(point.Position); 159 | } 160 | } 161 | 162 | void FAeonixNavigationPath::AddTraversedRegion(const FGuid& RegionId) 163 | { 164 | TraversedRegionIds.Add(RegionId); 165 | } 166 | 167 | bool FAeonixNavigationPath::CheckInvalidation(const TSet& RegeneratedRegions) const 168 | { 169 | // Check if any of our traversed regions were regenerated 170 | for (const FGuid& RegionId : TraversedRegionIds) 171 | { 172 | if (RegeneratedRegions.Contains(RegionId)) 173 | { 174 | return true; 175 | } 176 | } 177 | return false; 178 | } 179 | 180 | void FAeonixNavigationPath::MarkInvalid() 181 | { 182 | if (bIsValid) 183 | { 184 | bIsValid = false; 185 | UE_LOG(LogAeonixNavigation, Verbose, TEXT("Path invalidated - %d regions traversed"), TraversedRegionIds.Num()); 186 | OnPathInvalidated.Broadcast(this); 187 | } 188 | } 189 | --------------------------------------------------------------------------------