From 8333bc641b305aec024d2f971104eafa31ff7b68 Mon Sep 17 00:00:00 2001 From: winyunq Date: Sun, 21 Sep 2025 22:50:02 +0800 Subject: [PATCH 01/12] feat: Unify camera speed and implement zoom-based interpolation --- Content/AC_RTSCameraFollowMe.uasset | 3 --- Content/BP_RTSCamera.uasset | 4 +-- OpenRTSCamera.uplugin | 2 +- Source/OpenRTSCamera/Private/RTSCamera.cpp | 27 +++++++++++++------ .../OpenRTSCamera/Private/RTSSelectable.cpp | 2 +- Source/OpenRTSCamera/Public/OpenRTSCamera.h | 2 +- Source/OpenRTSCamera/Public/RTSCamera.h | 17 ++++++------ .../Public/RTSCameraBoundsVolume.h | 2 +- Source/OpenRTSCamera/Public/RTSHUD.h | 3 +-- Source/OpenRTSCamera/Public/RTSSelectable.h | 2 +- Source/OpenRTSCamera/Public/RTSSelector.h | 2 +- 11 files changed, 36 insertions(+), 30 deletions(-) delete mode 100644 Content/AC_RTSCameraFollowMe.uasset diff --git a/Content/AC_RTSCameraFollowMe.uasset b/Content/AC_RTSCameraFollowMe.uasset deleted file mode 100644 index cd63114..0000000 --- a/Content/AC_RTSCameraFollowMe.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1dceb093351e41cfe099ef87bdbfec3c42af2de2fd1fd821ea17d523df404a6b -size 25843 diff --git a/Content/BP_RTSCamera.uasset b/Content/BP_RTSCamera.uasset index 5682393..e90361f 100644 --- a/Content/BP_RTSCamera.uasset +++ b/Content/BP_RTSCamera.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad0d80f14898ade77d58d545aeb3150e1acc8287d0322c24aa7442cd98dea54e -size 25195 +oid sha256:0d2e201517cc64e3e555b53bdec3f1705cb8590341e6f9d4c0f9b7448cd39a6c +size 27303 diff --git a/OpenRTSCamera.uplugin b/OpenRTSCamera.uplugin index 27e74e4..8aa1b38 100644 --- a/OpenRTSCamera.uplugin +++ b/OpenRTSCamera.uplugin @@ -6,7 +6,7 @@ "Description": "An Unreal Engine 5 open-source RTS/MOBA camera implementation that aims to be fully featured, customizable and dependable", "DocsURL": "https://github.com/HeyZoos/OpenRTSCamera/wiki", "EnabledByDefault": true, - "EngineVersion": "5.3.0", + "EngineVersion": "5.5.0", "FileVersion": 3, "FriendlyName": "OpenRTSCamera", "Installed": false, diff --git a/Source/OpenRTSCamera/Private/RTSCamera.cpp b/Source/OpenRTSCamera/Private/RTSCamera.cpp index e5606c8..fca62cb 100644 --- a/Source/OpenRTSCamera/Private/RTSCamera.cpp +++ b/Source/OpenRTSCamera/Private/RTSCamera.cpp @@ -17,7 +17,6 @@ URTSCamera::URTSCamera() this->CameraBlockingVolumeTag = FName("OpenRTSCamera#CameraBounds"); this->CollisionChannel = ECC_WorldStatic; this->DragExtent = 0.6f; - this->EdgeScrollSpeed = 50; this->DistanceFromEdgeThreshold = 0.1f; this->EnableCameraLag = true; this->EnableCameraRotationLag = true; @@ -26,7 +25,9 @@ URTSCamera::URTSCamera() this->FindGroundTraceLength = 100000; this->MaximumZoomLength = 5000; this->MinimumZoomLength = 500; - this->MoveSpeed = 50; + this->MaxMoveSpeed = 1024.0f; + this->MinMoveSpeed = 128.0f; + this->NowMoveSpeed = this->MinMoveSpeed; // Initialize NowMoveSpeed to MinMoveSpeed this->RotateSpeed = 45; this->StartingYAngle = -45.0f; this->StartingZAngle = 0; @@ -74,6 +75,8 @@ void URTSCamera::BeginPlay() this->CheckForEnhancedInputComponent(); this->BindInputMappingContext(); this->BindInputActions(); + + } } @@ -88,6 +91,9 @@ void URTSCamera::TickComponent( if (NetMode != NM_DedicatedServer && this->PlayerController->GetViewTarget() == this->Owner) { this->DeltaSeconds = DeltaTime; + + + this->ApplyMoveCameraCommands(); this->ConditionallyPerformEdgeScrolling(); this->ConditionallyKeepCameraAtDesiredZoomAboveGround(); @@ -96,7 +102,6 @@ void URTSCamera::TickComponent( this->ConditionallyApplyCameraBounds(); } } - void URTSCamera::FollowTarget(AActor* Target) { this->CameraFollowTarget = Target; @@ -114,6 +119,12 @@ void URTSCamera::OnZoomCamera(const FInputActionValue& Value) this->MinimumZoomLength, this->MaximumZoomLength ); + + // Calculate Alpha for Lerp (0 at min zoom, 1 at max zoom) + float Alpha = (this->DesiredZoomLength - this->MinimumZoomLength) / (this->MaximumZoomLength - this->MinimumZoomLength); + + // Lerp NowMoveSpeed between MinMoveSpeed and MaxMoveSpeed + this->NowMoveSpeed = FMath::Lerp(this->MinMoveSpeed, this->MaxMoveSpeed, Alpha); } void URTSCamera::OnRotateCamera(const FInputActionValue& Value) @@ -228,7 +239,7 @@ void URTSCamera::ApplyMoveCameraCommands() { auto Movement = FVector2D(X, Y); Movement.Normalize(); - Movement *= this->MoveSpeed * Scale * this->DeltaSeconds; + Movement *= this->NowMoveSpeed * Scale * this->DeltaSeconds; this->Root->SetWorldLocation( this->Root->GetComponentLocation() + FVector(Movement.X, Movement.Y, 0.0f) ); @@ -424,7 +435,7 @@ void URTSCamera::EdgeScrollLeft() const const auto Movement = UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); this->Root->AddRelativeLocation( - -1 * this->Root->GetRightVector() * Movement * this->EdgeScrollSpeed * this->DeltaSeconds + -1 * this->Root->GetRightVector() * Movement * this->NowMoveSpeed * this->DeltaSeconds ); } @@ -440,7 +451,7 @@ void URTSCamera::EdgeScrollRight() const const auto Movement = UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); this->Root->AddRelativeLocation( - this->Root->GetRightVector() * Movement * this->EdgeScrollSpeed * this->DeltaSeconds + this->Root->GetRightVector() * Movement * this->NowMoveSpeed * this->DeltaSeconds ); } @@ -456,7 +467,7 @@ void URTSCamera::EdgeScrollUp() const const auto Movement = 1 - UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); this->Root->AddRelativeLocation( - this->Root->GetForwardVector() * Movement * this->EdgeScrollSpeed * this->DeltaSeconds + this->Root->GetForwardVector() * Movement * this->NowMoveSpeed * this->DeltaSeconds ); } @@ -472,7 +483,7 @@ void URTSCamera::EdgeScrollDown() const const auto Movement = UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); this->Root->AddRelativeLocation( - -1 * this->Root->GetForwardVector() * Movement * this->EdgeScrollSpeed * this->DeltaSeconds + -1 * this->Root->GetForwardVector() * Movement * this->NowMoveSpeed * this->DeltaSeconds ); } diff --git a/Source/OpenRTSCamera/Private/RTSSelectable.cpp b/Source/OpenRTSCamera/Private/RTSSelectable.cpp index 56c0479..01304ae 100644 --- a/Source/OpenRTSCamera/Private/RTSSelectable.cpp +++ b/Source/OpenRTSCamera/Private/RTSSelectable.cpp @@ -1 +1 @@ -#include "RTSSelectable.h" +#include "RTSSelectable.h" diff --git a/Source/OpenRTSCamera/Public/OpenRTSCamera.h b/Source/OpenRTSCamera/Public/OpenRTSCamera.h index 9e39ca2..0e6163d 100644 --- a/Source/OpenRTSCamera/Public/OpenRTSCamera.h +++ b/Source/OpenRTSCamera/Public/OpenRTSCamera.h @@ -2,7 +2,7 @@ #pragma once -#include "CoreMinimal.h" +#include #include "Modules/ModuleManager.h" class FOpenRTSCameraModule : public IModuleInterface diff --git a/Source/OpenRTSCamera/Public/RTSCamera.h b/Source/OpenRTSCamera/Public/RTSCamera.h index bb579d2..25eca16 100644 --- a/Source/OpenRTSCamera/Public/RTSCamera.h +++ b/Source/OpenRTSCamera/Public/RTSCamera.h @@ -2,7 +2,7 @@ #pragma once -#include "CoreMinimal.h" +#include #include "InputMappingContext.h" #include "Camera/CameraComponent.h" #include "Components/ActorComponent.h" @@ -66,7 +66,9 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent float StartingZAngle; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float MoveSpeed; + float MaxMoveSpeed; + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") + float MinMoveSpeed; UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") float RotateSpeed; @@ -104,13 +106,8 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Edge Scroll Settings") bool EnableEdgeScrolling; - UPROPERTY( - BlueprintReadWrite, - EditAnywhere, - Category = "RTSCamera - Edge Scroll Settings", - meta=(EditCondition="EnableEdgeScrolling") - ) - float EdgeScrollSpeed; + + UPROPERTY( BlueprintReadWrite, EditAnywhere, @@ -199,4 +196,6 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent FVector2D DragStartLocation; UPROPERTY() TArray MoveCameraCommands; + UPROPERTY() + float NowMoveSpeed; }; diff --git a/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h b/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h index 55a3a0d..33ce6ad 100644 --- a/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h +++ b/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h @@ -2,7 +2,7 @@ #pragma once -#include "CoreMinimal.h" +#include #include "GameFramework/CameraBlockingVolume.h" #include "RTSCameraBoundsVolume.generated.h" diff --git a/Source/OpenRTSCamera/Public/RTSHUD.h b/Source/OpenRTSCamera/Public/RTSHUD.h index 694685c..e4215a8 100644 --- a/Source/OpenRTSCamera/Public/RTSHUD.h +++ b/Source/OpenRTSCamera/Public/RTSHUD.h @@ -2,7 +2,7 @@ #pragma once -#include "CoreMinimal.h" +#include #include "GameFramework/HUD.h" #include "RTSHUD.generated.h" @@ -13,7 +13,6 @@ class OPENRTSCAMERA_API ARTSHUD : public AHUD public: ARTSHUD(); - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Selection Box") FLinearColor SelectionBoxColor; diff --git a/Source/OpenRTSCamera/Public/RTSSelectable.h b/Source/OpenRTSCamera/Public/RTSSelectable.h index 412c59e..dc7420b 100644 --- a/Source/OpenRTSCamera/Public/RTSSelectable.h +++ b/Source/OpenRTSCamera/Public/RTSSelectable.h @@ -1,4 +1,4 @@ -#pragma once +#pragma once #include "RTSSelectable.generated.h" UCLASS(Blueprintable, ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) diff --git a/Source/OpenRTSCamera/Public/RTSSelector.h b/Source/OpenRTSCamera/Public/RTSSelector.h index 4d164b5..da7a1b7 100644 --- a/Source/OpenRTSCamera/Public/RTSSelector.h +++ b/Source/OpenRTSCamera/Public/RTSSelector.h @@ -2,7 +2,7 @@ #pragma once -#include "CoreMinimal.h" +#include #include "InputAction.h" #include "InputMappingContext.h" #include "RTSHUD.h" From c13250fbec5867f9379136225bce2ab203cc7c9e Mon Sep 17 00:00:00 2001 From: winyunq Date: Sat, 27 Sep 2025 15:52:21 +0800 Subject: [PATCH 02/12] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=BA=86=E5=88=9D?= =?UTF-8?q?=E5=A7=8B=E9=95=9C=E5=A4=B4=E4=BD=8D=E7=BD=AE=EF=BC=8C=E4=B8=8D?= =?UTF-8?q?=E9=9D=A0=E8=B0=B1=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/OpenRTSCamera/Private/RTSCamera.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Source/OpenRTSCamera/Private/RTSCamera.cpp b/Source/OpenRTSCamera/Private/RTSCamera.cpp index fca62cb..e391fd5 100644 --- a/Source/OpenRTSCamera/Private/RTSCamera.cpp +++ b/Source/OpenRTSCamera/Private/RTSCamera.cpp @@ -259,7 +259,7 @@ void URTSCamera::CollectComponentDependencyReferences() void URTSCamera::ConfigureSpringArm() { - this->DesiredZoomLength = this->MaximumZoomLength; + this->DesiredZoomLength = this->MinimumZoomLength; this->SpringArm->TargetArmLength = this->DesiredZoomLength; this->SpringArm->bDoCollisionTest = false; this->SpringArm->bEnableCameraLag = this->EnableCameraLag; From 0f03a11e9a6bb771aebca5798cd969d685b1c461 Mon Sep 17 00:00:00 2001 From: winyunq Date: Thu, 23 Oct 2025 15:31:36 +0800 Subject: [PATCH 03/12] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E9=99=90=E5=88=B6=EF=BC=8C=E5=87=86=E5=A4=87=E9=92=88=E5=AF=B9?= =?UTF-8?q?5.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenRTSCamera.uplugin | 1 - 1 file changed, 1 deletion(-) diff --git a/OpenRTSCamera.uplugin b/OpenRTSCamera.uplugin index 8aa1b38..4793bc1 100644 --- a/OpenRTSCamera.uplugin +++ b/OpenRTSCamera.uplugin @@ -6,7 +6,6 @@ "Description": "An Unreal Engine 5 open-source RTS/MOBA camera implementation that aims to be fully featured, customizable and dependable", "DocsURL": "https://github.com/HeyZoos/OpenRTSCamera/wiki", "EnabledByDefault": true, - "EngineVersion": "5.5.0", "FileVersion": 3, "FriendlyName": "OpenRTSCamera", "Installed": false, From c633fc4d56bf38a3f512ecca6662dca1ea5eda04 Mon Sep 17 00:00:00 2001 From: winyunq Date: Sat, 14 Feb 2026 17:22:01 +0800 Subject: [PATCH 04/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0RTS=E9=95=9C=E5=A4=B4?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=9C=A8=E5=B0=8F=E5=9C=B0=E5=9B=BE=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/OpenRTSCamera/OpenRTSCamera.Build.cs | 3 +- .../OpenRTSCamera/Private/OpenRTSCamera.cpp | 2 + Source/OpenRTSCamera/Private/RTSCamera.cpp | 358 ++++++++++-------- .../Private/RTSCameraBoundsVolume.cpp | 13 +- .../Private/UI/RTSCameraMinimapWidget.cpp | 259 +++++++++++++ Source/OpenRTSCamera/Public/OpenRTSCamera.h | 2 + Source/OpenRTSCamera/Public/RTSCamera.h | 79 +++- .../Public/RTSCameraBoundsVolume.h | 4 + .../Public/UI/RTSCameraMinimapWidget.h | 75 ++++ 9 files changed, 617 insertions(+), 178 deletions(-) create mode 100644 Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp create mode 100644 Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h diff --git a/Source/OpenRTSCamera/OpenRTSCamera.Build.cs b/Source/OpenRTSCamera/OpenRTSCamera.Build.cs index 1d91624..c60f199 100644 --- a/Source/OpenRTSCamera/OpenRTSCamera.Build.cs +++ b/Source/OpenRTSCamera/OpenRTSCamera.Build.cs @@ -35,7 +35,8 @@ public OpenRTSCamera(ReadOnlyTargetRules Target) : base(Target) "EnhancedInput", "Slate", "SlateCore", - "UMG" + "UMG", + "InputCore" } ); diff --git a/Source/OpenRTSCamera/Private/OpenRTSCamera.cpp b/Source/OpenRTSCamera/Private/OpenRTSCamera.cpp index 875cb53..e73c357 100644 --- a/Source/OpenRTSCamera/Private/OpenRTSCamera.cpp +++ b/Source/OpenRTSCamera/Private/OpenRTSCamera.cpp @@ -4,6 +4,8 @@ #define LOCTEXT_NAMESPACE "FOpenRTSCameraModule" +DEFINE_LOG_CATEGORY(LogOpenRTSCamera); + void FOpenRTSCameraModule::StartupModule() { // This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module diff --git a/Source/OpenRTSCamera/Private/RTSCamera.cpp b/Source/OpenRTSCamera/Private/RTSCamera.cpp index e391fd5..37dacb2 100644 --- a/Source/OpenRTSCamera/Private/RTSCamera.cpp +++ b/Source/OpenRTSCamera/Private/RTSCamera.cpp @@ -13,6 +13,7 @@ URTSCamera::URTSCamera() { + /// 开启组件更新并设置默认相机策略参数 PrimaryComponentTick.bCanEverTick = true; this->CameraBlockingVolumeTag = FName("OpenRTSCamera#CameraBounds"); this->CollisionChannel = ECC_WorldStatic; @@ -27,29 +28,22 @@ URTSCamera::URTSCamera() this->MinimumZoomLength = 500; this->MaxMoveSpeed = 1024.0f; this->MinMoveSpeed = 128.0f; - this->NowMoveSpeed = this->MinMoveSpeed; // Initialize NowMoveSpeed to MinMoveSpeed + this->CurrentMovementSpeed = this->MinMoveSpeed; this->RotateSpeed = 45; this->StartingYAngle = -45.0f; this->StartingZAngle = 0; this->ZoomCatchupSpeed = 4; this->ZoomSpeed = -200; - static ConstructorHelpers::FObjectFinder - MoveCameraXAxisFinder(TEXT("/OpenRTSCamera/Inputs/MoveCameraXAxis")); - static ConstructorHelpers::FObjectFinder - MoveCameraYAxisFinder(TEXT("/OpenRTSCamera/Inputs/MoveCameraYAxis")); - static ConstructorHelpers::FObjectFinder - RotateCameraAxisFinder(TEXT("/OpenRTSCamera/Inputs/RotateCameraAxis")); - static ConstructorHelpers::FObjectFinder - TurnCameraLeftFinder(TEXT("/OpenRTSCamera/Inputs/TurnCameraLeft")); - static ConstructorHelpers::FObjectFinder - TurnCameraRightFinder(TEXT("/OpenRTSCamera/Inputs/TurnCameraRight")); - static ConstructorHelpers::FObjectFinder - ZoomCameraFinder(TEXT("/OpenRTSCamera/Inputs/ZoomCamera")); - static ConstructorHelpers::FObjectFinder - DragCameraFinder(TEXT("/OpenRTSCamera/Inputs/DragCamera")); - static ConstructorHelpers::FObjectFinder - InputMappingContextFinder(TEXT("/OpenRTSCamera/Inputs/OpenRTSCameraInputs")); + /// 加载并绑定 Enhanced Input 相关资源 + static ConstructorHelpers::FObjectFinder MoveCameraXAxisFinder(TEXT("/OpenRTSCamera/Inputs/MoveCameraXAxis")); + static ConstructorHelpers::FObjectFinder MoveCameraYAxisFinder(TEXT("/OpenRTSCamera/Inputs/MoveCameraYAxis")); + static ConstructorHelpers::FObjectFinder RotateCameraAxisFinder(TEXT("/OpenRTSCamera/Inputs/RotateCameraAxis")); + static ConstructorHelpers::FObjectFinder TurnCameraLeftFinder(TEXT("/OpenRTSCamera/Inputs/TurnCameraLeft")); + static ConstructorHelpers::FObjectFinder TurnCameraRightFinder(TEXT("/OpenRTSCamera/Inputs/TurnCameraRight")); + static ConstructorHelpers::FObjectFinder ZoomCameraFinder(TEXT("/OpenRTSCamera/Inputs/ZoomCamera")); + static ConstructorHelpers::FObjectFinder DragCameraFinder(TEXT("/OpenRTSCamera/Inputs/DragCamera")); + static ConstructorHelpers::FObjectFinder InputMappingContextFinder(TEXT("/OpenRTSCamera/Inputs/OpenRTSCameraInputs")); this->MoveCameraXAxis = MoveCameraXAxisFinder.Object; this->MoveCameraYAxis = MoveCameraYAxisFinder.Object; @@ -68,6 +62,7 @@ void URTSCamera::BeginPlay() const auto NetMode = this->GetNetMode(); if (NetMode != NM_DedicatedServer) { + /// 初始化依赖组件、配置弹簧臂并寻找地图边界 this->CollectComponentDependencyReferences(); this->ConfigureSpringArm(); this->TryToFindBoundaryVolumeReference(); @@ -75,8 +70,6 @@ void URTSCamera::BeginPlay() this->CheckForEnhancedInputComponent(); this->BindInputMappingContext(); this->BindInputActions(); - - } } @@ -88,12 +81,11 @@ void URTSCamera::TickComponent( { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); const auto NetMode = this->GetNetMode(); - if (NetMode != NM_DedicatedServer && this->PlayerController->GetViewTarget() == this->Owner) + + /// 仅在非专用服务器且当前相机为活跃视图时执行状态更新 (移动、缩放、边界检查等) + if (NetMode != NM_DedicatedServer && this->RTSPlayerController->GetViewTarget() == this->CameraOwner) { this->DeltaSeconds = DeltaTime; - - - this->ApplyMoveCameraCommands(); this->ConditionallyPerformEdgeScrolling(); this->ConditionallyKeepCameraAtDesiredZoomAboveGround(); @@ -114,23 +106,26 @@ void URTSCamera::UnFollowTarget() void URTSCamera::OnZoomCamera(const FInputActionValue& Value) { + /// 更新意图缩放距离 this->DesiredZoomLength = FMath::Clamp( this->DesiredZoomLength + Value.Get() * this->ZoomSpeed, this->MinimumZoomLength, this->MaximumZoomLength ); - // Calculate Alpha for Lerp (0 at min zoom, 1 at max zoom) + /// 计算缩放比例并动态调整移动感官速度 float Alpha = (this->DesiredZoomLength - this->MinimumZoomLength) / (this->MaximumZoomLength - this->MinimumZoomLength); - - // Lerp NowMoveSpeed between MinMoveSpeed and MaxMoveSpeed - this->NowMoveSpeed = FMath::Lerp(this->MinMoveSpeed, this->MaxMoveSpeed, Alpha); + this->CurrentMovementSpeed = FMath::Lerp(this->MinMoveSpeed, this->MaxMoveSpeed, Alpha); + + /// 基于意图高度即时刷新小地图视野框 + this->UpdateMinimapFrustum(); } void URTSCamera::OnRotateCamera(const FInputActionValue& Value) { - const auto WorldRotation = this->Root->GetComponentRotation(); - this->Root->SetWorldRotation( + /// 获取当前根组件旋转并应用水平旋转增量 + const auto WorldRotation = this->RootComponent->GetComponentRotation(); + this->RootComponent->SetWorldRotation( FRotator::MakeFromEuler( FVector( WorldRotation.Euler().X, @@ -139,12 +134,14 @@ void URTSCamera::OnRotateCamera(const FInputActionValue& Value) ) ) ); + this->UpdateMinimapFrustum(); } void URTSCamera::OnTurnCameraLeft(const FInputActionValue&) { - const auto WorldRotation = this->Root->GetRelativeRotation(); - this->Root->SetRelativeRotation( + /// 向左平滑旋转相机 + const auto WorldRotation = this->RootComponent->GetRelativeRotation(); + this->RootComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( WorldRotation.Euler().X, @@ -153,12 +150,14 @@ void URTSCamera::OnTurnCameraLeft(const FInputActionValue&) ) ) ); + this->UpdateMinimapFrustum(); } void URTSCamera::OnTurnCameraRight(const FInputActionValue&) { - const auto WorldRotation = this->Root->GetRelativeRotation(); - this->Root->SetRelativeRotation( + /// 向右平滑旋转相机 + const auto WorldRotation = this->RootComponent->GetRelativeRotation(); + this->RootComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( WorldRotation.Euler().X, @@ -167,34 +166,39 @@ void URTSCamera::OnTurnCameraRight(const FInputActionValue&) ) ) ); + this->UpdateMinimapFrustum(); } void URTSCamera::OnMoveCameraYAxis(const FInputActionValue& Value) { + /// 根据相机当前朝向请求垂直(Y轴)移动指令 this->RequestMoveCamera( - this->SpringArm->GetForwardVector().X, - this->SpringArm->GetForwardVector().Y, + this->SpringArmComponent->GetForwardVector().X, + this->SpringArmComponent->GetForwardVector().Y, Value.Get() ); } void URTSCamera::OnMoveCameraXAxis(const FInputActionValue& Value) { + /// 根据相机当前朝向请求水平(X轴)移动指令 this->RequestMoveCamera( - this->SpringArm->GetRightVector().X, - this->SpringArm->GetRightVector().Y, + this->SpringArmComponent->GetRightVector().X, + this->SpringArmComponent->GetRightVector().Y, Value.Get() ); } void URTSCamera::OnDragCamera(const FInputActionValue& Value) { + /// 开始拖拽:记录初始鼠标位置 if (!this->IsDragging && Value.Get()) { this->IsDragging = true; this->DragStartLocation = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); } + /// 拖拽中:计算位置增量并映射到移动指令 else if (this->IsDragging && Value.Get()) { const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); @@ -206,18 +210,19 @@ void URTSCamera::OnDragCamera(const FInputActionValue& Value) Delta.Y = FMath::Clamp(Delta.Y, -DragExtents.Y, DragExtents.Y) / DragExtents.Y; this->RequestMoveCamera( - this->SpringArm->GetRightVector().X, - this->SpringArm->GetRightVector().Y, + this->SpringArmComponent->GetRightVector().X, + this->SpringArmComponent->GetRightVector().Y, Delta.X ); this->RequestMoveCamera( - this->SpringArm->GetForwardVector().X, - this->SpringArm->GetForwardVector().Y, + this->SpringArmComponent->GetForwardVector().X, + this->SpringArmComponent->GetForwardVector().Y, Delta.Y * -1 ); } + /// 停止拖拽 else if (this->IsDragging && !Value.Get()) { this->IsDragging = false; @@ -235,13 +240,15 @@ void URTSCamera::RequestMoveCamera(const float X, const float Y, const float Sca void URTSCamera::ApplyMoveCameraCommands() { + /// 遍历执行积压的移动指令,应用当前速度并完成平移 for (const auto& [X, Y, Scale] : this->MoveCameraCommands) { auto Movement = FVector2D(X, Y); Movement.Normalize(); - Movement *= this->NowMoveSpeed * Scale * this->DeltaSeconds; - this->Root->SetWorldLocation( - this->Root->GetComponentLocation() + FVector(Movement.X, Movement.Y, 0.0f) + Movement *= this->CurrentMovementSpeed * Scale * this->DeltaSeconds; + + this->JumpTo( + this->RootComponent->GetComponentLocation() + FVector(Movement.X, Movement.Y, 0.0f) ); } @@ -250,21 +257,23 @@ void URTSCamera::ApplyMoveCameraCommands() void URTSCamera::CollectComponentDependencyReferences() { - this->Owner = this->GetOwner(); - this->Root = this->Owner->GetRootComponent(); - this->Camera = Cast(this->Owner->GetComponentByClass(UCameraComponent::StaticClass())); - this->SpringArm = Cast(this->Owner->GetComponentByClass(USpringArmComponent::StaticClass())); - this->PlayerController = UGameplayStatics::GetPlayerController(this->GetWorld(), 0); + /// 缓存所有者及其核心组件指针以供快速访问 + this->CameraOwner = this->GetOwner(); + this->RootComponent = this->CameraOwner->GetRootComponent(); + this->CameraComponent = Cast(this->CameraOwner->GetComponentByClass(UCameraComponent::StaticClass())); + this->SpringArmComponent = Cast(this->CameraOwner->GetComponentByClass(USpringArmComponent::StaticClass())); + this->RTSPlayerController = UGameplayStatics::GetPlayerController(this->GetWorld(), 0); } void URTSCamera::ConfigureSpringArm() { + /// 初始化弹簧臂臂长及旋转参数 this->DesiredZoomLength = this->MinimumZoomLength; - this->SpringArm->TargetArmLength = this->DesiredZoomLength; - this->SpringArm->bDoCollisionTest = false; - this->SpringArm->bEnableCameraLag = this->EnableCameraLag; - this->SpringArm->bEnableCameraRotationLag = this->EnableCameraRotationLag; - this->SpringArm->SetRelativeRotation( + this->SpringArmComponent->TargetArmLength = this->DesiredZoomLength; + this->SpringArmComponent->bDoCollisionTest = false; + this->SpringArmComponent->bEnableCameraLag = this->EnableCameraLag; + this->SpringArmComponent->bEnableCameraRotationLag = this->EnableCameraRotationLag; + this->SpringArmComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( 0.0, @@ -277,6 +286,7 @@ void URTSCamera::ConfigureSpringArm() void URTSCamera::TryToFindBoundaryVolumeReference() { + /// 通过标签检索地图中的相机边界体积 TArray BlockingVolumes; UGameplayStatics::GetAllActorsOfClassWithTag( this->GetWorld(), @@ -288,56 +298,48 @@ void URTSCamera::TryToFindBoundaryVolumeReference() if (BlockingVolumes.Num() > 0) { this->BoundaryVolume = BlockingVolumes[0]; + + /// 初始化预分配视野点数组并触发初次刷新 + MinimapFrustumPoints.SetNum(4); + this->UpdateMinimapFrustum(); } } -void URTSCamera::ConditionallyEnableEdgeScrolling() const +void URTSCamera::ConditionallyEnableEdgeScrolling() { + /// 如已启用,则将鼠标锁定并在视口中显示 if (this->EnableEdgeScrolling) { FInputModeGameAndUI InputMode; InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways); InputMode.SetHideCursorDuringCapture(false); - this->PlayerController->SetInputMode(InputMode); + this->RTSPlayerController->SetInputMode(InputMode); } } -void URTSCamera::CheckForEnhancedInputComponent() const +void URTSCamera::CheckForEnhancedInputComponent() { - if (Cast(this->PlayerController->InputComponent) == nullptr) + /// 检查是否配置了增强输入组件,若缺失则打印视觉错误提示 + if (Cast(this->RTSPlayerController->InputComponent) == nullptr) { UKismetSystemLibrary::PrintString( this->GetWorld(), - TEXT("Set Edit > Project Settings > Input > Default Classes to Enhanced Input Classes"), true, true, - FLinearColor::Red, - 100 - ); - - UKismetSystemLibrary::PrintString( - this->GetWorld(), - TEXT("Keyboard inputs will probably not function."), true, true, - FLinearColor::Red, - 100 - ); - - UKismetSystemLibrary::PrintString( - this->GetWorld(), - TEXT("Error: Enhanced input component not found."), true, true, + TEXT("Error: Enhanced input component not found. Check Project Settings > Input."), true, true, FLinearColor::Red, 100 ); } } -void URTSCamera::BindInputMappingContext() const +void URTSCamera::BindInputMappingContext() { - if (PlayerController && PlayerController->GetLocalPlayer()) + /// 绑定增强输入上下文,并显示鼠标光标 + if (RTSPlayerController && RTSPlayerController->GetLocalPlayer()) { - if (const auto Input = PlayerController->GetLocalPlayer()->GetSubsystem()) + if (const auto Input = RTSPlayerController->GetLocalPlayer()->GetSubsystem()) { - PlayerController->bShowMouseCursor = true; + RTSPlayerController->bShowMouseCursor = true; - // Check if the context is already bound to prevent double binding if (!Input->HasMappingContext(this->InputMappingContext)) { Input->AddMappingContext(this->InputMappingContext, 0); @@ -348,7 +350,8 @@ void URTSCamera::BindInputMappingContext() const void URTSCamera::BindInputActions() { - if (const auto EnhancedInputComponent = Cast(this->PlayerController->InputComponent)) + /// 将输入动作事件绑定到本地处理函数 + if (const auto EnhancedInputComponent = Cast(this->RTSPlayerController->InputComponent)) { EnhancedInputComponent->BindAction( this->ZoomCamera, @@ -401,104 +404,124 @@ void URTSCamera::BindInputActions() } } -void URTSCamera::SetActiveCamera() const +void URTSCamera::SetActiveCamera() { - this->PlayerController->SetViewTarget(this->GetOwner()); + /// 将当前控制器的观察目标设置为本组件的所有者 + this->RTSPlayerController->SetViewTarget(this->GetOwner()); } -void URTSCamera::JumpTo(const FVector Position) const +void URTSCamera::JumpTo(const FVector Position) { - this->Root->SetWorldLocation(Position); + /// 保持当前高度不变,更新水平位置并刷新视野 + float CurrentZ = this->RootComponent->GetComponentLocation().Z; + this->RootComponent->SetWorldLocation(FVector(Position.X, Position.Y, CurrentZ)); + this->UpdateMinimapFrustum(); } -void URTSCamera::ConditionallyPerformEdgeScrolling() const +void URTSCamera::ConditionallyPerformEdgeScrolling() { + /// 如已启用边缘滚动且当前未处于鼠标拖拽状态,则执行视口边缘推进逻辑 if (this->EnableEdgeScrolling && !this->IsDragging) { + const FVector OldLocation = this->RootComponent->GetComponentLocation(); + this->EdgeScrollLeft(); this->EdgeScrollRight(); this->EdgeScrollUp(); this->EdgeScrollDown(); + + /// 位置发生变动时立即同步视野框 + if (!this->RootComponent->GetComponentLocation().Equals(OldLocation, 0.1f)) + { + this->UpdateMinimapFrustum(); + } } } -void URTSCamera::EdgeScrollLeft() const +void URTSCamera::EdgeScrollLeft() { + /// 计算鼠标在左侧边缘的深度,并以此确定摄像机左移量 const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedMousePosition = 1 - UKismetMathLibrary::NormalizeToRange( + const auto NormalizedPosition = 1 - UKismetMathLibrary::NormalizeToRange( MousePosition.X, 0.0f, ViewportSize.X * this->DistanceFromEdgeThreshold ); - const auto Movement = UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); + const float MovementAlpha = UKismetMathLibrary::FClamp(NormalizedPosition, 0.0, 1.0); - this->Root->AddRelativeLocation( - -1 * this->Root->GetRightVector() * Movement * this->NowMoveSpeed * this->DeltaSeconds + this->RootComponent->AddRelativeLocation( + -1 * this->RootComponent->GetRightVector() * MovementAlpha * this->CurrentMovementSpeed * this->DeltaSeconds ); } -void URTSCamera::EdgeScrollRight() const +void URTSCamera::EdgeScrollRight() { + /// 计算鼠标在右侧边缘的深度,并以此确定摄像机右移量 const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedMousePosition = UKismetMathLibrary::NormalizeToRange( + const auto NormalizedPosition = UKismetMathLibrary::NormalizeToRange( MousePosition.X, ViewportSize.X * (1 - this->DistanceFromEdgeThreshold), ViewportSize.X ); - const auto Movement = UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); - this->Root->AddRelativeLocation( - this->Root->GetRightVector() * Movement * this->NowMoveSpeed * this->DeltaSeconds + const float MovementAlpha = UKismetMathLibrary::FClamp(NormalizedPosition, 0.0, 1.0); + this->RootComponent->AddRelativeLocation( + this->RootComponent->GetRightVector() * MovementAlpha * this->CurrentMovementSpeed * this->DeltaSeconds ); } -void URTSCamera::EdgeScrollUp() const +void URTSCamera::EdgeScrollUp() { + /// 计算鼠标在上侧边缘的深度,并以此确定摄像机前移量 const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedMousePosition = UKismetMathLibrary::NormalizeToRange( + const auto NormalizedPosition = UKismetMathLibrary::NormalizeToRange( MousePosition.Y, 0.0f, ViewportSize.Y * this->DistanceFromEdgeThreshold ); - const auto Movement = 1 - UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); - this->Root->AddRelativeLocation( - this->Root->GetForwardVector() * Movement * this->NowMoveSpeed * this->DeltaSeconds + const float MovementAlpha = 1 - UKismetMathLibrary::FClamp(NormalizedPosition, 0.0, 1.0); + this->RootComponent->AddRelativeLocation( + this->RootComponent->GetForwardVector() * MovementAlpha * this->CurrentMovementSpeed * this->DeltaSeconds ); } -void URTSCamera::EdgeScrollDown() const +void URTSCamera::EdgeScrollDown() { + /// 计算鼠标在下侧边缘的深度,并以此确定摄像机后移量 const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedMousePosition = UKismetMathLibrary::NormalizeToRange( + const auto NormalizedPosition = UKismetMathLibrary::NormalizeToRange( MousePosition.Y, ViewportSize.Y * (1 - this->DistanceFromEdgeThreshold), ViewportSize.Y ); - const auto Movement = UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); - this->Root->AddRelativeLocation( - -1 * this->Root->GetForwardVector() * Movement * this->NowMoveSpeed * this->DeltaSeconds + const float MovementAlpha = UKismetMathLibrary::FClamp(NormalizedPosition, 0.0, 1.0); + this->RootComponent->AddRelativeLocation( + -1 * this->RootComponent->GetForwardVector() * MovementAlpha * this->CurrentMovementSpeed * this->DeltaSeconds ); } -void URTSCamera::FollowTargetIfSet() const +void URTSCamera::FollowTargetIfSet() { if (this->CameraFollowTarget != nullptr) { - this->Root->SetWorldLocation(this->CameraFollowTarget->GetActorLocation()); + this->JumpTo(this->CameraFollowTarget->GetActorLocation()); } } -void URTSCamera::SmoothTargetArmLengthToDesiredZoom() const +void URTSCamera::SmoothTargetArmLengthToDesiredZoom() { - this->SpringArm->TargetArmLength = FMath::FInterpTo( - this->SpringArm->TargetArmLength, + /// 使用平滑插值算法将弹簧臂长度拉近至目标意图长度 + float OldArmLength = this->SpringArmComponent->TargetArmLength; + + this->SpringArmComponent->TargetArmLength = FMath::FInterpTo( + this->SpringArmComponent->TargetArmLength, this->DesiredZoomLength, this->DeltaSeconds, this->ZoomCatchupSpeed @@ -507,13 +530,14 @@ void URTSCamera::SmoothTargetArmLengthToDesiredZoom() const void URTSCamera::ConditionallyKeepCameraAtDesiredZoomAboveGround() { + /// 动态调整相机高度以贴合地形 if (this->EnableDynamicCameraHeight) { - const auto RootWorldLocation = this->Root->GetComponentLocation(); + const FVector RootWorldLocation = this->RootComponent->GetComponentLocation(); const TArray ActorsToIgnore; - auto HitResult = FHitResult(); - auto DidHit = UKismetSystemLibrary::LineTraceSingle( + FHitResult HitResult; + bool bDidHit = UKismetSystemLibrary::LineTraceSingle( this->GetWorld(), FVector(RootWorldLocation.X, RootWorldLocation.Y, RootWorldLocation.Z + this->FindGroundTraceLength), FVector(RootWorldLocation.X, RootWorldLocation.Y, RootWorldLocation.Z - this->FindGroundTraceLength), @@ -525,9 +549,9 @@ void URTSCamera::ConditionallyKeepCameraAtDesiredZoomAboveGround() true ); - if (DidHit) + if (bDidHit) { - this->Root->SetWorldLocation( + this->RootComponent->SetWorldLocation( FVector( HitResult.Location.X, HitResult.Location.Y, @@ -535,55 +559,85 @@ void URTSCamera::ConditionallyKeepCameraAtDesiredZoomAboveGround() ) ); } - - else if (!this->IsCameraOutOfBoundsErrorAlreadyDisplayed) - { - this->IsCameraOutOfBoundsErrorAlreadyDisplayed = true; - - UKismetSystemLibrary::PrintString( - this->GetWorld(), - "Or add a `RTSCameraBoundsVolume` actor to the scene.", - true, - true, - FLinearColor::Red, - 100 - ); - - UKismetSystemLibrary::PrintString( - this->GetWorld(), - "Increase trace length or change the starting position of the parent actor for the spring arm.", - true, - true, - FLinearColor::Red, - 100 - ); - - UKismetSystemLibrary::PrintString( - this->GetWorld(), - "Error: AC_RTSCamera needs to be placed on the ground!", - true, - true, - FLinearColor::Red, - 100 - ); - } } } -void URTSCamera::ConditionallyApplyCameraBounds() const +void URTSCamera::ConditionallyApplyCameraBounds() { + /// 如已定义边界,则将当前位置限制在边界体积范围内 if (this->BoundaryVolume != nullptr) { - const auto RootWorldLocation = this->Root->GetComponentLocation(); + const FVector CurrentWorldLocation = this->RootComponent->GetComponentLocation(); FVector Origin; FVector Extents; this->BoundaryVolume->GetActorBounds(false, Origin, Extents); - this->Root->SetWorldLocation( + + this->RootComponent->SetWorldLocation( FVector( - UKismetMathLibrary::Clamp(RootWorldLocation.X, Origin.X - Extents.X, Origin.X + Extents.X), - UKismetMathLibrary::Clamp(RootWorldLocation.Y, Origin.Y - Extents.Y, Origin.Y + Extents.Y), - RootWorldLocation.Z + UKismetMathLibrary::Clamp(CurrentWorldLocation.X, Origin.X - Extents.X, Origin.X + Extents.X), + UKismetMathLibrary::Clamp(CurrentWorldLocation.Y, Origin.Y - Extents.Y, Origin.Y + Extents.Y), + CurrentWorldLocation.Z ) ); } } + +void URTSCamera::UpdateMinimapFrustum() +{ + if (!SpringArmComponent || !CameraComponent || !RootComponent) return; + + /// 1. 提取当前相机的策略参数 (X, Y, Z_Intent, Pitch, Yaw) + FVector RootLocation = RootComponent->GetComponentLocation(); + FRotator RootRotation = RootComponent->GetComponentRotation(); + FRotator ArmRotation = SpringArmComponent->GetRelativeRotation(); + + /// 合成逻辑相机旋转 (使用根组件的 Yaw 和弹簧臂的 Pitch) + FRotator LogicCameraRotation = RootRotation + ArmRotation; + + /// 2. 使用意图高度 (DesiredZoomLength) 计算相机逻辑位置 + float CameraArmLength = this->DesiredZoomLength; + FVector LogicCameraLocation = RootLocation + LogicCameraRotation.Vector() * (-CameraArmLength); + + /// 3. 获取相机内参并计算视野张角 + float FieldOfView = CameraComponent->FieldOfView; + float AspectRatio = CameraComponent->AspectRatio; + if(AspectRatio <= 0.0f) AspectRatio = 1.777f; + + float HalfHorizontalFOV = FMath::DegreesToRadians(FieldOfView) / 2.0f; + float HalfVerticalFOV = FMath::Atan(FMath::Tan(HalfHorizontalFOV) / AspectRatio); + + float TangentHorizontal = FMath::Tan(HalfHorizontalFOV); + float TangentVertical = FMath::Tan(HalfVerticalFOV); + + /// 4. 计算视野四个方向的射线向量 + FVector ForwardVector = LogicCameraRotation.Vector(); + FVector RightVector = FRotationMatrix(LogicCameraRotation).GetScaledAxis(EAxis::Y); + FVector UpVector = FRotationMatrix(LogicCameraRotation).GetScaledAxis(EAxis::Z); + + FVector DirectionTopLeft = (ForwardVector - RightVector * TangentHorizontal + UpVector * TangentVertical).GetSafeNormal(); + FVector DirectionTopRight = (ForwardVector + RightVector * TangentHorizontal + UpVector * TangentVertical).GetSafeNormal(); + FVector DirectionBottomLeft = (ForwardVector - RightVector * TangentHorizontal - UpVector * TangentVertical).GetSafeNormal(); + FVector DirectionBottomRight = (ForwardVector + RightVector * TangentHorizontal - UpVector * TangentVertical).GetSafeNormal(); + + /// 5. 计算射线与地平面的交点进行位置投射 + float GroundZ = this->RootComponent->GetComponentLocation().Z; + + auto IntersectGround = [&](const FVector& RayOrigin, const FVector& RayDirection) -> FVector + { + if (RayDirection.Z >= -0.001f) return RayOrigin + RayDirection * 100000.0f; + float t = (GroundZ - RayOrigin.Z) / RayDirection.Z; + if (t < 0.0f) return RayOrigin + RayDirection * 100000.0f; + return RayOrigin + t * RayDirection; + }; + + /// 6. 更新视野点并缓存同步至 UI + if (MinimapFrustumPoints.Num() != 4) + { + MinimapFrustumPoints.SetNum(4); + } + + MinimapFrustumPoints[0] = IntersectGround(LogicCameraLocation, DirectionTopLeft); + MinimapFrustumPoints[1] = IntersectGround(LogicCameraLocation, DirectionTopRight); + MinimapFrustumPoints[2] = IntersectGround(LogicCameraLocation, DirectionBottomRight); + MinimapFrustumPoints[3] = IntersectGround(LogicCameraLocation, DirectionBottomLeft); +} diff --git a/Source/OpenRTSCamera/Private/RTSCameraBoundsVolume.cpp b/Source/OpenRTSCamera/Private/RTSCameraBoundsVolume.cpp index 1cc3a6a..bd2a0b4 100644 --- a/Source/OpenRTSCamera/Private/RTSCameraBoundsVolume.cpp +++ b/Source/OpenRTSCamera/Private/RTSCameraBoundsVolume.cpp @@ -5,10 +5,11 @@ ARTSCameraBoundsVolume::ARTSCameraBoundsVolume() { - this->Tags.Add("OpenRTSCamera#CameraBounds"); - - if (UPrimitiveComponent* PrimitiveComponent = this->FindComponentByClass()) - { - PrimitiveComponent->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName, false); - } + // Default constructor +} + +void ARTSCameraBoundsVolume::BeginPlay() +{ + Super::BeginPlay(); + Tags.AddUnique(FName("OpenRTSCamera#CameraBounds")); } diff --git a/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp new file mode 100644 index 0000000..70c9b79 --- /dev/null +++ b/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp @@ -0,0 +1,259 @@ +// Copyright 2024 Jesus Bracho All Rights Reserved. + +#include "UI/RTSCameraMinimapWidget.h" +#include "RTSCamera.h" +#include "OpenRTSCamera.h" +#include "GameFramework/PlayerController.h" +#include "Kismet/GameplayStatics.h" +#include "Rendering/DrawElements.h" + + + +URTSCameraMinimapWidget::URTSCameraMinimapWidget(const FObjectInitializer& ObjectInitializer) + : Super(ObjectInitializer) +{ + SetVisibility(ESlateVisibility::Visible); +} + +void URTSCameraMinimapWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + // Force visibility and interaction flags at runtime to override BP defaults + SetVisibility(ESlateVisibility::Visible); + SetIsFocusable(true); + + InitializeController(); +} + +void URTSCameraMinimapWidget::InitializeController() +{ + if (APlayerController* PC = GetOwningPlayer()) + { + PC->bEnableClickEvents = true; + PC->bEnableMouseOverEvents = true; + } + FindRTSCamera(); +} + +void URTSCameraMinimapWidget::FindRTSCamera() +{ + // 1. Try ViewTarget + if (!CachedRTSCamera) + { + APlayerController* PC = GetOwningPlayer(); + if (PC) + { + if (AActor* ViewTarget = PC->GetViewTarget()) + { + CachedRTSCamera = ViewTarget->FindComponentByClass(); + } + if (!CachedRTSCamera) + { + if (APawn* Pawn = PC->GetPawn()) + { + CachedRTSCamera = Pawn->FindComponentByClass(); + } + } + } + } + + // 3. Cache Dependencies + if (CachedRTSCamera) + { + AActor* Owner = CachedRTSCamera->GetOwner(); + if (Owner) + { + if (!CachedCameraComponent) CachedCameraComponent = Owner->FindComponentByClass(); + if (!CachedSpringArm) CachedSpringArm = Owner->FindComponentByClass(); + } + + // Update Bounds (Retry if invalid) + if (!bHasValidBounds) + { + if (AActor* BoundsActor = CachedRTSCamera->GetBoundaryVolume()) + { + FVector Origin, Extent; + BoundsActor->GetActorBounds(false, Origin, Extent); + CachedBoundsOrigin = Origin; + CachedBoundsExtent = Extent; + bHasValidBounds = true; + + UE_LOG(LogOpenRTSCamera, Log, TEXT("RTSCameraMinimapWidget: Bounds Found! Origin=%s, Extent=%s"), *Origin.ToString(), *Extent.ToString()); + } + else + { + static bool bLogOnce = false; + if (!bLogOnce) + { + UE_LOG(LogOpenRTSCamera, Warning, TEXT("RTSCameraMinimapWidget: RTSCamera found but BoundaryVolume is missing!")); + bLogOnce = true; + } + } + } + } +} + +FVector2D URTSCameraMinimapWidget::ConvertWorldToWidgetLocal(const FVector2D& WorldPos, const FVector2D& WidgetSize) const +{ + if (CachedBoundsExtent.X < KINDA_SMALL_NUMBER || CachedBoundsExtent.Y < KINDA_SMALL_NUMBER) return FVector2D::ZeroVector; + + // Normalized X/Y in Bounds Space + float NormX = (WorldPos.X - (CachedBoundsOrigin.X - CachedBoundsExtent.X)) / (2.0f * CachedBoundsExtent.X); + float NormY = (WorldPos.Y - (CachedBoundsOrigin.Y - CachedBoundsExtent.Y)) / (2.0f * CachedBoundsExtent.Y); + + // Map to Widget Space: World +X (North) -> Widget -Y, World +Y (East) -> Widget +X + return FVector2D(NormY * WidgetSize.X, (1.0f - NormX) * WidgetSize.Y); +} + +FVector2D URTSCameraMinimapWidget::ConvertWidgetLocalToWorld(const FVector2D& LocalPos, const FVector2D& WidgetSize) const +{ + if (WidgetSize.X <= 0.0f || WidgetSize.Y <= 0.0f) return FVector2D::ZeroVector; + + float U = LocalPos.X / WidgetSize.X; + float V = LocalPos.Y / WidgetSize.Y; + + // Invert Coordinate Mapping: + // U = NormY => NormY = U + // V = 1.0 - NormX => NormX = 1.0 - V + float NormX = 1.0f - V; + float NormY = U; + + float WorldX = (CachedBoundsOrigin.X - CachedBoundsExtent.X) + NormX * (2.0f * CachedBoundsExtent.X); + float WorldY = (CachedBoundsOrigin.Y - CachedBoundsExtent.Y) + NormY * (2.0f * CachedBoundsExtent.Y); + + return FVector2D(WorldX, WorldY); +} + +int32 URTSCameraMinimapWidget::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const +{ + // 1. Ensure Camera Reference + int32 MaxLayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); + + if (!CachedRTSCamera || !bHasValidBounds) + { + const_cast(this)->FindRTSCamera(); + } + + if (!CachedRTSCamera || !bHasValidBounds || !CachedSpringArm || !CachedCameraComponent) + { + return MaxLayerId; + } + + // 2. Read Cached Data (Source of Truth: RTSCamera) + const TArray& WorldPoints = CachedRTSCamera->MinimapFrustumPoints; + + // Debug Log + UE_LOG(LogOpenRTSCamera, Verbose, TEXT("MinimapWidget: CachedRTSCamera found. Points Num: %d"), WorldPoints.Num()); + + if (WorldPoints.Num() < 4) + { + UE_LOG(LogOpenRTSCamera, Warning, TEXT("MinimapWidget: Not enough points (%d)"), WorldPoints.Num()); + return MaxLayerId; + } + + // 3. Convert & Draw + FVector2D LocalSize = AllottedGeometry.GetLocalSize(); + // Debug Widget Size to ensure it's not zero (layout issue) + if (LocalSize.X < 1.0f || LocalSize.Y < 1.0f) + { + UE_LOG(LogOpenRTSCamera, Warning, TEXT("RTSCameraMinimapWidget: LocalSize is too small! (%s). Check UMG layout (Wrap with SizeBox?)."), *LocalSize.ToString()); + } + else + { + // UE_LOG(LogOpenRTSCamera, Verbose, TEXT("RTSCameraMinimapWidget: LocalSize %s"), *LocalSize.ToString()); + } + + TArray Points; + + for (const FVector& WorldPt : WorldPoints) + { + FVector2D ScreenPt = ConvertWorldToWidgetLocal(FVector2D(WorldPt), LocalSize); + Points.Add(ScreenPt); + // UE_LOG(LogOpenRTSCamera, Verbose, TEXT(" Pt: %s -> Screen: %s"), *WorldPt.ToString(), *ScreenPt.ToString()); + } + + // 4. Draw + if (Points.Num() > 0) + { + const FVector2D FirstPoint = Points[0]; + Points.Add(FirstPoint); // Close Loop (Copy first to avoid reallocation crash) + } + + FSlateDrawElement::MakeLines( + OutDrawElements, + LayerId + 1, + AllottedGeometry.ToPaintGeometry(), + Points, + ESlateDrawEffect::None, + FLinearColor::White, + true, + LineWidth + ); + + return MaxLayerId + 1; +} + +FReply URTSCameraMinimapWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + UE_LOG(LogOpenRTSCamera, Log, TEXT("MinimapWidget: OnMouseButtonDown (Button: %s)"), *InMouseEvent.GetEffectingButton().ToString()); + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) + { + bIsDragging = true; + if (CachedRTSCamera) + { + FVector2D ScreenPos = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); + FVector2D WorldPos = ConvertWidgetLocalToWorld( + ScreenPos, + InGeometry.GetLocalSize() + ); + + UE_LOG(LogOpenRTSCamera, Log, TEXT("Minimap Click: Screen=%s, World=%s. Jumping!"), *ScreenPos.ToString(), *WorldPos.ToString()); + + // Maintain current Z, don't force 0 if possible, or use JumpTo which might handle it. + // JumpTo set Z=0 in current impl. Let's trust JumpTo for now but log it. + CachedRTSCamera->JumpTo(FVector(WorldPos, 0.0f)); + } + return FReply::Handled().CaptureMouse(TakeWidget()); + } + return FReply::Unhandled(); +} + +FReply URTSCameraMinimapWidget::NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && bIsDragging) + { + bIsDragging = false; + return FReply::Handled().ReleaseMouseCapture(); + } + return FReply::Unhandled(); +} + +FReply URTSCameraMinimapWidget::NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + if (bIsDragging && HasMouseCapture()) + { + if (CachedRTSCamera) + { + FVector2D WorldPos = ConvertWidgetLocalToWorld( + InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()), + InGeometry.GetLocalSize() + ); + CachedRTSCamera->JumpTo(FVector(WorldPos, 0.0f)); + } + return FReply::Handled(); + } + return FReply::Unhandled(); +} + +void URTSCameraMinimapWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + Super::NativeOnMouseEnter(InGeometry, InMouseEvent); + UE_LOG(LogOpenRTSCamera, Log, TEXT("MinimapWidget: Mouse Entered Widget Area")); +} + +void URTSCameraMinimapWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent) +{ + Super::NativeOnMouseLeave(InMouseEvent); + UE_LOG(LogOpenRTSCamera, Log, TEXT("MinimapWidget: Mouse Left Widget Area")); +} diff --git a/Source/OpenRTSCamera/Public/OpenRTSCamera.h b/Source/OpenRTSCamera/Public/OpenRTSCamera.h index 0e6163d..3741d4d 100644 --- a/Source/OpenRTSCamera/Public/OpenRTSCamera.h +++ b/Source/OpenRTSCamera/Public/OpenRTSCamera.h @@ -11,3 +11,5 @@ class FOpenRTSCameraModule : public IModuleInterface virtual void StartupModule() override; virtual void ShutdownModule() override; }; + +DECLARE_LOG_CATEGORY_EXTERN(LogOpenRTSCamera, Log, All); diff --git a/Source/OpenRTSCamera/Public/RTSCamera.h b/Source/OpenRTSCamera/Public/RTSCamera.h index 25eca16..21c32c4 100644 --- a/Source/OpenRTSCamera/Public/RTSCamera.h +++ b/Source/OpenRTSCamera/Public/RTSCamera.h @@ -25,6 +25,11 @@ struct FMoveCameraCommand float Scale = 0; }; +/** + * @brief RTS 相机组件,负责处理相机的移动、旋转、缩放以及视野计算。 + * + * 该组件集成了平滑缩放、边缘滚动、目标跟随等功能,并能实时计算相机在地面的视野框点。 + **/ UCLASS(Blueprintable, ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class OPENRTSCAMERA_API URTSCamera : public UActorComponent { @@ -46,10 +51,18 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent void UnFollowTarget(); UFUNCTION(BlueprintCallable, Category = "RTSCamera") - void SetActiveCamera() const; + void SetActiveCamera(); + /** + * @brief 将相机瞬间移动到指定位置 + * + * @param 参数名称: Position 数据类型: FVector + **/ UFUNCTION(BlueprintCallable, Category = "RTSCamera") - void JumpTo(FVector Position) const; + void JumpTo(FVector Position); + + UFUNCTION(BlueprintPure, Category = "RTSCamera") + AActor* GetBoundaryVolume() const { return BoundaryVolume; } UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") float MinimumZoomLength; @@ -147,16 +160,27 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent void RequestMoveCamera(float X, float Y, float Scale); void ApplyMoveCameraCommands(); + /** @brief 相机所属的 Actor 引用 */ UPROPERTY() - AActor* Owner; + AActor* CameraOwner; + + /** @brief 根组件场景引用,用于控制相机在世界中的平移和旋转 */ UPROPERTY() - USceneComponent* Root; + USceneComponent* RootComponent; + + /** @brief 相机组件引用,提供 FOV 等光学参数 */ UPROPERTY() - UCameraComponent* Camera; + UCameraComponent* CameraComponent; + + /** @brief 弹簧臂组件引用,用于控制相机的距离(臂长)和俯角 */ UPROPERTY() - USpringArmComponent* SpringArm; + USpringArmComponent* SpringArmComponent; + + /** @brief 玩家控制器引用,用于处理输入和视图设置 */ UPROPERTY() - APlayerController* PlayerController; + APlayerController* RTSPlayerController; + + /** @brief 相机移动边界体积,用于限制相机活动范围 */ UPROPERTY() AActor* BoundaryVolume; UPROPERTY() @@ -165,22 +189,23 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent private: void CollectComponentDependencyReferences(); void ConfigureSpringArm(); + void TryToFindBoundaryVolumeReference(); - void ConditionallyEnableEdgeScrolling() const; - void CheckForEnhancedInputComponent() const; - void BindInputMappingContext() const; + void ConditionallyEnableEdgeScrolling(); + void CheckForEnhancedInputComponent(); + void BindInputMappingContext(); void BindInputActions(); - void ConditionallyPerformEdgeScrolling() const; - void EdgeScrollLeft() const; - void EdgeScrollRight() const; - void EdgeScrollUp() const; - void EdgeScrollDown() const; + void ConditionallyPerformEdgeScrolling(); + void EdgeScrollLeft(); + void EdgeScrollRight(); + void EdgeScrollUp(); + void EdgeScrollDown(); - void FollowTargetIfSet() const; - void SmoothTargetArmLengthToDesiredZoom() const; + void FollowTargetIfSet(); + void SmoothTargetArmLengthToDesiredZoom(); void ConditionallyKeepCameraAtDesiredZoomAboveGround(); - void ConditionallyApplyCameraBounds() const; + void ConditionallyApplyCameraBounds(); UPROPERTY() FName CameraBlockingVolumeTag; @@ -194,8 +219,24 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent bool IsDragging; UPROPERTY() FVector2D DragStartLocation; + /** @brief 相机移动指令队列 */ UPROPERTY() TArray MoveCameraCommands; + + /** @brief 当前移动速度 */ UPROPERTY() - float NowMoveSpeed; + float CurrentMovementSpeed; + +public: + /** + * @brief 计算并存储相机在地平面的四个视野投影点 + **/ + UPROPERTY(BlueprintReadOnly, Category = "RTSCamera|Minimap") + TArray MinimapFrustumPoints; + + /** + * @brief 强制更新视野框坐标。内部计算优先使用 DesiredZoomLength 以实现意图同步。 + **/ + UFUNCTION(BlueprintCallable, Category = "RTSCamera|Minimap") + void UpdateMinimapFrustum(); }; diff --git a/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h b/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h index 33ce6ad..bce438a 100644 --- a/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h +++ b/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h @@ -11,5 +11,9 @@ class OPENRTSCAMERA_API ARTSCameraBoundsVolume : public ACameraBlockingVolume { GENERATED_BODY() +public: ARTSCameraBoundsVolume(); + +protected: + virtual void BeginPlay() override; }; diff --git a/Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h b/Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h new file mode 100644 index 0000000..0e66c47 --- /dev/null +++ b/Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h @@ -0,0 +1,75 @@ +// Copyright 2024 Jesus Bracho All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "RTSCameraMinimapWidget.generated.h" + +class URTSCamera; + +/** + * URTSCameraMinimapWidget + * + * A widget that visualizes the RTS Camera's Field of View on a minimap. + * It uses the Camera's BoundaryVolume to determine the coordinate system. + * It handles input to move the camera (JumpTo). + */ +UCLASS(BlueprintType, Blueprintable) +class OPENRTSCAMERA_API URTSCameraMinimapWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + URTSCameraMinimapWidget(const FObjectInitializer& ObjectInitializer); + + /** + * Initialize the controller. Tries to find the RTSCamera on the owning player's pawn/view target. + */ + UFUNCTION(BlueprintCallable, Category = "RTSCamera|Minimap") + void InitializeController(); + +protected: + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Minimap") + float LineWidth = 2.0f; + + virtual void NativeConstruct() override; + virtual int32 NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; + + // --- Input Handling --- + virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + virtual FReply NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + virtual FReply NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + virtual void NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + virtual void NativeOnMouseLeave(const FPointerEvent& InMouseEvent) override; + +private: + void FindRTSCamera(); + + /** Convert World Location (XY) to Widget Local Coordinates (UV * Size) */ + FVector2D ConvertWorldToWidgetLocal(const FVector2D& WorldPos, const FVector2D& WidgetSize) const; + + /** Convert Widget Local Coordinates to World Location (XY) */ + FVector2D ConvertWidgetLocalToWorld(const FVector2D& LocalPos, const FVector2D& WidgetSize) const; + +protected: + /** Cached reference to the RTSCamera */ + UPROPERTY(Transient, BlueprintReadOnly, Category = "RTSCamera|Cache") + TObjectPtr CachedRTSCamera; + + /** Cached reference to the actual Camera Component (Source of Truth for FOV) */ + UPROPERTY(Transient, BlueprintReadOnly, Category = "RTSCamera|Cache") + TObjectPtr CachedCameraComponent; + + /** Cached reference to the Spring Arm (Source of Truth for Zoom/Rotation) */ + UPROPERTY(Transient, BlueprintReadOnly, Category = "RTSCamera|Cache") + TObjectPtr CachedSpringArm; + + /** Derived from BoundaryVolume */ + FVector CachedBoundsOrigin = FVector::ZeroVector; + FVector CachedBoundsExtent = FVector(100.f, 100.f, 100.f); + bool bHasValidBounds = false; + + /** Input State */ + bool bIsDragging = false; +}; From 5cfe0821d56a062f3a8edf5a0c00b375c9b3c817 Mon Sep 17 00:00:00 2001 From: winyunq Date: Sat, 14 Feb 2026 17:47:14 +0800 Subject: [PATCH 05/12] =?UTF-8?q?=E5=BD=92=E6=A1=A3=E4=B8=AD=EF=BC=8C?= =?UTF-8?q?=E4=BD=86=E6=98=AF=E6=9C=89=E9=97=AE=E9=A2=98=E3=80=82=E4=BC=9A?= =?UTF-8?q?=E9=97=AA=E9=80=80=EF=BC=8C=E4=BD=86=E6=98=AF=E5=B7=AE=E4=B8=8D?= =?UTF-8?q?=E5=A4=9A=E4=BA=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/OpenRTSCamera/Private/RTSCamera.cpp | 741 ++++++++---------- .../Private/UI/RTSCameraMinimapWidget.cpp | 248 +++--- Source/OpenRTSCamera/Public/RTSCamera.h | 361 ++++++--- .../Public/UI/RTSCameraMinimapWidget.h | 37 +- 4 files changed, 726 insertions(+), 661 deletions(-) diff --git a/Source/OpenRTSCamera/Private/RTSCamera.cpp b/Source/OpenRTSCamera/Private/RTSCamera.cpp index 37dacb2..7c2979b 100644 --- a/Source/OpenRTSCamera/Private/RTSCamera.cpp +++ b/Source/OpenRTSCamera/Private/RTSCamera.cpp @@ -13,631 +13,554 @@ URTSCamera::URTSCamera() { - /// 开启组件更新并设置默认相机策略参数 + /// 设置组件基本生存期属性 PrimaryComponentTick.bCanEverTick = true; - this->CameraBlockingVolumeTag = FName("OpenRTSCamera#CameraBounds"); - this->CollisionChannel = ECC_WorldStatic; - this->DragExtent = 0.6f; - this->DistanceFromEdgeThreshold = 0.1f; - this->EnableCameraLag = true; - this->EnableCameraRotationLag = true; - this->EnableDynamicCameraHeight = true; - this->EnableEdgeScrolling = true; - this->FindGroundTraceLength = 100000; - this->MaximumZoomLength = 5000; - this->MinimumZoomLength = 500; - this->MaxMoveSpeed = 1024.0f; - this->MinMoveSpeed = 128.0f; - this->CurrentMovementSpeed = this->MinMoveSpeed; - this->RotateSpeed = 45; - this->StartingYAngle = -45.0f; - this->StartingZAngle = 0; - this->ZoomCatchupSpeed = 4; - this->ZoomSpeed = -200; - - /// 加载并绑定 Enhanced Input 相关资源 - static ConstructorHelpers::FObjectFinder MoveCameraXAxisFinder(TEXT("/OpenRTSCamera/Inputs/MoveCameraXAxis")); - static ConstructorHelpers::FObjectFinder MoveCameraYAxisFinder(TEXT("/OpenRTSCamera/Inputs/MoveCameraYAxis")); - static ConstructorHelpers::FObjectFinder RotateCameraAxisFinder(TEXT("/OpenRTSCamera/Inputs/RotateCameraAxis")); - static ConstructorHelpers::FObjectFinder TurnCameraLeftFinder(TEXT("/OpenRTSCamera/Inputs/TurnCameraLeft")); - static ConstructorHelpers::FObjectFinder TurnCameraRightFinder(TEXT("/OpenRTSCamera/Inputs/TurnCameraRight")); - static ConstructorHelpers::FObjectFinder ZoomCameraFinder(TEXT("/OpenRTSCamera/Inputs/ZoomCamera")); - static ConstructorHelpers::FObjectFinder DragCameraFinder(TEXT("/OpenRTSCamera/Inputs/DragCamera")); - static ConstructorHelpers::FObjectFinder InputMappingContextFinder(TEXT("/OpenRTSCamera/Inputs/OpenRTSCameraInputs")); - - this->MoveCameraXAxis = MoveCameraXAxisFinder.Object; - this->MoveCameraYAxis = MoveCameraYAxisFinder.Object; - this->RotateCameraAxis = RotateCameraAxisFinder.Object; - this->TurnCameraLeft = TurnCameraLeftFinder.Object; - this->TurnCameraRight = TurnCameraRightFinder.Object; - this->DragCamera = DragCameraFinder.Object; - this->ZoomCamera = ZoomCameraFinder.Object; - this->InputMappingContext = InputMappingContextFinder.Object; + this->cameraBlockingVolumeTag = FName("OpenRTSCamera#CameraBounds"); + this->collisionChannel = ECC_WorldStatic; + this->dragExtent = 0.6f; + this->distanceFromEdgeThreshold = 0.1f; + this->enableCameraLag = true; + this->enableCameraRotationLag = true; + this->enableDynamicCameraHeight = true; + this->enableEdgeScrolling = true; + this->findGroundTraceLength = 100000; + this->maximumZoomLength = 5000; + this->minimumZoomLength = 500; + this->maxMovementSpeed = 1024.0f; + this->minMovementSpeed = 128.0f; + this->currentMovementSpeed = this->minMovementSpeed; + this->rotationSpeed = 45; + this->startingPitchAngle = -45.0f; + this->startingYawAngle = 0; + this->zoomCatchupSpeed = 4; + this->zoomSpeed = -200; + + /// 载入并关联输入资产 + static ConstructorHelpers::FObjectFinder xMoveActionFinder(TEXT("/OpenRTSCamera/Inputs/MoveCameraXAxis")); + static ConstructorHelpers::FObjectFinder yMoveActionFinder(TEXT("/OpenRTSCamera/Inputs/MoveCameraYAxis")); + static ConstructorHelpers::FObjectFinder rotateActionFinder(TEXT("/OpenRTSCamera/Inputs/RotateCameraAxis")); + static ConstructorHelpers::FObjectFinder leftTurnActionFinder(TEXT("/OpenRTSCamera/Inputs/TurnCameraLeft")); + static ConstructorHelpers::FObjectFinder rightTurnActionFinder(TEXT("/OpenRTSCamera/Inputs/TurnCameraRight")); + static ConstructorHelpers::FObjectFinder zoomActionFinder(TEXT("/OpenRTSCamera/Inputs/ZoomCamera")); + static ConstructorHelpers::FObjectFinder dragActionFinder(TEXT("/OpenRTSCamera/Inputs/DragCamera")); + static ConstructorHelpers::FObjectFinder contextFinder(TEXT("/OpenRTSCamera/Inputs/OpenRTSCameraInputs")); + + this->moveCameraXAxisAction = xMoveActionFinder.Object; + this->moveCameraYAxisAction = yMoveActionFinder.Object; + this->rotateCameraAxisAction = rotateActionFinder.Object; + this->turnCameraLeftAction = leftTurnActionFinder.Object; + this->turnCameraRightAction = rightTurnActionFinder.Object; + this->dragCameraAction = dragActionFinder.Object; + this->zoomCameraAction = zoomActionFinder.Object; + this->inputMappingContext = contextFinder.Object; } void URTSCamera::BeginPlay() { Super::BeginPlay(); - const auto NetMode = this->GetNetMode(); - if (NetMode != NM_DedicatedServer) + const auto netMode = this->GetNetMode(); + if (netMode != NM_DedicatedServer) { - /// 初始化依赖组件、配置弹簧臂并寻找地图边界 - this->CollectComponentDependencyReferences(); - this->ConfigureSpringArm(); - this->TryToFindBoundaryVolumeReference(); - this->ConditionallyEnableEdgeScrolling(); - this->CheckForEnhancedInputComponent(); - this->BindInputMappingContext(); - this->BindInputActions(); + /// 初始化依赖架构,建立输入映射链条 + this->resolveComponentDependencyPointers(); + this->setupInitialSpringArmState(); + this->locateMapBoundaryVolumeByTag(); + this->configureInputModeForEdgeScrolling(); + this->validateEnhancedInputAvailability(); + this->registerInputMappingContext(); + this->bindActionCallbacks(); } } void URTSCamera::TickComponent( - const float DeltaTime, - const ELevelTick TickType, + float DeltaTime, + ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) { Super::TickComponent(DeltaTime, TickType, ThisTickFunction); - const auto NetMode = this->GetNetMode(); + const auto netMode = this->GetNetMode(); - /// 仅在非专用服务器且当前相机为活跃视图时执行状态更新 (移动、缩放、边界检查等) - if (NetMode != NM_DedicatedServer && this->RTSPlayerController->GetViewTarget() == this->CameraOwner) + /// 仅在大客户端中处理相机位置插值与状态同步 + if (netMode != NM_DedicatedServer && this->realTimeStrategyPlayerController->GetViewTarget() == this->cameraOwner) { - this->DeltaSeconds = DeltaTime; - this->ApplyMoveCameraCommands(); - this->ConditionallyPerformEdgeScrolling(); - this->ConditionallyKeepCameraAtDesiredZoomAboveGround(); - this->SmoothTargetArmLengthToDesiredZoom(); - this->FollowTargetIfSet(); - this->ConditionallyApplyCameraBounds(); + this->deltaSeconds = DeltaTime; + this->applyAccumulatedMovementCommands(); + this->executeEdgeScrollingEvaluation(); + this->rectifyRootHeightFromTerrain(); + this->handleTargetArmLengthInterpolation(); + this->updateFollowPositionIfTargetActive(); + this->enforceCameraMovementBounds(); } } -void URTSCamera::FollowTarget(AActor* Target) + +void URTSCamera::followTarget(AActor* target) { - this->CameraFollowTarget = Target; + /// 设置物理跟随标记,相机将进入逐帧对齐模式 + this->activeCameraFollowTarget = target; } -void URTSCamera::UnFollowTarget() +void URTSCamera::unFollowTarget() { - this->CameraFollowTarget = nullptr; + /// 解除当前的相机跟随关系 + this->activeCameraFollowTarget = nullptr; } -void URTSCamera::OnZoomCamera(const FInputActionValue& Value) +void URTSCamera::onZoomCameraActionTriggered(const FInputActionValue& value) { - /// 更新意图缩放距离 - this->DesiredZoomLength = FMath::Clamp( - this->DesiredZoomLength + Value.Get() * this->ZoomSpeed, - this->MinimumZoomLength, - this->MaximumZoomLength + /// 更新意图缩放距离并通过该参数联动调整移动阻尼感 + this->desiredZoomLength = FMath::Clamp( + this->desiredZoomLength + value.Get() * this->zoomSpeed, + this->minimumZoomLength, + this->maximumZoomLength ); - /// 计算缩放比例并动态调整移动感官速度 - float Alpha = (this->DesiredZoomLength - this->MinimumZoomLength) / (this->MaximumZoomLength - this->MinimumZoomLength); - this->CurrentMovementSpeed = FMath::Lerp(this->MinMoveSpeed, this->MaxMoveSpeed, Alpha); + float speedAlpha = (this->desiredZoomLength - this->minimumZoomLength) / (this->maximumZoomLength - this->minimumZoomLength); + this->currentMovementSpeed = FMath::Lerp(this->minMovementSpeed, this->maxMovementSpeed, speedAlpha); - /// 基于意图高度即时刷新小地图视野框 - this->UpdateMinimapFrustum(); + /// 缩放意图发生的瞬间即时投射視野框,确保 UI 的战略响应无延迟 + this->updateMinimapFrustum(); } -void URTSCamera::OnRotateCamera(const FInputActionValue& Value) +void URTSCamera::onRotateCameraActionTriggered(const FInputActionValue& value) { - /// 获取当前根组件旋转并应用水平旋转增量 - const auto WorldRotation = this->RootComponent->GetComponentRotation(); - this->RootComponent->SetWorldRotation( + /// 处理水平偏航角输入并更新世界变换 + const auto actorRotation = this->rootComponent->GetComponentRotation(); + this->rootComponent->SetWorldRotation( FRotator::MakeFromEuler( FVector( - WorldRotation.Euler().X, - WorldRotation.Euler().Y, - WorldRotation.Euler().Z + Value.Get() + actorRotation.Euler().X, + actorRotation.Euler().Y, + actorRotation.Euler().Z + value.Get() ) ) ); - this->UpdateMinimapFrustum(); + this->updateMinimapFrustum(); } -void URTSCamera::OnTurnCameraLeft(const FInputActionValue&) +void URTSCamera::onTurnCameraLeftActionTriggered(const FInputActionValue&) { - /// 向左平滑旋转相机 - const auto WorldRotation = this->RootComponent->GetRelativeRotation(); - this->RootComponent->SetRelativeRotation( + /// 向左执行定量的步进式偏转 + const auto relativeRot = this->rootComponent->GetRelativeRotation(); + this->rootComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( - WorldRotation.Euler().X, - WorldRotation.Euler().Y, - WorldRotation.Euler().Z - this->RotateSpeed + relativeRot.Euler().X, + relativeRot.Euler().Y, + relativeRot.Euler().Z - this->rotationSpeed ) ) ); - this->UpdateMinimapFrustum(); + this->updateMinimapFrustum(); } -void URTSCamera::OnTurnCameraRight(const FInputActionValue&) +void URTSCamera::onTurnCameraRightActionTriggered(const FInputActionValue&) { - /// 向右平滑旋转相机 - const auto WorldRotation = this->RootComponent->GetRelativeRotation(); - this->RootComponent->SetRelativeRotation( + /// 向右执行定量的步进式偏转 + const auto relativeRot = this->rootComponent->GetRelativeRotation(); + this->rootComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( - WorldRotation.Euler().X, - WorldRotation.Euler().Y, - WorldRotation.Euler().Z + this->RotateSpeed + relativeRot.Euler().X, + relativeRot.Euler().Y, + relativeRot.Euler().Z + this->rotationSpeed ) ) ); - this->UpdateMinimapFrustum(); + this->updateMinimapFrustum(); } -void URTSCamera::OnMoveCameraYAxis(const FInputActionValue& Value) +void URTSCamera::onMoveCameraYAxisActionTriggered(const FInputActionValue& value) { - /// 根据相机当前朝向请求垂直(Y轴)移动指令 - this->RequestMoveCamera( - this->SpringArmComponent->GetForwardVector().X, - this->SpringArmComponent->GetForwardVector().Y, - Value.Get() + /// 处理纵向平移请求 + this->requestCameraMovement( + this->springArmComponent->GetForwardVector().X, + this->springArmComponent->GetForwardVector().Y, + value.Get() ); } -void URTSCamera::OnMoveCameraXAxis(const FInputActionValue& Value) +void URTSCamera::onMoveCameraXAxisActionTriggered(const FInputActionValue& value) { - /// 根据相机当前朝向请求水平(X轴)移动指令 - this->RequestMoveCamera( - this->SpringArmComponent->GetRightVector().X, - this->SpringArmComponent->GetRightVector().Y, - Value.Get() + /// 处理横向平移请求 + this->requestCameraMovement( + this->springArmComponent->GetRightVector().X, + this->springArmComponent->GetRightVector().Y, + value.Get() ); } -void URTSCamera::OnDragCamera(const FInputActionValue& Value) +void URTSCamera::onDragCameraActionTriggered(const FInputActionValue& value) { - /// 开始拖拽:记录初始鼠标位置 - if (!this->IsDragging && Value.Get()) + /// 记录拖拽起始状态 + if (!this->isDragging && value.Get()) { - this->IsDragging = true; - this->DragStartLocation = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + this->isDragging = true; + this->dragInteractionInitialLocation = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); } - /// 拖拽中:计算位置增量并映射到移动指令 - else if (this->IsDragging && Value.Get()) + /// 在激活期间计算平滑增量并转换为运动指令 + else if (this->isDragging && value.Get()) { - const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - auto DragExtents = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - DragExtents *= DragExtent; - - auto Delta = MousePosition - this->DragStartLocation; - Delta.X = FMath::Clamp(Delta.X, -DragExtents.X, DragExtents.X) / DragExtents.X; - Delta.Y = FMath::Clamp(Delta.Y, -DragExtents.Y, DragExtents.Y) / DragExtents.Y; - - this->RequestMoveCamera( - this->SpringArmComponent->GetRightVector().X, - this->SpringArmComponent->GetRightVector().Y, - Delta.X + const auto mousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + auto viewportSizeExtent = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); + viewportSizeExtent *= dragExtent; + + auto dragDelta = mousePosition - this->dragInteractionInitialLocation; + dragDelta.X = FMath::Clamp(dragDelta.X, -viewportSizeExtent.X, viewportSizeExtent.X) / viewportSizeExtent.X; + dragDelta.Y = FMath::Clamp(dragDelta.Y, -viewportSizeExtent.Y, viewportSizeExtent.Y) / viewportSizeExtent.Y; + + this->requestCameraMovement( + this->springArmComponent->GetRightVector().X, + this->springArmComponent->GetRightVector().Y, + dragDelta.X ); - this->RequestMoveCamera( - this->SpringArmComponent->GetForwardVector().X, - this->SpringArmComponent->GetForwardVector().Y, - Delta.Y * -1 + this->requestCameraMovement( + this->springArmComponent->GetForwardVector().X, + this->springArmComponent->GetForwardVector().Y, + dragDelta.Y * -1 ); } - /// 停止拖拽 - else if (this->IsDragging && !Value.Get()) + /// 清理拖拽标记 + else if (this->isDragging && !value.Get()) { - this->IsDragging = false; + this->isDragging = false; } } -void URTSCamera::RequestMoveCamera(const float X, const float Y, const float Scale) +void URTSCamera::requestCameraMovement(const float x, const float y, const float scale) { - FMoveCameraCommand MoveCameraCommand; - MoveCameraCommand.X = X; - MoveCameraCommand.Y = Y; - MoveCameraCommand.Scale = Scale; - MoveCameraCommands.Push(MoveCameraCommand); + /// 将运动请求压入队列,供 Tick 阶段统一消化 + FMoveCameraCommand movementCmd; + movementCmd.x = x; + movementCmd.y = y; + movementCmd.scale = scale; + this->pendingMovementCommands.Push(movementCmd); } -void URTSCamera::ApplyMoveCameraCommands() +void URTSCamera::applyAccumulatedMovementCommands() { - /// 遍历执行积压的移动指令,应用当前速度并完成平移 - for (const auto& [X, Y, Scale] : this->MoveCameraCommands) + /// 执行帧内所有挂起的平移指令并清空 + for (const auto& [x, y, scale] : this->pendingMovementCommands) { - auto Movement = FVector2D(X, Y); - Movement.Normalize(); - Movement *= this->CurrentMovementSpeed * Scale * this->DeltaSeconds; + auto directionVector = FVector2D(x, y); + directionVector.Normalize(); + directionVector *= this->currentMovementSpeed * scale * this->deltaSeconds; - this->JumpTo( - this->RootComponent->GetComponentLocation() + FVector(Movement.X, Movement.Y, 0.0f) + this->jumpTo( + this->rootComponent->GetComponentLocation() + FVector(directionVector.X, directionVector.Y, 0.0f) ); } - this->MoveCameraCommands.Empty(); + this->pendingMovementCommands.Empty(); } -void URTSCamera::CollectComponentDependencyReferences() +void URTSCamera::resolveComponentDependencyPointers() { - /// 缓存所有者及其核心组件指针以供快速访问 - this->CameraOwner = this->GetOwner(); - this->RootComponent = this->CameraOwner->GetRootComponent(); - this->CameraComponent = Cast(this->CameraOwner->GetComponentByClass(UCameraComponent::StaticClass())); - this->SpringArmComponent = Cast(this->CameraOwner->GetComponentByClass(USpringArmComponent::StaticClass())); - this->RTSPlayerController = UGameplayStatics::GetPlayerController(this->GetWorld(), 0); + /// 获取所有关键组件的指针,作为该插件架构的基石 + this->cameraOwner = this->GetOwner(); + this->rootComponent = this->cameraOwner->GetRootComponent(); + this->cameraComponent = Cast(this->cameraOwner->GetComponentByClass(UCameraComponent::StaticClass())); + this->springArmComponent = Cast(this->cameraOwner->GetComponentByClass(USpringArmComponent::StaticClass())); + this->realTimeStrategyPlayerController = UGameplayStatics::GetPlayerController(this->GetWorld(), 0); } -void URTSCamera::ConfigureSpringArm() +void URTSCamera::setupInitialSpringArmState() { - /// 初始化弹簧臂臂长及旋转参数 - this->DesiredZoomLength = this->MinimumZoomLength; - this->SpringArmComponent->TargetArmLength = this->DesiredZoomLength; - this->SpringArmComponent->bDoCollisionTest = false; - this->SpringArmComponent->bEnableCameraLag = this->EnableCameraLag; - this->SpringArmComponent->bEnableCameraRotationLag = this->EnableCameraRotationLag; - this->SpringArmComponent->SetRelativeRotation( + /// 配置弹簧臂的物理约束与渲染初始位姿 + this->desiredZoomLength = this->minimumZoomLength; + this->springArmComponent->TargetArmLength = this->desiredZoomLength; + this->springArmComponent->bDoCollisionTest = false; + this->springArmComponent->bEnableCameraLag = this->enableCameraLag; + this->springArmComponent->bEnableCameraRotationLag = this->enableCameraRotationLag; + this->springArmComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( 0.0, - this->StartingYAngle, - this->StartingZAngle + this->startingPitchAngle, + this->startingYawAngle ) ) ); } -void URTSCamera::TryToFindBoundaryVolumeReference() +void URTSCamera::locateMapBoundaryVolumeByTag() { - /// 通过标签检索地图中的相机边界体积 - TArray BlockingVolumes; + /// 在世界中通过静态标签检索用于运动边界约束的 Actor + TArray results; UGameplayStatics::GetAllActorsOfClassWithTag( this->GetWorld(), AActor::StaticClass(), - this->CameraBlockingVolumeTag, - BlockingVolumes + this->cameraBlockingVolumeTag, + results ); - if (BlockingVolumes.Num() > 0) + if (results.Num() > 0) { - this->BoundaryVolume = BlockingVolumes[0]; - - /// 初始化预分配视野点数组并触发初次刷新 - MinimapFrustumPoints.SetNum(4); - this->UpdateMinimapFrustum(); + this->movementBoundaryVolume = results[0]; + /// 初始化視野投影静态缓存点并执行初次同步 + this->updateMinimapFrustum(); } } -void URTSCamera::ConditionallyEnableEdgeScrolling() +void URTSCamera::configureInputModeForEdgeScrolling() { - /// 如已启用,则将鼠标锁定并在视口中显示 - if (this->EnableEdgeScrolling) + /// 当鼠标用于边缘滚动时,强制应用视口锁定策略 + if (this->enableEdgeScrolling) { - FInputModeGameAndUI InputMode; - InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways); - InputMode.SetHideCursorDuringCapture(false); - this->RTSPlayerController->SetInputMode(InputMode); + FInputModeGameAndUI gameModeSettings; + gameModeSettings.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways); + gameModeSettings.SetHideCursorDuringCapture(false); + this->realTimeStrategyPlayerController->SetInputMode(gameModeSettings); } } -void URTSCamera::CheckForEnhancedInputComponent() +void URTSCamera::validateEnhancedInputAvailability() { - /// 检查是否配置了增强输入组件,若缺失则打印视觉错误提示 - if (Cast(this->RTSPlayerController->InputComponent) == nullptr) + /// 校验当前的 InputComponent 是否兼容 Enhanced Input 语法 + if (Cast(this->realTimeStrategyPlayerController->InputComponent) == nullptr) { UKismetSystemLibrary::PrintString( this->GetWorld(), - TEXT("Error: Enhanced input component not found. Check Project Settings > Input."), true, true, + TEXT("Warning: RTSCamera requires Enhanced Input Component! Check Project Settings."), true, true, FLinearColor::Red, 100 ); } } -void URTSCamera::BindInputMappingContext() +void URTSCamera::registerInputMappingContext() { - /// 绑定增强输入上下文,并显示鼠标光标 - if (RTSPlayerController && RTSPlayerController->GetLocalPlayer()) + /// 向玩家输入子系统注册相机专用的映射上下文 + if (this->realTimeStrategyPlayerController && this->realTimeStrategyPlayerController->GetLocalPlayer()) { - if (const auto Input = RTSPlayerController->GetLocalPlayer()->GetSubsystem()) + if (const auto inputSystem = this->realTimeStrategyPlayerController->GetLocalPlayer()->GetSubsystem()) { - RTSPlayerController->bShowMouseCursor = true; + this->realTimeStrategyPlayerController->bShowMouseCursor = true; - if (!Input->HasMappingContext(this->InputMappingContext)) + if (!inputSystem->HasMappingContext(this->inputMappingContext)) { - Input->AddMappingContext(this->InputMappingContext, 0); + inputSystem->AddMappingContext(this->inputMappingContext, 0); } } } } -void URTSCamera::BindInputActions() +void URTSCamera::bindActionCallbacks() { - /// 将输入动作事件绑定到本地处理函数 - if (const auto EnhancedInputComponent = Cast(this->RTSPlayerController->InputComponent)) + /// 执行运动指令与 C++ 响应函数的逻辑挂挂接 + if (const auto eic = Cast(this->realTimeStrategyPlayerController->InputComponent)) { - EnhancedInputComponent->BindAction( - this->ZoomCamera, - ETriggerEvent::Triggered, - this, - &URTSCamera::OnZoomCamera - ); - - EnhancedInputComponent->BindAction( - this->RotateCameraAxis, - ETriggerEvent::Triggered, - this, - &URTSCamera::OnRotateCamera - ); - - EnhancedInputComponent->BindAction( - this->TurnCameraLeft, - ETriggerEvent::Triggered, - this, - &URTSCamera::OnTurnCameraLeft - ); - - EnhancedInputComponent->BindAction( - this->TurnCameraRight, - ETriggerEvent::Triggered, - this, - &URTSCamera::OnTurnCameraRight - ); - - EnhancedInputComponent->BindAction( - this->MoveCameraXAxis, - ETriggerEvent::Triggered, - this, - &URTSCamera::OnMoveCameraXAxis - ); - - EnhancedInputComponent->BindAction( - this->MoveCameraYAxis, - ETriggerEvent::Triggered, - this, - &URTSCamera::OnMoveCameraYAxis - ); - - EnhancedInputComponent->BindAction( - this->DragCamera, - ETriggerEvent::Triggered, - this, - &URTSCamera::OnDragCamera - ); + eic->BindAction(this->zoomCameraAction, ETriggerEvent::Triggered, this, &URTSCamera::onZoomCameraActionTriggered); + eic->BindAction(this->rotateCameraAxisAction, ETriggerEvent::Triggered, this, &URTSCamera::onRotateCameraActionTriggered); + eic->BindAction(this->turnCameraLeftAction, ETriggerEvent::Triggered, this, &URTSCamera::onTurnCameraLeftActionTriggered); + eic->BindAction(this->turnCameraRightAction, ETriggerEvent::Triggered, this, &URTSCamera::onTurnCameraRightActionTriggered); + eic->BindAction(this->moveCameraXAxisAction, ETriggerEvent::Triggered, this, &URTSCamera::onMoveCameraXAxisActionTriggered); + eic->BindAction(this->moveCameraYAxisAction, ETriggerEvent::Triggered, this, &URTSCamera::onMoveCameraYAxisActionTriggered); + eic->BindAction(this->dragCameraAction, ETriggerEvent::Triggered, this, &URTSCamera::onDragCameraActionTriggered); } } -void URTSCamera::SetActiveCamera() +void URTSCamera::setActiveCamera() { - /// 将当前控制器的观察目标设置为本组件的所有者 - this->RTSPlayerController->SetViewTarget(this->GetOwner()); + /// 将玩家当前的渲染视角强制聚焦于此组件 + this->realTimeStrategyPlayerController->SetViewTarget(this->GetOwner()); } -void URTSCamera::JumpTo(const FVector Position) +void URTSCamera::jumpTo(const FVector position) { - /// 保持当前高度不变,更新水平位置并刷新视野 - float CurrentZ = this->RootComponent->GetComponentLocation().Z; - this->RootComponent->SetWorldLocation(FVector(Position.X, Position.Y, CurrentZ)); - this->UpdateMinimapFrustum(); + /// 执行瞬时的视变换同步,并触发视野投影点手动刷新 + float cachedZ = this->rootComponent->GetComponentLocation().Z; + this->rootComponent->SetWorldLocation(FVector(position.X, position.Y, cachedZ)); + this->updateMinimapFrustum(); } -void URTSCamera::ConditionallyPerformEdgeScrolling() +void URTSCamera::executeEdgeScrollingEvaluation() { - /// 如已启用边缘滚动且当前未处于鼠标拖拽状态,则执行视口边缘推进逻辑 - if (this->EnableEdgeScrolling && !this->IsDragging) + /// 仅在功能开启且未进行拖拽干扰时,执行屏幕边缘检测 + if (this->enableEdgeScrolling && !this->isDragging) { - const FVector OldLocation = this->RootComponent->GetComponentLocation(); + const FVector locationBeforePush = this->rootComponent->GetComponentLocation(); - this->EdgeScrollLeft(); - this->EdgeScrollRight(); - this->EdgeScrollUp(); - this->EdgeScrollDown(); + this->performEdgeScrollLeft(); + this->performEdgeScrollRight(); + this->performEdgeScrollUp(); + this->performEdgeScrollDown(); - /// 位置发生变动时立即同步视野框 - if (!this->RootComponent->GetComponentLocation().Equals(OldLocation, 0.1f)) + if (!this->rootComponent->GetComponentLocation().Equals(locationBeforePush, 0.1f)) { - this->UpdateMinimapFrustum(); + this->updateMinimapFrustum(); } } } -void URTSCamera::EdgeScrollLeft() +void URTSCamera::performEdgeScrollLeft() { - /// 计算鼠标在左侧边缘的深度,并以此确定摄像机左移量 - const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedPosition = 1 - UKismetMathLibrary::NormalizeToRange( - MousePosition.X, - 0.0f, - ViewportSize.X * this->DistanceFromEdgeThreshold - ); + /// 基于鼠标左偏移计算平移推力 + const auto mp = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + const auto vs = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); + const auto normalizedVal = 1 - UKismetMathLibrary::NormalizeToRange(mp.X, 0.0f, vs.X * this->distanceFromEdgeThreshold); - const float MovementAlpha = UKismetMathLibrary::FClamp(NormalizedPosition, 0.0, 1.0); - - this->RootComponent->AddRelativeLocation( - -1 * this->RootComponent->GetRightVector() * MovementAlpha * this->CurrentMovementSpeed * this->DeltaSeconds - ); + const float alpha = UKismetMathLibrary::FClamp(normalizedVal, 0.0, 1.0); + this->rootComponent->AddRelativeLocation(-1 * this->rootComponent->GetRightVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } -void URTSCamera::EdgeScrollRight() +void URTSCamera::performEdgeScrollRight() { - /// 计算鼠标在右侧边缘的深度,并以此确定摄像机右移量 - const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedPosition = UKismetMathLibrary::NormalizeToRange( - MousePosition.X, - ViewportSize.X * (1 - this->DistanceFromEdgeThreshold), - ViewportSize.X - ); + /// 基于鼠标右偏移计算平移推力 + const auto mp = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + const auto vs = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); + const auto normalizedVal = UKismetMathLibrary::NormalizeToRange(mp.X, vs.X * (1 - this->distanceFromEdgeThreshold), vs.X); - const float MovementAlpha = UKismetMathLibrary::FClamp(NormalizedPosition, 0.0, 1.0); - this->RootComponent->AddRelativeLocation( - this->RootComponent->GetRightVector() * MovementAlpha * this->CurrentMovementSpeed * this->DeltaSeconds - ); + const float alpha = UKismetMathLibrary::FClamp(normalizedVal, 0.0, 1.0); + this->rootComponent->AddRelativeLocation(this->rootComponent->GetRightVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } -void URTSCamera::EdgeScrollUp() +void URTSCamera::performEdgeScrollUp() { - /// 计算鼠标在上侧边缘的深度,并以此确定摄像机前移量 - const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedPosition = UKismetMathLibrary::NormalizeToRange( - MousePosition.Y, - 0.0f, - ViewportSize.Y * this->DistanceFromEdgeThreshold - ); + /// 基于鼠标上偏移计算平移推力 + const auto mp = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + const auto vs = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); + const auto normalizedVal = UKismetMathLibrary::NormalizeToRange(mp.Y, 0.0f, vs.Y * this->distanceFromEdgeThreshold); - const float MovementAlpha = 1 - UKismetMathLibrary::FClamp(NormalizedPosition, 0.0, 1.0); - this->RootComponent->AddRelativeLocation( - this->RootComponent->GetForwardVector() * MovementAlpha * this->CurrentMovementSpeed * this->DeltaSeconds - ); + const float alpha = 1 - UKismetMathLibrary::FClamp(normalizedVal, 0.0, 1.0); + this->rootComponent->AddRelativeLocation(this->rootComponent->GetForwardVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } -void URTSCamera::EdgeScrollDown() +void URTSCamera::performEdgeScrollDown() { - /// 计算鼠标在下侧边缘的深度,并以此确定摄像机后移量 - const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedPosition = UKismetMathLibrary::NormalizeToRange( - MousePosition.Y, - ViewportSize.Y * (1 - this->DistanceFromEdgeThreshold), - ViewportSize.Y - ); + /// 基于鼠标下偏移计算平移推力 + const auto mp = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + const auto vs = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); + const auto normalizedVal = UKismetMathLibrary::NormalizeToRange(mp.Y, vs.Y * (1 - this->distanceFromEdgeThreshold), vs.Y); - const float MovementAlpha = UKismetMathLibrary::FClamp(NormalizedPosition, 0.0, 1.0); - this->RootComponent->AddRelativeLocation( - -1 * this->RootComponent->GetForwardVector() * MovementAlpha * this->CurrentMovementSpeed * this->DeltaSeconds - ); + const float alpha = UKismetMathLibrary::FClamp(normalizedVal, 0.0, 1.0); + this->rootComponent->AddRelativeLocation(-1 * this->rootComponent->GetForwardVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } -void URTSCamera::FollowTargetIfSet() +void URTSCamera::updateFollowPositionIfTargetActive() { - if (this->CameraFollowTarget != nullptr) + /// 将相机根坐标强行锚定在追随目标之上 + if (this->activeCameraFollowTarget != nullptr) { - this->JumpTo(this->CameraFollowTarget->GetActorLocation()); + this->jumpTo(this->activeCameraFollowTarget->GetActorLocation()); } } -void URTSCamera::SmoothTargetArmLengthToDesiredZoom() +void URTSCamera::handleTargetArmLengthInterpolation() { - /// 使用平滑插值算法将弹簧臂长度拉近至目标意图长度 - float OldArmLength = this->SpringArmComponent->TargetArmLength; - - this->SpringArmComponent->TargetArmLength = FMath::FInterpTo( - this->SpringArmComponent->TargetArmLength, - this->DesiredZoomLength, - this->DeltaSeconds, - this->ZoomCatchupSpeed + /// 基于 deltaSeconds 驱动缩放插值,完善物理层的顺滑过渡 + this->springArmComponent->TargetArmLength = FMath::FInterpTo( + this->springArmComponent->TargetArmLength, + this->desiredZoomLength, + this->deltaSeconds, + this->zoomCatchupSpeed ); } -void URTSCamera::ConditionallyKeepCameraAtDesiredZoomAboveGround() +void URTSCamera::rectifyRootHeightFromTerrain() { - /// 动态调整相机高度以贴合地形 - if (this->EnableDynamicCameraHeight) + /// 射线补偿检测:使根节点坐标实时贴合地形海拔 + if (this->enableDynamicCameraHeight) { - const FVector RootWorldLocation = this->RootComponent->GetComponentLocation(); - const TArray ActorsToIgnore; + const FVector currentRootXYZ = this->rootComponent->GetComponentLocation(); + const TArray excluded; - FHitResult HitResult; - bool bDidHit = UKismetSystemLibrary::LineTraceSingle( + FHitResult floorHit; + bool bValidFloor = UKismetSystemLibrary::LineTraceSingle( this->GetWorld(), - FVector(RootWorldLocation.X, RootWorldLocation.Y, RootWorldLocation.Z + this->FindGroundTraceLength), - FVector(RootWorldLocation.X, RootWorldLocation.Y, RootWorldLocation.Z - this->FindGroundTraceLength), - UEngineTypes::ConvertToTraceType(this->CollisionChannel), + FVector(currentRootXYZ.X, currentRootXYZ.Y, currentRootXYZ.Z + this->findGroundTraceLength), + FVector(currentRootXYZ.X, currentRootXYZ.Y, currentRootXYZ.Z - this->findGroundTraceLength), + UEngineTypes::ConvertToTraceType(this->collisionChannel), true, - ActorsToIgnore, - EDrawDebugTrace::Type::None, - HitResult, + excluded, + EDrawDebugTrace::None, + floorHit, true ); - if (bDidHit) + if (bValidFloor) { - this->RootComponent->SetWorldLocation( - FVector( - HitResult.Location.X, - HitResult.Location.Y, - HitResult.Location.Z - ) - ); + this->rootComponent->SetWorldLocation(FVector(floorHit.Location.X, floorHit.Location.Y, floorHit.Location.Z)); } } } -void URTSCamera::ConditionallyApplyCameraBounds() +void URTSCamera::enforceCameraMovementBounds() { - /// 如已定义边界,则将当前位置限制在边界体积范围内 - if (this->BoundaryVolume != nullptr) + /// 强制检查并将相机坐标拉回预定义的体积包围阵列内 + if (this->movementBoundaryVolume != nullptr) { - const FVector CurrentWorldLocation = this->RootComponent->GetComponentLocation(); - FVector Origin; - FVector Extents; - this->BoundaryVolume->GetActorBounds(false, Origin, Extents); + const FVector posToClamp = this->rootComponent->GetComponentLocation(); + FVector boxOrigin; + FVector boxExtents; + this->movementBoundaryVolume->GetActorBounds(false, boxOrigin, boxExtents); - this->RootComponent->SetWorldLocation( + this->rootComponent->SetWorldLocation( FVector( - UKismetMathLibrary::Clamp(CurrentWorldLocation.X, Origin.X - Extents.X, Origin.X + Extents.X), - UKismetMathLibrary::Clamp(CurrentWorldLocation.Y, Origin.Y - Extents.Y, Origin.Y + Extents.Y), - CurrentWorldLocation.Z + UKismetMathLibrary::Clamp(posToClamp.X, boxOrigin.X - boxExtents.X, boxOrigin.X + boxExtents.X), + UKismetMathLibrary::Clamp(posToClamp.Y, boxOrigin.Y - boxExtents.Y, boxOrigin.Y + boxExtents.Y), + posToClamp.Z ) ); } } -void URTSCamera::UpdateMinimapFrustum() +void URTSCamera::updateMinimapFrustum() { - if (!SpringArmComponent || !CameraComponent || !RootComponent) return; + /// 战略視野投影核心逻辑:将缩放意图(DesiredZoomLength)直接转化为地平面的投影区域,确保 UI 响应不受物理插值影响 + if (!this->springArmComponent || !this->cameraComponent || !this->rootComponent) return; - /// 1. 提取当前相机的策略参数 (X, Y, Z_Intent, Pitch, Yaw) - FVector RootLocation = RootComponent->GetComponentLocation(); - FRotator RootRotation = RootComponent->GetComponentRotation(); - FRotator ArmRotation = SpringArmComponent->GetRelativeRotation(); + const FVector rPos = this->rootComponent->GetComponentLocation(); + const FRotator rRot = this->rootComponent->GetComponentRotation(); + const FRotator aRot = this->springArmComponent->GetRelativeRotation(); - /// 合成逻辑相机旋转 (使用根组件的 Yaw 和弹簧臂的 Pitch) - FRotator LogicCameraRotation = RootRotation + ArmRotation; + /// 合成相机的总逻辑朝向 + const FRotator logicalRotation = rRot + aRot; - /// 2. 使用意图高度 (DesiredZoomLength) 计算相机逻辑位置 - float CameraArmLength = this->DesiredZoomLength; - FVector LogicCameraLocation = RootLocation + LogicCameraRotation.Vector() * (-CameraArmLength); + /// 关键点:使用 DesiredZoomLength 预判物理终点位置 + const float intentLength = this->desiredZoomLength; + const FVector logicalOrigin = rPos + logicalRotation.Vector() * (-intentLength); - /// 3. 获取相机内参并计算视野张角 - float FieldOfView = CameraComponent->FieldOfView; - float AspectRatio = CameraComponent->AspectRatio; - if(AspectRatio <= 0.0f) AspectRatio = 1.777f; + const float fovValue = this->cameraComponent->FieldOfView; + float arValue = this->cameraComponent->AspectRatio; + if(arValue <= 0.0f) arValue = 1.777f; - float HalfHorizontalFOV = FMath::DegreesToRadians(FieldOfView) / 2.0f; - float HalfVerticalFOV = FMath::Atan(FMath::Tan(HalfHorizontalFOV) / AspectRatio); + const float hFOV = FMath::DegreesToRadians(fovValue) / 2.0f; + const float vFOV = FMath::Atan(FMath::Tan(hFOV) / arValue); - float TangentHorizontal = FMath::Tan(HalfHorizontalFOV); - float TangentVertical = FMath::Tan(HalfVerticalFOV); + const float tanH = FMath::Tan(hFOV); + const float tanV = FMath::Tan(vFOV); - /// 4. 计算视野四个方向的射线向量 - FVector ForwardVector = LogicCameraRotation.Vector(); - FVector RightVector = FRotationMatrix(LogicCameraRotation).GetScaledAxis(EAxis::Y); - FVector UpVector = FRotationMatrix(LogicCameraRotation).GetScaledAxis(EAxis::Z); + const FVector forwardVector = logicalRotation.Vector(); + const FVector rightVector = FRotationMatrix(logicalRotation).GetScaledAxis(EAxis::Y); + const FVector upVector = FRotationMatrix(logicalRotation).GetScaledAxis(EAxis::Z); - FVector DirectionTopLeft = (ForwardVector - RightVector * TangentHorizontal + UpVector * TangentVertical).GetSafeNormal(); - FVector DirectionTopRight = (ForwardVector + RightVector * TangentHorizontal + UpVector * TangentVertical).GetSafeNormal(); - FVector DirectionBottomLeft = (ForwardVector - RightVector * TangentHorizontal - UpVector * TangentVertical).GetSafeNormal(); - FVector DirectionBottomRight = (ForwardVector + RightVector * TangentHorizontal - UpVector * TangentVertical).GetSafeNormal(); + /// 计算四个边界射线 + const FVector trDir = (forwardVector + rightVector * tanH + upVector * tanV).GetSafeNormal(); + const FVector tlDir = (forwardVector - rightVector * tanH + upVector * tanV).GetSafeNormal(); + const FVector brDir = (forwardVector + rightVector * tanH - upVector * tanV).GetSafeNormal(); + const FVector blDir = (forwardVector - rightVector * tanH - upVector * tanV).GetSafeNormal(); - /// 5. 计算射线与地平面的交点进行位置投射 - float GroundZ = this->RootComponent->GetComponentLocation().Z; + const float gZ = this->rootComponent->GetComponentLocation().Z; - auto IntersectGround = [&](const FVector& RayOrigin, const FVector& RayDirection) -> FVector + auto calcInt = [&](const FVector& ro, const FVector& rd) -> FVector { - if (RayDirection.Z >= -0.001f) return RayOrigin + RayDirection * 100000.0f; - float t = (GroundZ - RayOrigin.Z) / RayDirection.Z; - if (t < 0.0f) return RayOrigin + RayDirection * 100000.0f; - return RayOrigin + t * RayDirection; + if (rd.Z >= -0.001f) return ro + rd * 100000.0f; + float t = (gZ - ro.Z) / rd.Z; + if (t < 0.0f) return ro + rd * 100000.0f; + return ro + t * rd; }; - /// 6. 更新视野点并缓存同步至 UI - if (MinimapFrustumPoints.Num() != 4) - { - MinimapFrustumPoints.SetNum(4); - } + /// 填充静态数据数组,供外部 Widget 直接读取以降低渲染层级的内存开销 + this->minimapFrustumPoints[0] = calcInt(logicalOrigin, tlDir); + this->minimapFrustumPoints[1] = calcInt(logicalOrigin, trDir); + this->minimapFrustumPoints[2] = calcInt(logicalOrigin, brDir); + this->minimapFrustumPoints[3] = calcInt(logicalOrigin, blDir); - MinimapFrustumPoints[0] = IntersectGround(LogicCameraLocation, DirectionTopLeft); - MinimapFrustumPoints[1] = IntersectGround(LogicCameraLocation, DirectionTopRight); - MinimapFrustumPoints[2] = IntersectGround(LogicCameraLocation, DirectionBottomRight); - MinimapFrustumPoints[3] = IntersectGround(LogicCameraLocation, DirectionBottomLeft); + /// 计算完成后发起广播,通知 UI 订阅者执行定向重绘 + this->onMinimapFrustumUpdated.Broadcast(); } diff --git a/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp index 70c9b79..6259166 100644 --- a/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp +++ b/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp @@ -7,223 +7,214 @@ #include "Kismet/GameplayStatics.h" #include "Rendering/DrawElements.h" - - URTSCameraMinimapWidget::URTSCameraMinimapWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer) { - SetVisibility(ESlateVisibility::Visible); + /// 初始化控件可见性,并默认设置绘制线宽 + this->SetVisibility(ESlateVisibility::Visible); } void URTSCameraMinimapWidget::NativeConstruct() { Super::NativeConstruct(); - // Force visibility and interaction flags at runtime to override BP defaults - SetVisibility(ESlateVisibility::Visible); - SetIsFocusable(true); + /// 锁定交互属性并执行初始控制器搜索 + this->SetVisibility(ESlateVisibility::Visible); + this->SetIsFocusable(true); - InitializeController(); + this->InitializeController(); } void URTSCameraMinimapWidget::InitializeController() { - if (APlayerController* PC = GetOwningPlayer()) + if (APlayerController* playerController = this->GetOwningPlayer()) { - PC->bEnableClickEvents = true; - PC->bEnableMouseOverEvents = true; + playerController->bEnableClickEvents = true; + playerController->bEnableMouseOverEvents = true; } - FindRTSCamera(); + this->findRTSCamera(); } -void URTSCameraMinimapWidget::FindRTSCamera() +void URTSCameraMinimapWidget::findRTSCamera() { - // 1. Try ViewTarget - if (!CachedRTSCamera) + /// 尝试从当前的观察目标或 Pawn 中定位 RTSCamera 组件 + if (!this->cachedRTSCamera) { - APlayerController* PC = GetOwningPlayer(); - if (PC) + APlayerController* playerController = this->GetOwningPlayer(); + if (playerController) { - if (AActor* ViewTarget = PC->GetViewTarget()) + if (AActor* viewTarget = playerController->GetViewTarget()) { - CachedRTSCamera = ViewTarget->FindComponentByClass(); + this->cachedRTSCamera = viewTarget->FindComponentByClass(); } - if (!CachedRTSCamera) + if (!this->cachedRTSCamera) { - if (APawn* Pawn = PC->GetPawn()) + if (APawn* pawn = playerController->GetPawn()) { - CachedRTSCamera = Pawn->FindComponentByClass(); + this->cachedRTSCamera = pawn->FindComponentByClass(); } } } } - // 3. Cache Dependencies - if (CachedRTSCamera) + /// 若成功捕获组件,则建立响应式订阅并缓存辅助引用 + if (this->cachedRTSCamera) { - AActor* Owner = CachedRTSCamera->GetOwner(); - if (Owner) - { - if (!CachedCameraComponent) CachedCameraComponent = Owner->FindComponentByClass(); - if (!CachedSpringArm) CachedSpringArm = Owner->FindComponentByClass(); - } + /// 绑定视野更新委托:由相机的“主动推送”驱动 UI 的“局部失效” + this->cachedRTSCamera->onMinimapFrustumUpdated.RemoveAll(this); + this->cachedRTSCamera->onMinimapFrustumUpdated.AddUObject(this, &URTSCameraMinimapWidget::handleMinimapFrustumUpdated); - // Update Bounds (Retry if invalid) - if (!bHasValidBounds) + AActor* cameraOwner = this->cachedRTSCamera->GetOwner(); + if (cameraOwner) { - if (AActor* BoundsActor = CachedRTSCamera->GetBoundaryVolume()) + if (!this->cachedCameraComponent) { - FVector Origin, Extent; - BoundsActor->GetActorBounds(false, Origin, Extent); - CachedBoundsOrigin = Origin; - CachedBoundsExtent = Extent; - bHasValidBounds = true; - - UE_LOG(LogOpenRTSCamera, Log, TEXT("RTSCameraMinimapWidget: Bounds Found! Origin=%s, Extent=%s"), *Origin.ToString(), *Extent.ToString()); + this->cachedCameraComponent = cameraOwner->FindComponentByClass(); } - else + if (!this->cachedSpringArm) { - static bool bLogOnce = false; - if (!bLogOnce) - { - UE_LOG(LogOpenRTSCamera, Warning, TEXT("RTSCameraMinimapWidget: RTSCamera found but BoundaryVolume is missing!")); - bLogOnce = true; - } + this->cachedSpringArm = cameraOwner->FindComponentByClass(); + } + } + + /// 初始同步地图边界数据 + if (!this->bHasValidBounds) + { + if (AActor* boundsActor = this->cachedRTSCamera->getMovementBoundaryVolume()) + { + FVector origin; + FVector extent; + boundsActor->GetActorBounds(false, origin, extent); + this->cachedBoundsOrigin = origin; + this->cachedBoundsExtent = extent; + this->bHasValidBounds = true; } } } } +void URTSCameraMinimapWidget::handleMinimapFrustumUpdated() +{ + /// 响应式重绘核心:仅在相机通过委托告知数据变动时,才标记 Slate 渲染层失效。 + /// 开发提示:配合 Invalidation Box 使用,可使本控件在静止状态下完全忽略 NativePaint 开销。 + this->Invalidate(EInvalidateWidgetReason::Paint); +} + FVector2D URTSCameraMinimapWidget::ConvertWorldToWidgetLocal(const FVector2D& WorldPos, const FVector2D& WidgetSize) const { - if (CachedBoundsExtent.X < KINDA_SMALL_NUMBER || CachedBoundsExtent.Y < KINDA_SMALL_NUMBER) return FVector2D::ZeroVector; + /// 将世界坐标系下的点线性映射至小地图控件的局部 0-1 空间,并适配轴向偏移 + if (this->cachedBoundsExtent.X < KINDA_SMALL_NUMBER || this->cachedBoundsExtent.Y < KINDA_SMALL_NUMBER) + { + return FVector2D::ZeroVector; + } - // Normalized X/Y in Bounds Space - float NormX = (WorldPos.X - (CachedBoundsOrigin.X - CachedBoundsExtent.X)) / (2.0f * CachedBoundsExtent.X); - float NormY = (WorldPos.Y - (CachedBoundsOrigin.Y - CachedBoundsExtent.Y)) / (2.0f * CachedBoundsExtent.Y); + float normalizedX = (WorldPos.X - (this->cachedBoundsOrigin.X - this->cachedBoundsExtent.X)) / (2.0f * this->cachedBoundsExtent.X); + float normalizedY = (WorldPos.Y - (this->cachedBoundsOrigin.Y - this->cachedBoundsExtent.Y)) / (2.0f * this->cachedBoundsExtent.Y); - // Map to Widget Space: World +X (North) -> Widget -Y, World +Y (East) -> Widget +X - return FVector2D(NormY * WidgetSize.X, (1.0f - NormX) * WidgetSize.Y); + return FVector2D(normalizedY * WidgetSize.X, (1.0f - normalizedX) * WidgetSize.Y); } FVector2D URTSCameraMinimapWidget::ConvertWidgetLocalToWorld(const FVector2D& LocalPos, const FVector2D& WidgetSize) const { - if (WidgetSize.X <= 0.0f || WidgetSize.Y <= 0.0f) return FVector2D::ZeroVector; + /// 将小地图局部像素坐标反投影回世界地图水平面的 X/Y 坐标 + if (WidgetSize.X <= 0.0f || WidgetSize.Y <= 0.0f) + { + return FVector2D::ZeroVector; + } - float U = LocalPos.X / WidgetSize.X; - float V = LocalPos.Y / WidgetSize.Y; + float uParam = LocalPos.X / WidgetSize.X; + float vParam = LocalPos.Y / WidgetSize.Y; - // Invert Coordinate Mapping: - // U = NormY => NormY = U - // V = 1.0 - NormX => NormX = 1.0 - V - float NormX = 1.0f - V; - float NormY = U; + float normalizedX = 1.0f - vParam; + float normalizedY = uParam; - float WorldX = (CachedBoundsOrigin.X - CachedBoundsExtent.X) + NormX * (2.0f * CachedBoundsExtent.X); - float WorldY = (CachedBoundsOrigin.Y - CachedBoundsExtent.Y) + NormY * (2.0f * CachedBoundsExtent.Y); + float worldX = (this->cachedBoundsOrigin.X - this->cachedBoundsExtent.X) + normalizedX * (2.0f * this->cachedBoundsExtent.X); + float worldY = (this->cachedBoundsOrigin.Y - this->cachedBoundsExtent.Y) + normalizedY * (2.0f * this->cachedBoundsExtent.Y); - return FVector2D(WorldX, WorldY); + return FVector2D(worldX, worldY); } -int32 URTSCameraMinimapWidget::NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const +int32 URTSCameraMinimapWidget::NativePaint( + const FPaintArgs& Args, + const FGeometry& AllottedGeometry, + const FSlateRect& MyCullingRect, + FSlateWindowElementList& OutDrawElements, + int32 LayerId, + const FWidgetStyle& InWidgetStyle, + bool bParentEnabled +) const { - // 1. Ensure Camera Reference - int32 MaxLayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); + /// 执行基础绘制流程。注:如果当前组件没有被 Invalidate,Slate 可能会完全跳过此函数执行。 + int32 maxLayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); - if (!CachedRTSCamera || !bHasValidBounds) + if (!this->cachedRTSCamera || !this->bHasValidBounds) { - const_cast(this)->FindRTSCamera(); + const_cast(this)->findRTSCamera(); } - if (!CachedRTSCamera || !bHasValidBounds || !CachedSpringArm || !CachedCameraComponent) + if (!this->cachedRTSCamera || !this->bHasValidBounds || !this->cachedSpringArm || !this->cachedCameraComponent) { - return MaxLayerId; + return maxLayerId; } - // 2. Read Cached Data (Source of Truth: RTSCamera) - const TArray& WorldPoints = CachedRTSCamera->MinimapFrustumPoints; - - // Debug Log - UE_LOG(LogOpenRTSCamera, Verbose, TEXT("MinimapWidget: CachedRTSCamera found. Points Num: %d"), WorldPoints.Num()); - - if (WorldPoints.Num() < 4) + FVector2D geometrySize = AllottedGeometry.GetLocalSize(); + if (geometrySize.X < 1.0f || geometrySize.Y < 1.0f) { - UE_LOG(LogOpenRTSCamera, Warning, TEXT("MinimapWidget: Not enough points (%d)"), WorldPoints.Num()); - return MaxLayerId; - } - - // 3. Convert & Draw - FVector2D LocalSize = AllottedGeometry.GetLocalSize(); - // Debug Widget Size to ensure it's not zero (layout issue) - if (LocalSize.X < 1.0f || LocalSize.Y < 1.0f) - { - UE_LOG(LogOpenRTSCamera, Warning, TEXT("RTSCameraMinimapWidget: LocalSize is too small! (%s). Check UMG layout (Wrap with SizeBox?)."), *LocalSize.ToString()); - } - else - { - // UE_LOG(LogOpenRTSCamera, Verbose, TEXT("RTSCameraMinimapWidget: LocalSize %s"), *LocalSize.ToString()); + return maxLayerId; } - TArray Points; - - for (const FVector& WorldPt : WorldPoints) + /// 从相机缓存中检索投影点并建立线条路径。此处的开销仅存在于重绘帧。 + TArray viewportDrawPoints; + for (int32 i = 0; i < 4; ++i) { - FVector2D ScreenPt = ConvertWorldToWidgetLocal(FVector2D(WorldPt), LocalSize); - Points.Add(ScreenPt); - // UE_LOG(LogOpenRTSCamera, Verbose, TEXT(" Pt: %s -> Screen: %s"), *WorldPt.ToString(), *ScreenPt.ToString()); + const FVector& worldPt = this->cachedRTSCamera->minimapFrustumPoints[i]; + FVector2D widgetPt = this->ConvertWorldToWidgetLocal(FVector2D(worldPt.X, worldPt.Y), geometrySize); + viewportDrawPoints.Add(widgetPt); } - // 4. Draw - if (Points.Num() > 0) + if (viewportDrawPoints.Num() > 0) { - const FVector2D FirstPoint = Points[0]; - Points.Add(FirstPoint); // Close Loop (Copy first to avoid reallocation crash) + viewportDrawPoints.Add(viewportDrawPoints[0]); } + /// 在指定图层上生成绘制指令 FSlateDrawElement::MakeLines( OutDrawElements, LayerId + 1, AllottedGeometry.ToPaintGeometry(), - Points, + viewportDrawPoints, ESlateDrawEffect::None, FLinearColor::White, true, - LineWidth + this->lineWidth ); - return MaxLayerId + 1; + return maxLayerId + 1; } FReply URTSCameraMinimapWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { - UE_LOG(LogOpenRTSCamera, Log, TEXT("MinimapWidget: OnMouseButtonDown (Button: %s)"), *InMouseEvent.GetEffectingButton().ToString()); + /// 响应点击:将屏幕点击直接转化为相机的战略突变 if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) { - bIsDragging = true; - if (CachedRTSCamera) + this->bIsDragging = true; + if (this->cachedRTSCamera) { - FVector2D ScreenPos = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); - FVector2D WorldPos = ConvertWidgetLocalToWorld( - ScreenPos, - InGeometry.GetLocalSize() - ); - - UE_LOG(LogOpenRTSCamera, Log, TEXT("Minimap Click: Screen=%s, World=%s. Jumping!"), *ScreenPos.ToString(), *WorldPos.ToString()); - - // Maintain current Z, don't force 0 if possible, or use JumpTo which might handle it. - // JumpTo set Z=0 in current impl. Let's trust JumpTo for now but log it. - CachedRTSCamera->JumpTo(FVector(WorldPos, 0.0f)); + FVector2D localPos = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); + FVector2D worldPos = this->ConvertWidgetLocalToWorld(localPos, InGeometry.GetLocalSize()); + this->cachedRTSCamera->jumpTo(FVector(worldPos.X, worldPos.Y, 0.0f)); } - return FReply::Handled().CaptureMouse(TakeWidget()); + return FReply::Handled().CaptureMouse(this->TakeWidget()); } return FReply::Unhandled(); } FReply URTSCameraMinimapWidget::NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { - if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && bIsDragging) + /// 释放拖拽锁 + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && this->bIsDragging) { - bIsDragging = false; + this->bIsDragging = false; return FReply::Handled().ReleaseMouseCapture(); } return FReply::Unhandled(); @@ -231,15 +222,14 @@ FReply URTSCameraMinimapWidget::NativeOnMouseButtonUp(const FGeometry& InGeometr FReply URTSCameraMinimapWidget::NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { - if (bIsDragging && HasMouseCapture()) + /// 拖拽追踪:连续更新相机位置 + if (this->bIsDragging && this->HasMouseCapture()) { - if (CachedRTSCamera) + if (this->cachedRTSCamera) { - FVector2D WorldPos = ConvertWidgetLocalToWorld( - InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()), - InGeometry.GetLocalSize() - ); - CachedRTSCamera->JumpTo(FVector(WorldPos, 0.0f)); + FVector2D localPos = InGeometry.AbsoluteToLocal(InMouseEvent.GetScreenSpacePosition()); + FVector2D worldPos = this->ConvertWidgetLocalToWorld(localPos, InGeometry.GetLocalSize()); + this->cachedRTSCamera->jumpTo(FVector(worldPos.X, worldPos.Y, 0.0f)); } return FReply::Handled(); } @@ -249,11 +239,9 @@ FReply URTSCameraMinimapWidget::NativeOnMouseMove(const FGeometry& InGeometry, c void URTSCameraMinimapWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) { Super::NativeOnMouseEnter(InGeometry, InMouseEvent); - UE_LOG(LogOpenRTSCamera, Log, TEXT("MinimapWidget: Mouse Entered Widget Area")); } void URTSCameraMinimapWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent) { Super::NativeOnMouseLeave(InMouseEvent); - UE_LOG(LogOpenRTSCamera, Log, TEXT("MinimapWidget: Mouse Left Widget Area")); } diff --git a/Source/OpenRTSCamera/Public/RTSCamera.h b/Source/OpenRTSCamera/Public/RTSCamera.h index 21c32c4..1444805 100644 --- a/Source/OpenRTSCamera/Public/RTSCamera.h +++ b/Source/OpenRTSCamera/Public/RTSCamera.h @@ -9,26 +9,34 @@ #include "GameFramework/SpringArmComponent.h" #include "RTSCamera.generated.h" +/** @brief 视野框数据更新时的多播委托声明 */ +DECLARE_MULTICAST_DELEGATE(FOnMinimapFrustumUpdated); + /** - * We use these commands so that move camera inputs can be tied to the tick rate of the game. - * https://github.com/HeyZoos/OpenRTSCamera/issues/27 - */ + * @brief 封装相机平移请求的指令结构 + **/ USTRUCT() struct FMoveCameraCommand { GENERATED_BODY() + + /// 目标位置在 X 轴上的分量增量 UPROPERTY() - float X = 0; + float x = 0; + + /// 目标位置在 Y 轴上的分量增量 UPROPERTY() - float Y = 0; + float y = 0; + + /// 本次移动指令的缩放权重比例 UPROPERTY() - float Scale = 0; + float scale = 0; }; /** - * @brief RTS 相机组件,负责处理相机的移动、旋转、缩放以及视野计算。 + * @brief RTS 相机组件,处理视口平移、边缘滚动、意图缩放及视野投影逻辑。 * - * 该组件集成了平滑缩放、边缘滚动、目标跟随等功能,并能实时计算相机在地面的视野框点。 + * 组件遵循“战略由人,战术由AI”的设计原则,旨在平衡操作的顺滑感与 UI 的即时反馈。 **/ UCLASS(Blueprintable, ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) class OPENRTSCAMERA_API URTSCamera : public UActorComponent @@ -36,207 +44,340 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent GENERATED_BODY() public: + /** + * @brief 初始化相机组件的默认属性与子对象引用 + **/ URTSCamera(); + /** + * @brief 主逻辑更新函数。驱动物理插值、地形校正及边界约束。 + * + * @param 参数名称: deltaTime 数据类型: float + * @param 参数名称: tickType 数据类型: ELevelTick + * @param 参数名称: thisTickFunction 数据类型: FActorComponentTickFunction* + **/ + /** + * @brief 每帧更新相机状态。驱动物理插值、地形校正及边界约束。 + * + * @param 参数名称: DeltaTime 数据类型: float + * @param 参数名称: TickType 数据类型: ELevelTick + * @param 参数名称: ThisTickFunction 数据类型: FActorComponentTickFunction* + **/ virtual void TickComponent( float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction ) override; + /** + * @brief 使相机视野物理锁定并跟随指定的目标 Actor + * + * @param 参数名称: target 数据类型: AActor* + **/ UFUNCTION(BlueprintCallable, Category = "RTSCamera") - void FollowTarget(AActor* Target); + void followTarget(AActor* target); + /** + * @brief 取消当前的相机目标跟随状态 + **/ UFUNCTION(BlueprintCallable, Category = "RTSCamera") - void UnFollowTarget(); + void unFollowTarget(); + /** + * @brief 将本地控制器的相机观察点设置为此组件的所有者 + **/ UFUNCTION(BlueprintCallable, Category = "RTSCamera") - void SetActiveCamera(); + void setActiveCamera(); /** - * @brief 将相机瞬间移动到指定位置 + * @brief 将相机组件瞬間移动至指定的 X/Y 地面坐标 * - * @param 参数名称: Position 数据类型: FVector + * @param 参数名称: position 数据类型: FVector **/ UFUNCTION(BlueprintCallable, Category = "RTSCamera") - void JumpTo(FVector Position); + void jumpTo(FVector position); + /** + * @brief 获取当前用于相机移动约束的边界体积引用 + * + * @return 返回值类型: AActor* + **/ UFUNCTION(BlueprintPure, Category = "RTSCamera") - AActor* GetBoundaryVolume() const { return BoundaryVolume; } + AActor* getMovementBoundaryVolume() const { return movementBoundaryVolume; } + /// 相机缩放的最小目标距离(最接近地面) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") - float MinimumZoomLength; + float minimumZoomLength; + + /// 相机缩放的最大目标距离(最高视野) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") - float MaximumZoomLength; + float maximumZoomLength; + + /// 缩放插值的补全速率(值越大,物理位置追赶意图的速度越快) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") - float ZoomCatchupSpeed; + float zoomCatchupSpeed; + + /// 单次滚轮操作触发的缩放距离步长 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") - float ZoomSpeed; + float zoomSpeed; + /// 初始化时的相机俯仰角 (Pitch) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float StartingYAngle; + float startingPitchAngle; + + /// 初始化时的相机偏航角 (Yaw) UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float StartingZAngle; + float startingYawAngle; + /// 相机在最大缩放高度时的移动速度上限 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float MaxMoveSpeed; + float maxMovementSpeed; + + /// 相机在最小缩放高度时的基础移动速度 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float MinMoveSpeed; + float minMovementSpeed; + + /// 输入控制下的水平旋转感官速度 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float RotateSpeed; + float rotationSpeed; - /** - * Controls how fast the drag will move the camera. - * Higher values will make the camera move more slowly. - * The drag speed is calculated as follows: - * DragSpeed = MousePositionDelta / (ViewportExtents * DragExtent) - * If the drag extent is small, the drag speed will hit the "max speed" of `this->MoveSpeed` more quickly. - */ + /// 相机拖拽操作在视口中的拉伸增量比例 UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (ClampMin = "0.0", ClampMax = "1.0") ) - float DragExtent; + float dragExtent; + /// 启用相机位置移动的插值延迟 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - bool EnableCameraLag; + bool enableCameraLag; + + /// 启用相机视野旋转的插值延迟 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - bool EnableCameraRotationLag; + bool enableCameraRotationLag; + /// 启用基于地形起伏动态修正相机根高度的功能 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Dynamic Camera Height Settings") - bool EnableDynamicCameraHeight; + bool enableDynamicCameraHeight; + + /// 用于地形高度探测的碰撞通道类型 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Dynamic Camera Height Settings") - TEnumAsByte CollisionChannel; + TEnumAsByte collisionChannel; + + /// 地形校正射线在向上/向下扫描时的最大探测距离 UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Dynamic Camera Height Settings", - meta=(EditCondition="EnableDynamicCameraHeight") + meta=(EditCondition="enableDynamicCameraHeight") ) - float FindGroundTraceLength; + float findGroundTraceLength; + /// 启用鼠标触发视口边缘后的相机滚动 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Edge Scroll Settings") - bool EnableEdgeScrolling; - + bool enableEdgeScrolling; + /// 判定为边缘的屏幕占比阈值(百分比) UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Edge Scroll Settings", - meta=(EditCondition="EnableEdgeScrolling") + meta=(EditCondition="enableEdgeScrolling") ) - float DistanceFromEdgeThreshold; + float distanceFromEdgeThreshold; + /// 相机对应的增强输入上下文 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Inputs") - UInputMappingContext* InputMappingContext; + UInputMappingContext* inputMappingContext; + + /// 旋转控制输入动作映射 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Inputs") - UInputAction* RotateCameraAxis; + UInputAction* rotateCameraAxisAction; + + /// 向左步进旋转输入动作映射 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Inputs") - UInputAction* TurnCameraLeft; + UInputAction* turnCameraLeftAction; + + /// 向右步进旋转输入动作映射 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Inputs") - UInputAction* TurnCameraRight; + UInputAction* turnCameraRightAction; + + /// 前后移动输入动作映射 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Inputs") - UInputAction* MoveCameraYAxis; + UInputAction* moveCameraYAxisAction; + + /// 左右移动输入动作映射 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Inputs") - UInputAction* MoveCameraXAxis; + UInputAction* moveCameraXAxisAction; + + /// 鼠标拖拽输入动作映射 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Inputs") - UInputAction* DragCamera; + UInputAction* dragCameraAction; + + /// 缩放输入动作映射 UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Inputs") - UInputAction* ZoomCamera; + UInputAction* zoomCameraAction; + + /// 委托实例:当视野投影点数组被成功更新后触发 + FOnMinimapFrustumUpdated onMinimapFrustumUpdated; protected: + /** + * @brief 生命周期起始点:建立组件依赖与输入绑定 + **/ virtual void BeginPlay() override; - void OnZoomCamera(const FInputActionValue& Value); - void OnRotateCamera(const FInputActionValue& Value); - void OnTurnCameraLeft(const FInputActionValue& Value); - void OnTurnCameraRight(const FInputActionValue& Value); - void OnMoveCameraYAxis(const FInputActionValue& Value); - void OnMoveCameraXAxis(const FInputActionValue& Value); - void OnDragCamera(const FInputActionValue& Value); + /** + * @brief 响应增强输入事件。执行缩放并标记視野更新。 + * + * @param 参数名称: value 数据类型: const FInputActionValue& + **/ + void onZoomCameraActionTriggered(const FInputActionValue& value); + + /** + * @brief 响应增强输入事件。执行相机旋转并标记視野更新。 + * + * @param 参数名称: value 数据类型: const FInputActionValue& + **/ + void onRotateCameraActionTriggered(const FInputActionValue& value); - void RequestMoveCamera(float X, float Y, float Scale); - void ApplyMoveCameraCommands(); + /** + * @brief 响应增强输入事件。执行向左转向操作。 + * + * @param 参数名称: value 数据类型: const FInputActionValue& + **/ + void onTurnCameraLeftActionTriggered(const FInputActionValue& value); - /** @brief 相机所属的 Actor 引用 */ + /** + * @brief 响应增强输入事件。执行向右转向操作。 + * + * @param 参数名称: value 数据类型: const FInputActionValue& + **/ + void onTurnCameraRightActionTriggered(const FInputActionValue& value); + + /** + * @brief 响应增强输入事件。请求纵向平移指令。 + * + * @param 参数名称: value 数据类型: const FInputActionValue& + **/ + void onMoveCameraYAxisActionTriggered(const FInputActionValue& value); + + /** + * @brief 响应增强输入事件。请求横向平移指令。 + * + * @param 参数名称: value 数据类型: const FInputActionValue& + **/ + void onMoveCameraXAxisActionTriggered(const FInputActionValue& value); + + /** + * @brief 响应增强输入事件。控制鼠标拖拽状态下的相机平移逻辑。 + * + * @param 参数名称: value 数据类型: const FInputActionValue& + **/ + void onDragCameraActionTriggered(const FInputActionValue& value); + + /** + * @brief 将坐标移动意图转化为战术指令并加入执行队列 + * + * @param 参数名称: x 数据类型: float + * @param 参数名称: y 数据类型: float + * @param 参数名称: scale 数据类型: float + **/ + void requestCameraMovement(float x, float y, float scale); + + /** + * @brief 在一个逻辑帧内,分步执行指令队列中积压的所有平移指令 + **/ + void applyAccumulatedMovementCommands(); + + /// 组件所属的 Actor 引用,定义了相机的生命周期主体 UPROPERTY() - AActor* CameraOwner; + AActor* cameraOwner; - /** @brief 根组件场景引用,用于控制相机在世界中的平移和旋转 */ + /// 根场景组件,控制整个相机组在该坐标下的水平面移动 UPROPERTY() - USceneComponent* RootComponent; + USceneComponent* rootComponent; - /** @brief 相机组件引用,提供 FOV 等光学参数 */ + /// 相机组件,定义视野视野参数与最终渲染输出 UPROPERTY() - UCameraComponent* CameraComponent; + UCameraComponent* cameraComponent; - /** @brief 弹簧臂组件引用,用于控制相机的距离(臂长)和俯角 */ + /// 弹簧臂组件,控制相机与中心点之间的意图长度与俯仰关系 UPROPERTY() - USpringArmComponent* SpringArmComponent; + USpringArmComponent* springArmComponent; - /** @brief 玩家控制器引用,用于处理输入和视图设置 */ + /// 实时战略专用的玩家控制器,分发输入事件 UPROPERTY() - APlayerController* RTSPlayerController; + APlayerController* realTimeStrategyPlayerController; - /** @brief 相机移动边界体积,用于限制相机活动范围 */ + /// 用于限制相机在地图中活动范围的体积 UPROPERTY() - AActor* BoundaryVolume; + AActor* movementBoundaryVolume; + + /// 玩家输入的理想缩放目标高度,视野计算将优先同步此意图而非物理插值过程 UPROPERTY() - float DesiredZoomLength; + float desiredZoomLength; private: - void CollectComponentDependencyReferences(); - void ConfigureSpringArm(); - - void TryToFindBoundaryVolumeReference(); - void ConditionallyEnableEdgeScrolling(); - void CheckForEnhancedInputComponent(); - void BindInputMappingContext(); - void BindInputActions(); - - void ConditionallyPerformEdgeScrolling(); - void EdgeScrollLeft(); - void EdgeScrollRight(); - void EdgeScrollUp(); - void EdgeScrollDown(); - - void FollowTargetIfSet(); - void SmoothTargetArmLengthToDesiredZoom(); - void ConditionallyKeepCameraAtDesiredZoomAboveGround(); - void ConditionallyApplyCameraBounds(); - + void resolveComponentDependencyPointers(); + void setupInitialSpringArmState(); + + void locateMapBoundaryVolumeByTag(); + void configureInputModeForEdgeScrolling(); + void validateEnhancedInputAvailability(); + void registerInputMappingContext(); + void bindActionCallbacks(); + + void executeEdgeScrollingEvaluation(); + void performEdgeScrollLeft(); + void performEdgeScrollRight(); + void performEdgeScrollUp(); + void performEdgeScrollDown(); + + void updateFollowPositionIfTargetActive(); + void handleTargetArmLengthInterpolation(); + void rectifyRootHeightFromTerrain(); + void enforceCameraMovementBounds(); + + /// 检索边界体积时匹配的静态场景标签 UPROPERTY() - FName CameraBlockingVolumeTag; - UPROPERTY() - AActor* CameraFollowTarget; + FName cameraBlockingVolumeTag; + + /// 相机当前正在锁定跟随的 Actor 实测对象 UPROPERTY() - float DeltaSeconds; + AActor* activeCameraFollowTarget; + + /// 自上一帧以来的时间增量(秒) UPROPERTY() - bool IsCameraOutOfBoundsErrorAlreadyDisplayed; + float deltaSeconds; + + /// 状态位:指示是否正在进行鼠标拖拽操作 UPROPERTY() - bool IsDragging; + bool isDragging; + + /// 拖拽操作开始时的视口坐标缓存 UPROPERTY() - FVector2D DragStartLocation; - /** @brief 相机移动指令队列 */ + FVector2D dragInteractionInitialLocation; + + /// 移动指令队列,用于适配变动帧率下的平滑渲染 UPROPERTY() - TArray MoveCameraCommands; + TArray pendingMovementCommands; - /** @brief 当前移动速度 */ + /// 当前瞬时计算的相机移动速度值 UPROPERTY() - float CurrentMovementSpeed; + float currentMovementSpeed; public: - /** - * @brief 计算并存储相机在地平面的四个视野投影点 - **/ - UPROPERTY(BlueprintReadOnly, Category = "RTSCamera|Minimap") - TArray MinimapFrustumPoints; + /// 静态数组,存储由视野投影计算出的地平面四个接地区顶点。 + /// 顺序遵循:[0]左上, [1]右上, [2]右下, [3]左下。 + FVector minimapFrustumPoints[4]; /** - * @brief 强制更新视野框坐标。内部计算优先使用 DesiredZoomLength 以实现意图同步。 + * @brief 强制触发視野投影计算,基于当前位置及缩放意图刷新 minimapFrustumPoint 数组数据。 + * 注意:仅在相机产生明确的战略意图变更(移动、缩放、跳转)时产生计算开销。 **/ UFUNCTION(BlueprintCallable, Category = "RTSCamera|Minimap") - void UpdateMinimapFrustum(); + void updateMinimapFrustum(); }; diff --git a/Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h b/Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h index 0e66c47..002b161 100644 --- a/Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h +++ b/Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h @@ -30,8 +30,9 @@ class OPENRTSCAMERA_API URTSCameraMinimapWidget : public UUserWidget void InitializeController(); protected: + /// 小地图视野框的线条绘制宽度 UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Minimap") - float LineWidth = 2.0f; + float lineWidth = 2.0f; virtual void NativeConstruct() override; virtual int32 NativePaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override; @@ -44,7 +45,15 @@ class OPENRTSCAMERA_API URTSCameraMinimapWidget : public UUserWidget virtual void NativeOnMouseLeave(const FPointerEvent& InMouseEvent) override; private: - void FindRTSCamera(); + /** + * @brief 根据相机组件的引用定位并缓存相关依赖资产 + **/ + void findRTSCamera(); + + /** + * @brief 响应相机视野更新委托的回调函数,执行 UI 定向失效重绘 + **/ + void handleMinimapFrustumUpdated(); /** Convert World Location (XY) to Widget Local Coordinates (UV * Size) */ FVector2D ConvertWorldToWidgetLocal(const FVector2D& WorldPos, const FVector2D& WidgetSize) const; @@ -53,23 +62,27 @@ class OPENRTSCAMERA_API URTSCameraMinimapWidget : public UUserWidget FVector2D ConvertWidgetLocalToWorld(const FVector2D& LocalPos, const FVector2D& WidgetSize) const; protected: - /** Cached reference to the RTSCamera */ + /** @brief 缓存的 RTS 相机组件引用 */ UPROPERTY(Transient, BlueprintReadOnly, Category = "RTSCamera|Cache") - TObjectPtr CachedRTSCamera; + TObjectPtr cachedRTSCamera; - /** Cached reference to the actual Camera Component (Source of Truth for FOV) */ + /** @brief 缓存的相机渲染组件,作为视场角(FOV)的原始依据 */ UPROPERTY(Transient, BlueprintReadOnly, Category = "RTSCamera|Cache") - TObjectPtr CachedCameraComponent; + TObjectPtr cachedCameraComponent; - /** Cached reference to the Spring Arm (Source of Truth for Zoom/Rotation) */ + /** @brief 缓存的弹簧臂组件,作为相机缩放与倾斜的物理依据 */ UPROPERTY(Transient, BlueprintReadOnly, Category = "RTSCamera|Cache") - TObjectPtr CachedSpringArm; + TObjectPtr cachedSpringArm; - /** Derived from BoundaryVolume */ - FVector CachedBoundsOrigin = FVector::ZeroVector; - FVector CachedBoundsExtent = FVector(100.f, 100.f, 100.f); + /// 地图边界的中心点缓存 + FVector cachedBoundsOrigin = FVector::ZeroVector; + + /// 地图边界的延伸范围缓存 + FVector cachedBoundsExtent = FVector(100.f, 100.f, 100.f); + + /// 标识当前缓存的地图边界数据是否有效 bool bHasValidBounds = false; - /** Input State */ + /// 状态位:标识玩家当前是否正在通过鼠标在控件上执行位置拖拽 bool bIsDragging = false; }; From 1573c6c6af064d14348f83e1a0b9548f96b0201d Mon Sep 17 00:00:00 2001 From: winyunq Date: Sat, 14 Feb 2026 17:59:46 +0800 Subject: [PATCH 06/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Private/UI/RTSCameraMinimapWidget.cpp | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp index 6259166..3ea88c1 100644 --- a/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp +++ b/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp @@ -163,26 +163,30 @@ int32 URTSCameraMinimapWidget::NativePaint( return maxLayerId; } - /// 从相机缓存中检索投影点并建立线条路径。此处的开销仅存在于重绘帧。 - TArray viewportDrawPoints; + /// 战术兼容性调整:虽然静态数组更直接,但由于 Slate 的 MakeLines API 严格要求 TArray 类型容器, + /// 此处采用预分配空间的 TArray 以平衡性能与接口规范。 + TArray drawPoints; + drawPoints.Reserve(5); + for (int32 i = 0; i < 4; ++i) { const FVector& worldPt = this->cachedRTSCamera->minimapFrustumPoints[i]; - FVector2D widgetPt = this->ConvertWorldToWidgetLocal(FVector2D(worldPt.X, worldPt.Y), geometrySize); - viewportDrawPoints.Add(widgetPt); + drawPoints.Add(this->ConvertWorldToWidgetLocal(FVector2D(worldPt.X, worldPt.Y), geometrySize)); } - if (viewportDrawPoints.Num() > 0) + if (drawPoints.Num() > 0) { - viewportDrawPoints.Add(viewportDrawPoints[0]); + /// 彻底消除断言崩溃:显式拷贝首个元素至栈变量。 + /// UE 5.6 严禁直接 Add 容器内部的元素地址,以防扩容时发生非法访问。 + const FVector2D closedPoint = drawPoints[0]; + drawPoints.Add(closedPoint); } - /// 在指定图层上生成绘制指令 FSlateDrawElement::MakeLines( OutDrawElements, LayerId + 1, AllottedGeometry.ToPaintGeometry(), - viewportDrawPoints, + drawPoints, ESlateDrawEffect::None, FLinearColor::White, true, From cfd7ba95f6a02bd222eca95410d6f209a77c5267 Mon Sep 17 00:00:00 2001 From: winyunq Date: Sun, 15 Feb 2026 09:02:30 +0800 Subject: [PATCH 07/12] =?UTF-8?q?=E7=9B=B8=E6=9C=BA=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BA=86=E8=BE=B9=E7=95=8C=E6=98=BE=E7=A4=BA=EF=BC=8C=E4=BD=BF?= =?UTF-8?q?=E5=BE=97=E4=B8=8D=E4=BC=9A=E9=9C=B2=E9=A6=85=E3=80=82=E7=9B=B8?= =?UTF-8?q?=E6=9C=BA=E6=9B=B4=E6=96=B0=E4=BA=86=E4=B8=80=E4=B8=8B=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=8D=95=E4=BD=8D=EF=BC=8C=E7=90=86=E8=AE=BA=E4=B8=8A?= =?UTF-8?q?=E5=AF=B9Mass=20battle=E5=90=8C=E6=A0=B7=E6=9C=89=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Content/MiniMapRegion.uasset | 3 + OpenRTSCamera.uplugin | 4 + Source/OpenRTSCamera/OpenRTSCamera.Build.cs | 6 +- Source/OpenRTSCamera/Private/RTSCamera.cpp | 429 ++++++++++++++------ Source/OpenRTSCamera/Private/RTSHUD.cpp | 90 +++- Source/OpenRTSCamera/Public/RTSCamera.h | 78 ++-- Source/OpenRTSCamera/Public/RTSHUD.h | 2 + 7 files changed, 466 insertions(+), 146 deletions(-) create mode 100644 Content/MiniMapRegion.uasset diff --git a/Content/MiniMapRegion.uasset b/Content/MiniMapRegion.uasset new file mode 100644 index 0000000..683a3b2 --- /dev/null +++ b/Content/MiniMapRegion.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:21ee8deea34f5b69206ce08413985c0250d304c22c612691daf4ce4465a2e4e5 +size 22409 diff --git a/OpenRTSCamera.uplugin b/OpenRTSCamera.uplugin index 4793bc1..16a996a 100644 --- a/OpenRTSCamera.uplugin +++ b/OpenRTSCamera.uplugin @@ -26,6 +26,10 @@ { "Name": "EnhancedInput", "Enabled": true + }, + { + "Name": "MassBattleMinimap", + "Enabled": true } ], "SupportURL": "https://github.com/HeyZoos/OpenRTSCamera/issues", diff --git a/Source/OpenRTSCamera/OpenRTSCamera.Build.cs b/Source/OpenRTSCamera/OpenRTSCamera.Build.cs index c60f199..6b14cad 100644 --- a/Source/OpenRTSCamera/OpenRTSCamera.Build.cs +++ b/Source/OpenRTSCamera/OpenRTSCamera.Build.cs @@ -36,7 +36,11 @@ public OpenRTSCamera(ReadOnlyTargetRules Target) : base(Target) "Slate", "SlateCore", "UMG", - "InputCore" + "InputCore", + "MassBattleMinimap", + "MassBattle", + "MassAPI", + "MassEntity" } ); diff --git a/Source/OpenRTSCamera/Private/RTSCamera.cpp b/Source/OpenRTSCamera/Private/RTSCamera.cpp index 7c2979b..2fa3c4b 100644 --- a/Source/OpenRTSCamera/Private/RTSCamera.cpp +++ b/Source/OpenRTSCamera/Private/RTSCamera.cpp @@ -1,6 +1,11 @@ // Copyright 2024 Jesus Bracho All Rights Reserved. +#define RTS_CAMERA_CPP #include "RTSCamera.h" +#include "MassBattleMinimapRegion.h" + +// 定义 RTSCamera 专用日志分类 +DEFINE_LOG_CATEGORY_STATIC(LogRTSCamera, Log, All); #include "Blueprint/WidgetLayoutLibrary.h" #include "Engine/LocalPlayer.h" @@ -13,9 +18,9 @@ URTSCamera::URTSCamera() { +// ... (previous lines remain unchanged) /// 设置组件基本生存期属性 PrimaryComponentTick.bCanEverTick = true; - this->cameraBlockingVolumeTag = FName("OpenRTSCamera#CameraBounds"); this->collisionChannel = ECC_WorldStatic; this->dragExtent = 0.6f; this->distanceFromEdgeThreshold = 0.1f; @@ -34,6 +39,12 @@ URTSCamera::URTSCamera() this->startingYawAngle = 0; this->zoomCatchupSpeed = 4; this->zoomSpeed = -200; + this->minimumZoomBoundaryConstraint = 0.5f; + this->boundaryTransitionZoneRatio = 0.15f; + this->bEnableXBoundaryConstraint = true; + this->bEnableYBoundaryConstraint = true; + this->currentLateralSocketOffset = 0.0f; + this->currentVerticalSocketOffset = 0.0f; /// 载入并关联输入资产 static ConstructorHelpers::FObjectFinder xMoveActionFinder(TEXT("/OpenRTSCamera/Inputs/MoveCameraXAxis")); @@ -88,10 +99,9 @@ void URTSCamera::TickComponent( this->deltaSeconds = DeltaTime; this->applyAccumulatedMovementCommands(); this->executeEdgeScrollingEvaluation(); - this->rectifyRootHeightFromTerrain(); this->handleTargetArmLengthInterpolation(); this->updateFollowPositionIfTargetActive(); - this->enforceCameraMovementBounds(); + this->applyBoundaryConstraints(); } } @@ -142,13 +152,13 @@ void URTSCamera::onRotateCameraActionTriggered(const FInputActionValue& value) void URTSCamera::onTurnCameraLeftActionTriggered(const FInputActionValue&) { /// 向左执行定量的步进式偏转 - const auto relativeRot = this->rootComponent->GetRelativeRotation(); + const FRotator relativeRotation = this->rootComponent->GetRelativeRotation(); this->rootComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( - relativeRot.Euler().X, - relativeRot.Euler().Y, - relativeRot.Euler().Z - this->rotationSpeed + relativeRotation.Euler().X, + relativeRotation.Euler().Y, + relativeRotation.Euler().Z - this->rotationSpeed ) ) ); @@ -158,13 +168,13 @@ void URTSCamera::onTurnCameraLeftActionTriggered(const FInputActionValue&) void URTSCamera::onTurnCameraRightActionTriggered(const FInputActionValue&) { /// 向右执行定量的步进式偏转 - const auto relativeRot = this->rootComponent->GetRelativeRotation(); + const FRotator relativeRotation = this->rootComponent->GetRelativeRotation(); this->rootComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( - relativeRot.Euler().X, - relativeRot.Euler().Y, - relativeRot.Euler().Z + this->rotationSpeed + relativeRotation.Euler().X, + relativeRotation.Euler().Y, + relativeRotation.Euler().Z + this->rotationSpeed ) ) ); @@ -175,8 +185,8 @@ void URTSCamera::onMoveCameraYAxisActionTriggered(const FInputActionValue& value { /// 处理纵向平移请求 this->requestCameraMovement( - this->springArmComponent->GetForwardVector().X, - this->springArmComponent->GetForwardVector().Y, + this->rootComponent->GetForwardVector().X, + this->rootComponent->GetForwardVector().Y, value.Get() ); } @@ -185,8 +195,8 @@ void URTSCamera::onMoveCameraXAxisActionTriggered(const FInputActionValue& value { /// 处理横向平移请求 this->requestCameraMovement( - this->springArmComponent->GetRightVector().X, - this->springArmComponent->GetRightVector().Y, + this->rootComponent->GetRightVector().X, + this->rootComponent->GetRightVector().Y, value.Get() ); } @@ -212,14 +222,14 @@ void URTSCamera::onDragCameraActionTriggered(const FInputActionValue& value) dragDelta.Y = FMath::Clamp(dragDelta.Y, -viewportSizeExtent.Y, viewportSizeExtent.Y) / viewportSizeExtent.Y; this->requestCameraMovement( - this->springArmComponent->GetRightVector().X, - this->springArmComponent->GetRightVector().Y, + this->rootComponent->GetRightVector().X, + this->rootComponent->GetRightVector().Y, dragDelta.X ); this->requestCameraMovement( - this->springArmComponent->GetForwardVector().X, - this->springArmComponent->GetForwardVector().Y, + this->rootComponent->GetForwardVector().X, + this->rootComponent->GetForwardVector().Y, dragDelta.Y * -1 ); } @@ -231,24 +241,24 @@ void URTSCamera::onDragCameraActionTriggered(const FInputActionValue& value) } } -void URTSCamera::requestCameraMovement(const float x, const float y, const float scale) +void URTSCamera::requestCameraMovement(const float xAxisValue, const float yAxisValue, const float movementScale) { /// 将运动请求压入队列,供 Tick 阶段统一消化 - FMoveCameraCommand movementCmd; - movementCmd.x = x; - movementCmd.y = y; - movementCmd.scale = scale; - this->pendingMovementCommands.Push(movementCmd); + FMoveCameraCommand movementCommand; + movementCommand.xAxisValue = xAxisValue; + movementCommand.yAxisValue = yAxisValue; + movementCommand.movementScale = movementScale; + this->pendingMovementCommands.Push(movementCommand); } void URTSCamera::applyAccumulatedMovementCommands() { /// 执行帧内所有挂起的平移指令并清空 - for (const auto& [x, y, scale] : this->pendingMovementCommands) + for (const auto& [xAxisValue, yAxisValue, movementScale] : this->pendingMovementCommands) { - auto directionVector = FVector2D(x, y); + FVector2D directionVector(xAxisValue, yAxisValue); directionVector.Normalize(); - directionVector *= this->currentMovementSpeed * scale * this->deltaSeconds; + directionVector *= this->currentMovementSpeed * movementScale * this->deltaSeconds; this->jumpTo( this->rootComponent->GetComponentLocation() + FVector(directionVector.X, directionVector.Y, 0.0f) @@ -285,23 +295,96 @@ void URTSCamera::setupInitialSpringArmState() ) ) ); + + if (this->cameraComponent != nullptr) + { + // 关键:回归绑定模式以消除缩放抖动,且将相对旋转清零以解决 -90 度视角叠加 Bug。 + this->cameraComponent->bUsePawnControlRotation = false; + this->cameraComponent->SetUsingAbsoluteRotation(false); + + // 视角初始化诊断:验证当前俯仰角设定,此时相机将纯粹继承弹簧臂的俯仰。 + UE_LOG(LogRTSCamera, Warning, TEXT("视角初始化诊断 (V9 零叠加版): 起始俯仰角=%.2f, 起始偏航角=%.2f"), this->startingPitchAngle, this->startingYawAngle); + + // 修复 -90 度叠加:相机作为子组件不需要再次设置 -45 度相对俯角。 + this->cameraComponent->SetRelativeRotation(FRotator::ZeroRotator); + this->cameraComponent->FieldOfView = 45.0f; + } } void URTSCamera::locateMapBoundaryVolumeByTag() { - /// 在世界中通过静态标签检索用于运动边界约束的 Actor - TArray results; - UGameplayStatics::GetAllActorsOfClassWithTag( + /// 直接在世界中通过类类型检索 AMinimapRegion + TArray foundActors; + UGameplayStatics::GetAllActorsOfClass( this->GetWorld(), - AActor::StaticClass(), - this->cameraBlockingVolumeTag, - results + AMinimapRegion::StaticClass(), + foundActors ); - if (results.Num() > 0) + if (foundActors.Num() > 0) { - this->movementBoundaryVolume = results[0]; - /// 初始化視野投影静态缓存点并执行初次同步 + this->movementBoundaryVolume = foundActors[0]; + if (AMinimapRegion* minimapRegion = Cast(this->movementBoundaryVolume)) + { + const FVector logicalExtent = minimapRegion->BoundsComponent->GetScaledBoxExtent(); + const float mapOverflowDistance = minimapRegion->MapOverflowUU; + + // --- 視野溢出定量诊断 (V14 - 全程线性强稳版) --- + // 我们精确量化高空与低空视角的物理物性,以验证线性斜率 k。 + if (this->cameraComponent != nullptr) + { + const float pitchAngleInRadians = FMath::DegreesToRadians(FMath::Abs(this->startingPitchAngle)); + const float horizontalFieldOfViewHalf = FMath::DegreesToRadians(this->cameraComponent->FieldOfView) / 2.0f; + const FVector2D viewportSize = UWidgetLayoutLibrary::GetViewportSize(this->GetWorld()); + const float viewportAspectRatioValue = (viewportSize.Y > 0.0f) ? (viewportSize.X / viewportSize.Y) : this->cameraComponent->AspectRatio; + const float verticalFieldOfViewHalf = FMath::Atan(FMath::Tan(horizontalFieldOfViewHalf) / viewportAspectRatioValue); + + // 辅助计算闭包函数 (用于获取任意高度下的视野属性) + auto calculateReachForLength = [&](float length) { + const float z = length * FMath::Sin(pitchAngleInRadians); + const float slant = z / FMath::Sin(pitchAngleInRadians + verticalFieldOfViewHalf); + const float lateralReach = slant * FMath::Tan(horizontalFieldOfViewHalf); + const float forwardReach = slant * FMath::Cos(pitchAngleInRadians + verticalFieldOfViewHalf); + return FVector(z, lateralReach, forwardReach); + }; + + const FVector maxPhysics = calculateReachForLength(this->maximumZoomLength); + const FVector minPhysics = calculateReachForLength(this->minimumZoomLength); + + const float maxPhysicsX = maxPhysics.Z; // 南北延伸 (Forward Reach) + const float maxPhysicsY = maxPhysics.Y; // 东西跨度 (Lateral Reach) + + // 计算后向延伸 (Backward Reach) + const float slantBottom = maxPhysics.X / FMath::Sin(pitchAngleInRadians - verticalFieldOfViewHalf); + const float backwardReach = slantBottom * FMath::Cos(pitchAngleInRadians - verticalFieldOfViewHalf); + + this->lateralReachFactor = maxPhysicsY / this->maximumZoomLength; + this->forwardReachFactor = maxPhysicsX / this->maximumZoomLength; + this->backwardReachFactor = backwardReach / this->maximumZoomLength; + + const float lateralAngleDeg = FMath::RadiansToDegrees(FMath::Atan(this->lateralReachFactor)); + const float forwardAngleDeg = FMath::RadiansToDegrees(FMath::Atan(this->forwardReachFactor)); + const float backwardAngleDeg = FMath::RadiansToDegrees(FMath::Atan(this->backwardReachFactor)); + + UE_LOG(LogRTSCamera, Warning, TEXT("=== 边界溢出诊断报告 (V14 - 全程线性版) ===")); + UE_LOG(LogRTSCamera, Warning, TEXT("俯仰基准: %.1f | 溢出阈值 (MapOverflowUU): %.1f"), this->startingPitchAngle, mapOverflowDistance); + UE_LOG(LogRTSCamera, Warning, TEXT("[预计算系数] Lateral: %.4f (%.2f°) | Forward: %.4f (%.2f°) | Backward: %.4f (%.2f°)"), + this->lateralReachFactor, lateralAngleDeg, this->forwardReachFactor, forwardAngleDeg, this->backwardReachFactor, backwardAngleDeg); + const float minPhysicsX = minPhysics.Z; + const float minPhysicsY = minPhysics.Y; + + UE_LOG(LogRTSCamera, Warning, TEXT("[高空 Zmax=%.1f] 东西补偿 (Reach-O): %.1f | 南北补偿 (Reach-O): %.1f"), maxPhysics.X, maxPhysicsY - mapOverflowDistance, maxPhysicsX - mapOverflowDistance); + UE_LOG(LogRTSCamera, Warning, TEXT("[低空 Zmin=%.1f] 东西补偿 (Reach-O): %.1f | 南北补偿 (Reach-O): %.1f"), minPhysics.X, minPhysicsY - mapOverflowDistance, minPhysicsX - mapOverflowDistance); + UE_LOG(LogRTSCamera, Warning, TEXT("--- 线性偏置计算 (y = kz + b) ---")); + UE_LOG(LogRTSCamera, Warning, TEXT("东西向 (Y) 补偿范围: %.1f -> %.1f"), minPhysics.Y - mapOverflowDistance, maxPhysics.Y - mapOverflowDistance); + UE_LOG(LogRTSCamera, Warning, TEXT("南北向 (X) 补偿范围: %.1f -> %.1f"), minPhysics.Z - mapOverflowDistance, maxPhysics.Z - mapOverflowDistance); + UE_LOG(LogRTSCamera, Warning, TEXT("=========================================")); + } + + // 还原基础初始化信息日志 + UE_LOG(LogRTSCamera, Log, TEXT("RTSCamera 初始化: 挂载 [%s], 逻辑边界: %.1f x %.1f, 溢出保护: %.1f"), + *minimapRegion->GetName(), logicalExtent.X * 2.0f, logicalExtent.Y * 2.0f, minimapRegion->MapOverflowUU); + } this->updateMinimapFrustum(); } } @@ -352,15 +435,15 @@ void URTSCamera::registerInputMappingContext() void URTSCamera::bindActionCallbacks() { /// 执行运动指令与 C++ 响应函数的逻辑挂挂接 - if (const auto eic = Cast(this->realTimeStrategyPlayerController->InputComponent)) + if (const auto enhancedInputComponent = Cast(this->realTimeStrategyPlayerController->InputComponent)) { - eic->BindAction(this->zoomCameraAction, ETriggerEvent::Triggered, this, &URTSCamera::onZoomCameraActionTriggered); - eic->BindAction(this->rotateCameraAxisAction, ETriggerEvent::Triggered, this, &URTSCamera::onRotateCameraActionTriggered); - eic->BindAction(this->turnCameraLeftAction, ETriggerEvent::Triggered, this, &URTSCamera::onTurnCameraLeftActionTriggered); - eic->BindAction(this->turnCameraRightAction, ETriggerEvent::Triggered, this, &URTSCamera::onTurnCameraRightActionTriggered); - eic->BindAction(this->moveCameraXAxisAction, ETriggerEvent::Triggered, this, &URTSCamera::onMoveCameraXAxisActionTriggered); - eic->BindAction(this->moveCameraYAxisAction, ETriggerEvent::Triggered, this, &URTSCamera::onMoveCameraYAxisActionTriggered); - eic->BindAction(this->dragCameraAction, ETriggerEvent::Triggered, this, &URTSCamera::onDragCameraActionTriggered); + enhancedInputComponent->BindAction(this->zoomCameraAction, ETriggerEvent::Triggered, this, &URTSCamera::onZoomCameraActionTriggered); + enhancedInputComponent->BindAction(this->rotateCameraAxisAction, ETriggerEvent::Triggered, this, &URTSCamera::onRotateCameraActionTriggered); + enhancedInputComponent->BindAction(this->turnCameraLeftAction, ETriggerEvent::Triggered, this, &URTSCamera::onTurnCameraLeftActionTriggered); + enhancedInputComponent->BindAction(this->turnCameraRightAction, ETriggerEvent::Triggered, this, &URTSCamera::onTurnCameraRightActionTriggered); + enhancedInputComponent->BindAction(this->moveCameraXAxisAction, ETriggerEvent::Triggered, this, &URTSCamera::onMoveCameraXAxisActionTriggered); + enhancedInputComponent->BindAction(this->moveCameraYAxisAction, ETriggerEvent::Triggered, this, &URTSCamera::onMoveCameraYAxisActionTriggered); + enhancedInputComponent->BindAction(this->dragCameraAction, ETriggerEvent::Triggered, this, &URTSCamera::onDragCameraActionTriggered); } } @@ -375,6 +458,7 @@ void URTSCamera::jumpTo(const FVector position) /// 执行瞬时的视变换同步,并触发视野投影点手动刷新 float cachedZ = this->rootComponent->GetComponentLocation().Z; this->rootComponent->SetWorldLocation(FVector(position.X, position.Y, cachedZ)); + this->applyBoundaryConstraints(); this->updateMinimapFrustum(); } @@ -400,44 +484,44 @@ void URTSCamera::executeEdgeScrollingEvaluation() void URTSCamera::performEdgeScrollLeft() { /// 基于鼠标左偏移计算平移推力 - const auto mp = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto vs = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto normalizedVal = 1 - UKismetMathLibrary::NormalizeToRange(mp.X, 0.0f, vs.X * this->distanceFromEdgeThreshold); + const auto mousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + const auto viewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); + const auto normalizedValue = 1 - UKismetMathLibrary::NormalizeToRange(mousePosition.X, 0.0f, viewportSize.X * this->distanceFromEdgeThreshold); - const float alpha = UKismetMathLibrary::FClamp(normalizedVal, 0.0, 1.0); + const float alpha = UKismetMathLibrary::FClamp(normalizedValue, 0.0, 1.0); this->rootComponent->AddRelativeLocation(-1 * this->rootComponent->GetRightVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } void URTSCamera::performEdgeScrollRight() { /// 基于鼠标右偏移计算平移推力 - const auto mp = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto vs = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto normalizedVal = UKismetMathLibrary::NormalizeToRange(mp.X, vs.X * (1 - this->distanceFromEdgeThreshold), vs.X); + const auto mousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + const auto viewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); + const auto normalizedValue = UKismetMathLibrary::NormalizeToRange(mousePosition.X, viewportSize.X * (1 - this->distanceFromEdgeThreshold), viewportSize.X); - const float alpha = UKismetMathLibrary::FClamp(normalizedVal, 0.0, 1.0); + const float alpha = UKismetMathLibrary::FClamp(normalizedValue, 0.0, 1.0); this->rootComponent->AddRelativeLocation(this->rootComponent->GetRightVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } void URTSCamera::performEdgeScrollUp() { /// 基于鼠标上偏移计算平移推力 - const auto mp = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto vs = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto normalizedVal = UKismetMathLibrary::NormalizeToRange(mp.Y, 0.0f, vs.Y * this->distanceFromEdgeThreshold); + const auto mousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + const auto viewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); + const auto normalizedValue = UKismetMathLibrary::NormalizeToRange(mousePosition.Y, 0.0f, viewportSize.Y * this->distanceFromEdgeThreshold); - const float alpha = 1 - UKismetMathLibrary::FClamp(normalizedVal, 0.0, 1.0); + const float alpha = 1 - UKismetMathLibrary::FClamp(normalizedValue, 0.0, 1.0); this->rootComponent->AddRelativeLocation(this->rootComponent->GetForwardVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } void URTSCamera::performEdgeScrollDown() { /// 基于鼠标下偏移计算平移推力 - const auto mp = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto vs = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto normalizedVal = UKismetMathLibrary::NormalizeToRange(mp.Y, vs.Y * (1 - this->distanceFromEdgeThreshold), vs.Y); + const auto mousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); + const auto viewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); + const auto normalizedValue = UKismetMathLibrary::NormalizeToRange(mousePosition.Y, viewportSize.Y * (1 - this->distanceFromEdgeThreshold), viewportSize.Y); - const float alpha = UKismetMathLibrary::FClamp(normalizedVal, 0.0, 1.0); + const float alpha = UKismetMathLibrary::FClamp(normalizedValue, 0.0, 1.0); this->rootComponent->AddRelativeLocation(-1 * this->rootComponent->GetForwardVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } @@ -467,16 +551,16 @@ void URTSCamera::rectifyRootHeightFromTerrain() if (this->enableDynamicCameraHeight) { const FVector currentRootXYZ = this->rootComponent->GetComponentLocation(); - const TArray excluded; + const TArray excludedActors; FHitResult floorHit; - bool bValidFloor = UKismetSystemLibrary::LineTraceSingle( + const bool bValidFloor = UKismetSystemLibrary::LineTraceSingle( this->GetWorld(), FVector(currentRootXYZ.X, currentRootXYZ.Y, currentRootXYZ.Z + this->findGroundTraceLength), FVector(currentRootXYZ.X, currentRootXYZ.Y, currentRootXYZ.Z - this->findGroundTraceLength), UEngineTypes::ConvertToTraceType(this->collisionChannel), true, - excluded, + excludedActors, EDrawDebugTrace::None, floorHit, true @@ -489,78 +573,181 @@ void URTSCamera::rectifyRootHeightFromTerrain() } } -void URTSCamera::enforceCameraMovementBounds() -{ - /// 强制检查并将相机坐标拉回预定义的体积包围阵列内 - if (this->movementBoundaryVolume != nullptr) - { - const FVector posToClamp = this->rootComponent->GetComponentLocation(); - FVector boxOrigin; - FVector boxExtents; - this->movementBoundaryVolume->GetActorBounds(false, boxOrigin, boxExtents); - - this->rootComponent->SetWorldLocation( - FVector( - UKismetMathLibrary::Clamp(posToClamp.X, boxOrigin.X - boxExtents.X, boxOrigin.X + boxExtents.X), - UKismetMathLibrary::Clamp(posToClamp.Y, boxOrigin.Y - boxExtents.Y, boxOrigin.Y + boxExtents.Y), - posToClamp.Z - ) - ); - } -} void URTSCamera::updateMinimapFrustum() { - /// 战略視野投影核心逻辑:将缩放意图(DesiredZoomLength)直接转化为地平面的投影区域,确保 UI 响应不受物理插值影响 - if (!this->springArmComponent || !this->cameraComponent || !this->rootComponent) return; + /// 战略視野投影核心逻辑:直接基于相机的世界位姿计算四个角点。 + if (this->cameraComponent == nullptr || this->rootComponent == nullptr) + { + return; + } - const FVector rPos = this->rootComponent->GetComponentLocation(); - const FRotator rRot = this->rootComponent->GetComponentRotation(); - const FRotator aRot = this->springArmComponent->GetRelativeRotation(); + // 核心:强制触发 SpringArm 及其子组件的世界变换计算 + // 否则在同一帧内,GetComponentLocation 拿到的可能是修改 SocketOffset 之前的旧位置 + // 但是我不认可这种逻辑,因为对小地图来说这不重要,我觉得是无稽之谈 + // if (this->springArmComponent) + // { + // this->springArmComponent->UpdateComponentToWorld(); + // } + // this->cameraComponent->UpdateComponentToWorld(); + + const FVector cameraLocation = this->cameraComponent->GetComponentLocation(); + const FRotator cameraRotation = this->cameraComponent->GetComponentRotation(); - /// 合成相机的总逻辑朝向 - const FRotator logicalRotation = rRot + aRot; - - /// 关键点:使用 DesiredZoomLength 预判物理终点位置 - const float intentLength = this->desiredZoomLength; - const FVector logicalOrigin = rPos + logicalRotation.Vector() * (-intentLength); - - const float fovValue = this->cameraComponent->FieldOfView; - float arValue = this->cameraComponent->AspectRatio; - if(arValue <= 0.0f) arValue = 1.777f; + const float fieldOfViewValue = this->cameraComponent->FieldOfView; + + // 拒绝假设:动态获取视口尺寸计算长宽比 + const FVector2D viewportSize = UWidgetLayoutLibrary::GetViewportSize(this->GetWorld()); + float aspectRatioValue = (viewportSize.Y > 0.0f) ? (viewportSize.X / viewportSize.Y) : this->cameraComponent->AspectRatio; + if (this->cameraComponent->bConstrainAspectRatio) + { + aspectRatioValue = this->cameraComponent->AspectRatio; + } - const float hFOV = FMath::DegreesToRadians(fovValue) / 2.0f; - const float vFOV = FMath::Atan(FMath::Tan(hFOV) / arValue); + const float horizontalFieldOfView = FMath::DegreesToRadians(fieldOfViewValue) / 2.0f; + const float verticalFieldOfView = FMath::Atan(FMath::Tan(horizontalFieldOfView) / aspectRatioValue); - const float tanH = FMath::Tan(hFOV); - const float tanV = FMath::Tan(vFOV); + const float tangentHorizontal = FMath::Tan(horizontalFieldOfView); + const float tangentVertical = FMath::Tan(verticalFieldOfView); - const FVector forwardVector = logicalRotation.Vector(); - const FVector rightVector = FRotationMatrix(logicalRotation).GetScaledAxis(EAxis::Y); - const FVector upVector = FRotationMatrix(logicalRotation).GetScaledAxis(EAxis::Z); + const FVector forwardVector = cameraRotation.Vector(); + const FVector rightVector = FRotationMatrix(cameraRotation).GetScaledAxis(EAxis::Y); + const FVector upVector = FRotationMatrix(cameraRotation).GetScaledAxis(EAxis::Z); /// 计算四个边界射线 - const FVector trDir = (forwardVector + rightVector * tanH + upVector * tanV).GetSafeNormal(); - const FVector tlDir = (forwardVector - rightVector * tanH + upVector * tanV).GetSafeNormal(); - const FVector brDir = (forwardVector + rightVector * tanH - upVector * tanV).GetSafeNormal(); - const FVector blDir = (forwardVector - rightVector * tanH - upVector * tanV).GetSafeNormal(); + const FVector topRightDirection = (forwardVector + rightVector * tangentHorizontal + upVector * tangentVertical).GetSafeNormal(); + const FVector topLeftDirection = (forwardVector - rightVector * tangentHorizontal + upVector * tangentVertical).GetSafeNormal(); + const FVector bottomRightDirection = (forwardVector + rightVector * tangentHorizontal - upVector * tangentVertical).GetSafeNormal(); + const FVector bottomLeftDirection = (forwardVector - rightVector * tangentHorizontal - upVector * tangentVertical).GetSafeNormal(); - const float gZ = this->rootComponent->GetComponentLocation().Z; + const float groundAltitude = this->rootComponent->GetComponentLocation().Z; - auto calcInt = [&](const FVector& ro, const FVector& rd) -> FVector + auto calculateIntersection = [&](const FVector& rayOrigin, const FVector& rayDirection) -> FVector { - if (rd.Z >= -0.001f) return ro + rd * 100000.0f; - float t = (gZ - ro.Z) / rd.Z; - if (t < 0.0f) return ro + rd * 100000.0f; - return ro + t * rd; + if (rayDirection.Z >= -0.001f) + { + return rayOrigin + rayDirection * 100000.0f; + } + + const float timeToIntersection = (groundAltitude - rayOrigin.Z) / rayDirection.Z; + if (timeToIntersection < 0.0f) + { + return rayOrigin + rayDirection * 100000.0f; + } + + return rayOrigin + rayDirection * timeToIntersection; }; - /// 填充静态数据数组,供外部 Widget 直接读取以降低渲染层级的内存开销 - this->minimapFrustumPoints[0] = calcInt(logicalOrigin, tlDir); - this->minimapFrustumPoints[1] = calcInt(logicalOrigin, trDir); - this->minimapFrustumPoints[2] = calcInt(logicalOrigin, brDir); - this->minimapFrustumPoints[3] = calcInt(logicalOrigin, blDir); + /// 填充战略投影点数组 + this->minimapFrustumPoints[0] = calculateIntersection(cameraLocation, topLeftDirection); + this->minimapFrustumPoints[1] = calculateIntersection(cameraLocation, topRightDirection); + this->minimapFrustumPoints[2] = calculateIntersection(cameraLocation, bottomRightDirection); + this->minimapFrustumPoints[3] = calculateIntersection(cameraLocation, bottomLeftDirection); - /// 计算完成后发起广播,通知 UI 订阅者执行定向重绘 + /// 计算完成后发起视野更新广播 this->onMinimapFrustumUpdated.Broadcast(); } + +void URTSCamera::applyBoundaryConstraints() +{ + if (this->movementBoundaryVolume == nullptr || this->springArmComponent == nullptr) + { + return; + } + + // 1. 获取边界数据 + FVector boxOrigin = FVector::ZeroVector; + FVector boxExtents = FVector::ZeroVector; + if (const AMinimapRegion* minimapRegion = Cast(this->movementBoundaryVolume)) + { + if (minimapRegion->BoundsComponent != nullptr) + { + boxExtents = minimapRegion->BoundsComponent->GetScaledBoxExtent(); + boxOrigin = minimapRegion->BoundsComponent->GetComponentLocation(); + } + } + + if (boxExtents.IsZero()) return; + + // 2. 地形高度同步 (取代 Tick 中的独立调用) + this->rectifyRootHeightFromTerrain(); + + // 3. 计算并应用偏移 + const FVector currentPos = this->rootComponent->GetComponentLocation(); + this->currentLateralSocketOffset = this->calculateYOffset(currentPos.Y); + this->currentVerticalSocketOffset = this->calculateXOffset(currentPos.X); + + this->springArmComponent->SocketOffset = FVector(this->currentVerticalSocketOffset, this->currentLateralSocketOffset, 0.0f); + + // 4. Root 物理坐标锁定 (核心:边界限制永远生效,Flag 仅控制是否产生 Offset) + FVector clampedLocation = currentPos; + clampedLocation.X = FMath::Clamp(clampedLocation.X, boxOrigin.X - boxExtents.X, boxOrigin.X + boxExtents.X); + clampedLocation.Y = FMath::Clamp(clampedLocation.Y, boxOrigin.Y - boxExtents.Y, boxOrigin.Y + boxExtents.Y); + + this->rootComponent->SetWorldLocation(clampedLocation); +} + +float URTSCamera::calculateYOffset(float worldY) const +{ + if (!this->bEnableYBoundaryConstraint || this->movementBoundaryVolume == nullptr) return 0.0f; + + const AMinimapRegion* minimapRegion = Cast(this->movementBoundaryVolume); + if (!minimapRegion || !minimapRegion->BoundsComponent) return 0.0f; + + const FVector boxOrigin = minimapRegion->BoundsComponent->GetComponentLocation(); + const FVector boxExtents = minimapRegion->BoundsComponent->GetScaledBoxExtent(); + const float mapOverflow = minimapRegion->MapOverflowUU; + + const float differenceY = worldY - boxOrigin.Y; + const float normalizedDistanceY = FMath::Abs(differenceY) / FMath::Max(boxExtents.Y, 1.0f); + const float safeZoneRatio = 1.0f - this->boundaryTransitionZoneRatio; + + if (normalizedDistanceY > safeZoneRatio) + { + const float triggerAlpha = (normalizedDistanceY - safeZoneRatio) / FMath::Max(this->boundaryTransitionZoneRatio, 0.01f); + const float reach = this->springArmComponent->TargetArmLength * this->lateralReachFactor; + const float offset = triggerAlpha * (reach * this->minimumZoomBoundaryConstraint) * ((differenceY > 0.0f) ? -1.0f : 1.0f); + + UE_LOG(LogRTSCamera, VeryVerbose, TEXT("横向 (Y) 比例补偿: alpha=%.2f, reach=%.1f, offset=%.1f"), triggerAlpha, reach, offset); + return offset; + } + + return 0.0f; +} + +float URTSCamera::calculateXOffset(float worldX) const +{ + if (!this->bEnableXBoundaryConstraint || this->movementBoundaryVolume == nullptr) return 0.0f; + + const AMinimapRegion* minimapRegion = Cast(this->movementBoundaryVolume); + if (!minimapRegion || !minimapRegion->BoundsComponent) return 0.0f; + + const FVector boxOrigin = minimapRegion->BoundsComponent->GetComponentLocation(); + const FVector boxExtents = minimapRegion->BoundsComponent->GetScaledBoxExtent(); + const float mapOverflow = minimapRegion->MapOverflowUU; + + const float differenceX = worldX - boxOrigin.X; + const float normalizedDistanceX = FMath::Abs(differenceX) / FMath::Max(boxExtents.X, 1.0f); + const float safeZoneRatio = 1.0f - this->boundaryTransitionZoneRatio; + + if (normalizedDistanceX > safeZoneRatio) + { + const float triggerAlpha = (normalizedDistanceX - safeZoneRatio) / FMath::Max(this->boundaryTransitionZoneRatio, 0.01f); + + // 南北向由于 Pitch 倾角是不对称的 + // 北端 (diff > 0) 使用前进伸展 forwardReach;南端 (diff < 0) 使用后退伸展 backwardReach + const float currentFactor = (differenceX > 0.0f) ? this->forwardReachFactor : this->backwardReachFactor; + const float reach = this->springArmComponent->TargetArmLength * currentFactor; + + // 核心逻辑: + // 1. 在北端 (diff > 0),我们需要向南 (Negative X) 偏移,把视口顶部的地图拉回来。 + // 2. 在南端 (diff < 0),我们需要向北 (Positive X) 偏移,把视口底部的地图边界拉进来。 + const float direction = (differenceX > 0.0f) ? -1.0f : 1.0f; + const float offset = direction * triggerAlpha * (reach * this->minimumZoomBoundaryConstraint); + + UE_LOG(LogRTSCamera, VeryVerbose, TEXT("南北 (X) 修正方案: diff=%.1f, factor=%.4f, offset=%.1f"), differenceX, currentFactor, offset); + return offset; + } + + return 0.0f; +} diff --git a/Source/OpenRTSCamera/Private/RTSHUD.cpp b/Source/OpenRTSCamera/Private/RTSHUD.cpp index 5a00793..c778944 100644 --- a/Source/OpenRTSCamera/Private/RTSHUD.cpp +++ b/Source/OpenRTSCamera/Private/RTSHUD.cpp @@ -73,14 +73,102 @@ void ARTSHUD::PerformSelection_Implementation() TArray SelectedActors; GetActorsInSelectionRectangle(SelectionStart, SelectionEnd, SelectedActors, false, false); + // Filter for Selectable Actors + TArray ValidSelectableActors; + for (AActor* Actor : SelectedActors) + { + if (Actor && Actor->FindComponentByClass()) + { + ValidSelectableActors.Add(Actor); + } + } + // Find the URTSSelector component and pass the selected actors to it. if (const auto PC = GetOwningPlayerController()) { if (const auto SelectorComponent = PC->FindComponentByClass()) { - SelectorComponent->HandleSelectedActors(SelectedActors); + if (ValidSelectableActors.Num() > 0) + { + UE_LOG(LogTemp, Log, TEXT("RTSHUD: Found %d Selectable Actors, skipping Mass selection."), ValidSelectableActors.Num()); + SelectorComponent->HandleSelectedActors(ValidSelectableActors); + } + else + { + UE_LOG(LogTemp, Log, TEXT("RTSHUD: No Selectable Actors found, attempting Mass selection.")); + // Clear previous selection and try MassBattle selection + SelectorComponent->HandleSelectedActors(TArray()); + PerformMassSelection(); + } } } bIsPerformingSelection = false; } + +#include "MassBattleFuncLib.h" +#include "MassBattleStructs.h" + +void ARTSHUD::PerformMassSelection() +{ + APlayerController* PC = GetOwningPlayerController(); + if (!PC || !PC->PlayerCameraManager) return; + + // Calculate selection box bounds + FVector2D Min(FMath::Min(SelectionStart.X, SelectionEnd.X), FMath::Min(SelectionStart.Y, SelectionEnd.Y)); + FVector2D Max(FMath::Max(SelectionStart.X, SelectionEnd.X), FMath::Max(SelectionStart.Y, SelectionEnd.Y)); + + // Minimum selection size threshold + if (FVector2D::DistSquared(Min, Max) < 100.0f) + { + UE_LOG(LogTemp, Warning, TEXT("RTSHUD: Selection box too small for Mass pick.")); + return; + } + + TArray ScreenPoints = { + Min, + FVector2D(Max.X, Min.Y), + Max, + FVector2D(Min.X, Max.Y) + }; + + FViewTracePoints TracePoints; + TracePoints.ViewPoint = PC->PlayerCameraManager->GetCameraLocation(); + + for (const FVector2D& ScreenPoint : ScreenPoints) + { + FVector WorldPos, WorldDirection; + if (PC->DeprojectScreenPositionToWorld(ScreenPoint.X, ScreenPoint.Y, WorldPos, WorldDirection)) + { + // Project points onto a plane at some distance to form the frustum + // Increased to 100,000 to ensure it covers the ground from typical RTS camera heights + TracePoints.SelectionPoints.Add(WorldPos + WorldDirection * 100000.0f); + } + } + + UE_LOG(LogTemp, Log, TEXT("RTSHUD: Generated %d Selection Points for Mass Trace."), TracePoints.SelectionPoints.Num()); + + if (TracePoints.SelectionPoints.Num() == 4) + { + bool bHit = false; + TArray Results; + UMassBattleFuncLib::ViewTraceForAgents(this, bHit, Results, -1, TracePoints); + + if (bHit) + { + UE_LOG(LogTemp, Log, TEXT("RTSHUD: Selected %d Mass Entities"), Results.Num()); + for (const FTraceResult& Result : Results) + { + UE_LOG(LogTemp, Log, TEXT(" - Entity Index: %d"), Result.Entity.Index); + } + } + else + { + UE_LOG(LogTemp, Warning, TEXT("RTSHUD: Mass Trace returned NO hits (bHit=false).")); + } + } + else + { + UE_LOG(LogTemp, Error, TEXT("RTSHUD: Failed to generate 4 world points for Mass Trace.")); + } +} diff --git a/Source/OpenRTSCamera/Public/RTSCamera.h b/Source/OpenRTSCamera/Public/RTSCamera.h index 1444805..73d2d92 100644 --- a/Source/OpenRTSCamera/Public/RTSCamera.h +++ b/Source/OpenRTSCamera/Public/RTSCamera.h @@ -22,15 +22,15 @@ struct FMoveCameraCommand /// 目标位置在 X 轴上的分量增量 UPROPERTY() - float x = 0; + float xAxisValue = 0; /// 目标位置在 Y 轴上的分量增量 UPROPERTY() - float y = 0; + float yAxisValue = 0; /// 本次移动指令的缩放权重比例 UPROPERTY() - float scale = 0; + float movementScale = 0; }; /** @@ -106,39 +106,39 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent AActor* getMovementBoundaryVolume() const { return movementBoundaryVolume; } /// 相机缩放的最小目标距离(最接近地面) - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings", meta = (DisplayName = "最小缩放高度", ToolTip = "相机距离地面的最近距离。")) float minimumZoomLength; /// 相机缩放的最大目标距离(最高视野) - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings", meta = (DisplayName = "最大缩放高度", ToolTip = "相机距离地面的最远距离。")) float maximumZoomLength; /// 缩放插值的补全速率(值越大,物理位置追赶意图的速度越快) - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings", meta = (DisplayName = "缩放平滑速度", ToolTip = "物理相机追赶缩放意图的速度。")) float zoomCatchupSpeed; /// 单次滚轮操作触发的缩放距离步长 - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings", meta = (DisplayName = "单次缩进步幅", ToolTip = "鼠标滚轮单次滚动引起的距离变化量。")) float zoomSpeed; /// 初始化时的相机俯仰角 (Pitch) - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "初始俯仰角", ToolTip = "相机的初始垂直倾斜角度(度)。")) float startingPitchAngle; /// 初始化时的相机偏航角 (Yaw) - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "初始偏航角", ToolTip = "相机的初始水平旋转角度(度)。")) float startingYawAngle; /// 相机在最大缩放高度时的移动速度上限 - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "最大平移速度", ToolTip = "在高空缩放时的移动速度。")) float maxMovementSpeed; /// 相机在最小缩放高度时的基础移动速度 - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "最小平移速度", ToolTip = "在低空缩放时的移动速度。")) float minMovementSpeed; /// 输入控制下的水平旋转感官速度 - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "旋转响应速度", ToolTip = "相机水平旋转的速度权重。")) float rotationSpeed; /// 相机拖拽操作在视口中的拉伸增量比例 @@ -146,16 +146,31 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", - meta = (ClampMin = "0.0", ClampMax = "1.0") + meta = (ClampMin = "0.0", ClampMax = "1.0", DisplayName = "拖拽惯性系数", ToolTip = "鼠标拖拽移动时的平滑权重。") ) float dragExtent; + /** 当接近边界时,约束触发的插值强度 (0=相机根部重合边缘, 1=视野边缘对齐边缘) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera | Boundary", meta = (ClampMin = "0.0", ClampMax = "1.0", DisplayName = "低空边界约束强度", ToolTip = "0表示相机的根组件中心能够到达边界,1表示相机的视野边缘会被锁在地图内部。")) + float minimumZoomBoundaryConstraint; + + /** 边界侧倾过渡区的比例 (0.15 代表最后 15% 区域触发) */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera | Boundary", meta = (ClampMin = "0.0", ClampMax = "0.5", DisplayName = "边界侧倾过渡比例", ToolTip = "定义从地图边缘向内多少比例开始产生侧倾效果。")) + float boundaryTransitionZoneRatio; + + /** 是否启用 X 轴 (南北/上下) 方向的边界视野约束 */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera | Boundary", meta = (DisplayName = "启用 X 轴边界约束")) + bool bEnableXBoundaryConstraint; + + /** 是否启用 Y 轴 (东西/左右) 方向的边界视野约束 */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera | Boundary", meta = (DisplayName = "启用 Y 轴边界约束")) + bool bEnableYBoundaryConstraint; /// 启用相机位置移动的插值延迟 - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "启用位置平滑", ToolTip = "开启后相机移动将具有物理惯性。")) bool enableCameraLag; /// 启用相机视野旋转的插值延迟 - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "启用旋转平滑", ToolTip = "开启后相机旋转将具有物理惯性。")) bool enableCameraRotationLag; /// 启用基于地形起伏动态修正相机根高度的功能 @@ -281,11 +296,11 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent /** * @brief 将坐标移动意图转化为战术指令并加入执行队列 * - * @param 参数名称: x 数据类型: float - * @param 参数名称: y 数据类型: float - * @param 参数名称: scale 数据类型: float + * @param xAxisValue 数据类型: float + * @param yAxisValue 数据类型: float + * @param movementScale 数据类型: float **/ - void requestCameraMovement(float x, float y, float scale); + void requestCameraMovement(float xAxisValue, float yAxisValue, float movementScale); /** * @brief 在一个逻辑帧内,分步执行指令队列中积压的所有平移指令 @@ -339,20 +354,37 @@ class OPENRTSCAMERA_API URTSCamera : public UActorComponent void updateFollowPositionIfTargetActive(); void handleTargetArmLengthInterpolation(); void rectifyRootHeightFromTerrain(); - void enforceCameraMovementBounds(); + + /** @brief 计算当前坐标下的边界补偿并应用 */ + void applyBoundaryConstraints(); - /// 检索边界体积时匹配的静态场景标签 - UPROPERTY() - FName cameraBlockingVolumeTag; + /** @brief 基于 Y 坐标计算 Lateral Socket Offset */ + float calculateYOffset(float worldY) const; + + /** @brief 基于 X 坐标计算 Vertical Socket Offset */ + float calculateXOffset(float worldX) const; /// 相机当前正在锁定跟随的 Actor 实测对象 UPROPERTY() AActor* activeCameraFollowTarget; + /// 缓存边界侧移量 (SocketOffset.Y) + float currentLateralSocketOffset; + /// 缓存边界纵移量 (SocketOffset.X) + float currentVerticalSocketOffset; /// 自上一帧以来的时间增量(秒) UPROPERTY() float deltaSeconds; + /** @brief 预计算的横向延伸系数 (Lateral Reach / TargetArmLength) */ + float lateralReachFactor; + + /** @brief 预计算的纵向延伸系数 (Forward Reach / TargetArmLength) */ + float forwardReachFactor; + + /** @brief 预计算的后向延伸系数 (Backward Reach / TargetArmLength) */ + float backwardReachFactor; + /// 状态位:指示是否正在进行鼠标拖拽操作 UPROPERTY() bool isDragging; diff --git a/Source/OpenRTSCamera/Public/RTSHUD.h b/Source/OpenRTSCamera/Public/RTSHUD.h index e4215a8..95ef004 100644 --- a/Source/OpenRTSCamera/Public/RTSHUD.h +++ b/Source/OpenRTSCamera/Public/RTSHUD.h @@ -38,6 +38,8 @@ class OPENRTSCAMERA_API ARTSHUD : public AHUD virtual void DrawHUD() override; private: + void PerformMassSelection(); + bool bIsDrawingSelectionBox; bool bIsPerformingSelection; FVector2D SelectionStart; From 54ef7c7f9646ec198ae18345668ca27ff9d504cb Mon Sep 17 00:00:00 2001 From: winyunq Date: Mon, 16 Feb 2026 14:26:00 +0800 Subject: [PATCH 08/12] =?UTF-8?q?-=20=E5=AE=9E=E7=8E=B0=20Starcraft=20?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC=E7=9A=84=E9=80=89=E6=8B=A9=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=9A=20=20=20-=20=E5=B7=A6=E9=94=AE?= =?UTF-8?q?=E7=82=B9=E5=87=BB=EF=BC=9A=E5=8D=95=E9=80=89=20(Exclusive=20Se?= =?UTF-8?q?lect)=E3=80=82=20=20=20-=20=E5=B7=A6=E9=94=AE=E6=A1=86=E9=80=89?= =?UTF-8?q?=EF=BC=9A=E5=A4=9A=E9=80=89=20(Box=20Select)=E3=80=82=20=20=20-?= =?UTF-8?q?=20Shift=20+=20=E7=82=B9=E5=87=BB=EF=BC=9A=E5=8F=8D=E9=80=89/?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=20(Toggle/Remove)=E3=80=82=20=20=20-=20Shift?= =?UTF-8?q?=20+=20=E6=A1=86=E9=80=89=EF=BC=9A=E8=BF=BD=E5=8A=A0=E9=80=89?= =?UTF-8?q?=E6=8B=A9=20(Append)=EF=BC=8C=E4=B8=94=E5=85=B7=E5=A4=87?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=80=E8=87=B4=E6=80=A7=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=20(Type=20Consistency)=E3=80=82=20=20=20-=20Ctrl=20+=20?= =?UTF-8?q?=E7=82=B9=E5=87=BB=EF=BC=9A=E5=85=A8=E5=B1=8F=E5=90=8C=E7=B1=BB?= =?UTF-8?q?=E9=80=89=E6=8B=A9=20(Select=20All=20On=20Screen)=E3=80=82=20?= =?UTF-8?q?=20=20-=20Tab=EF=BC=9A=E5=9C=A8=E9=80=89=E4=B8=AD=E5=8D=95?= =?UTF-8?q?=E4=BD=8D=E7=9A=84=E5=AD=90=E7=BB=84=E9=97=B4=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E5=88=87=E6=8D=A2=20(Cycle=20Subgroup)=E3=80=82=20-=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A1=86=E9=80=89=E5=88=A4=E5=AE=9A=E9=98=88?= =?UTF-8?q?=E5=80=BC=EF=BC=9A=20=20=20-=20=E5=BC=95=E5=85=A5=201=E5=83=8F?= =?UTF-8?q?=E7=B4=A0=20(1px)=20=E4=B8=A5=E6=A0=BC=E9=98=88=E5=80=BC?= =?UTF-8?q?=EF=BC=8C=E5=8C=BA=E5=88=86=E7=82=B9=E5=87=BB=E4=B8=8E=E6=A1=86?= =?UTF-8?q?=E9=80=89=E3=80=82=20=20=20-=20=E5=8F=AA=E6=9C=89=E6=8B=96?= =?UTF-8?q?=E6=8B=BD=E8=B7=9D=E7=A6=BB=20>=201px=20=E6=97=B6=E6=89=8D?= =?UTF-8?q?=E7=BB=98=E5=88=B6=E7=BB=BF=E6=A1=86=E5=92=8C=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=A1=86=E9=80=89=E9=80=BB=E8=BE=91=EF=BC=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E6=89=80=E8=A7=81=E5=8D=B3=E6=89=80=E5=BE=97=E3=80=82=20-=20?= =?UTF-8?q?=E5=A2=9E=E5=BC=BA=E8=A7=86=E8=A7=89=E5=8F=8D=E9=A6=88=EF=BC=9A?= =?UTF-8?q?=20=20=20-=20=E7=BB=BF=E6=A1=86=E5=A2=9E=E5=8A=A0=E5=8D=8A?= =?UTF-8?q?=E9=80=8F=E6=98=8E=E8=92=99=E5=B1=82=20(SelectionBoxFillColor)?= =?UTF-8?q?=EF=BC=8C=E6=8F=90=E5=8D=87=E8=BE=A8=E8=AF=86=E5=BA=A6=E3=80=82?= =?UTF-8?q?=20=20=20-=20UI=20=E5=A2=9E=E5=8A=A0=20Grid=20=E6=B1=A0?= =?UTF-8?q?=E5=8C=96=E5=A4=8D=E7=94=A8=E9=80=BB=E8=BE=91=EF=BC=8C=E8=A7=A3?= =?UTF-8?q?=E5=86=B3=20Grid=20=E5=B8=83=E5=B1=80=E6=8A=96=E5=8A=A8?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E3=80=82=20-=20=E6=95=B0=E6=8D=AE=E5=B1=82?= =?UTF-8?q?=E5=AE=8C=E5=96=84=EF=BC=9A=20=20=20-=20URTSSelectable=20?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=96=B0=E5=A2=9E=20Icon,=20Health,=20Shield?= =?UTF-8?q?=20=E7=AD=89=E5=9F=BA=E7=A1=80=20RTS=20=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E3=80=82=20=20=20-=20SelectionSubsystem=20=E6=AD=A3=E7=A1=AE?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E5=B9=B6=E4=BC=A0=E9=80=92=E8=BF=99=E4=BA=9B?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=88=B0=20UI=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/OpenRTSCamera/Private/RTSHUD.cpp | 249 +++++++++++++-- .../Private/RTSSelectionSubsystem.cpp | 302 ++++++++++++++++++ .../Private/UI/RTSActiveGroupWidget.cpp | 66 ++++ .../Private/UI/RTSSelectionWidget.cpp | 296 +++++++++++++++++ .../Private/UI/RTSUnitIconWidget.cpp | 152 +++++++++ Source/OpenRTSCamera/Public/RTSHUD.h | 8 +- Source/OpenRTSCamera/Public/RTSSelectable.h | 20 ++ .../Public/RTSSelectionStructs.h | 99 ++++++ .../Public/RTSSelectionSubsystem.h | 98 ++++++ .../Public/UI/RTSActiveGroupWidget.h | 45 +++ .../Public/UI/RTSSelectionWidget.h | 93 ++++++ .../Public/UI/RTSUnitIconWidget.h | 68 ++++ 12 files changed, 1470 insertions(+), 26 deletions(-) create mode 100644 Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp create mode 100644 Source/OpenRTSCamera/Private/UI/RTSActiveGroupWidget.cpp create mode 100644 Source/OpenRTSCamera/Private/UI/RTSSelectionWidget.cpp create mode 100644 Source/OpenRTSCamera/Private/UI/RTSUnitIconWidget.cpp create mode 100644 Source/OpenRTSCamera/Public/RTSSelectionStructs.h create mode 100644 Source/OpenRTSCamera/Public/RTSSelectionSubsystem.h create mode 100644 Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h create mode 100644 Source/OpenRTSCamera/Public/UI/RTSSelectionWidget.h create mode 100644 Source/OpenRTSCamera/Public/UI/RTSUnitIconWidget.h diff --git a/Source/OpenRTSCamera/Private/RTSHUD.cpp b/Source/OpenRTSCamera/Private/RTSHUD.cpp index c778944..561ebfe 100644 --- a/Source/OpenRTSCamera/Private/RTSHUD.cpp +++ b/Source/OpenRTSCamera/Private/RTSHUD.cpp @@ -1,4 +1,7 @@ #include "RTSHUD.h" +#include "RTSSelectionSubsystem.h" +#include "RTSSelectable.h" +#include "MassBattleFuncLib.h" #include "RTSSelector.h" #include "Engine/Canvas.h" @@ -6,7 +9,9 @@ ARTSHUD::ARTSHUD() { SelectionBoxColor = FLinearColor::Green; + SelectionBoxFillColor = FLinearColor(0.0f, 1.0f, 0.0f, 0.15f); SelectionBoxThickness = 1.0f; + MinSelectionSizeSq = 1.0f; // 1 pixel threshold as requested bIsDrawingSelectionBox = false; bIsPerformingSelection = false; } @@ -16,23 +21,43 @@ void ARTSHUD::DrawHUD() { Super::DrawHUD(); // Call the base class implementation. - // Draw the selection box if it's active. + // Draw the selection box if it's active AND large enough to be a box. if (bIsDrawingSelectionBox) { - DrawSelectionBox(SelectionStart, SelectionEnd); + if (FVector2D::DistSquared(SelectionStart, SelectionEnd) > MinSelectionSizeSq) + { + DrawSelectionBox(SelectionStart, SelectionEnd); + } } + // Perform selection actions if required. // Perform selection actions if required. if (bIsPerformingSelection) { PerformSelection(); } + + // --- Input Polling (One-Step Solution) --- + if (APlayerController* PC = GetOwningPlayerController()) + { + if (PC->WasInputKeyJustPressed(EKeys::Tab)) + { + if (const ULocalPlayer* LP = PC->GetLocalPlayer()) + { + if (URTSSelectionSubsystem* Subsystem = LP->GetSubsystem()) + { + Subsystem->CycleGroup(); + } + } + } + } } // Starts the selection process, setting the initial point and activating the selection flag. void ARTSHUD::BeginSelection(const FVector2D& StartPoint) { SelectionStart = StartPoint; + SelectionEnd = StartPoint; // Initialize End to Start to avoid stale data bIsDrawingSelectionBox = true; } @@ -54,11 +79,32 @@ void ARTSHUD::DrawSelectionBox_Implementation(const FVector2D& StartPoint, const { if (Canvas) { - // Calculate corners of the selection rectangle. + // Calculate Top-Left and Size + float MinX = FMath::Min(SelectionStart.X, SelectionEnd.X); + float MinY = FMath::Min(SelectionStart.Y, SelectionEnd.Y); + float Width = FMath::Abs(SelectionEnd.X - SelectionStart.X); + float Height = FMath::Abs(SelectionEnd.Y - SelectionStart.Y); + + // 1. Draw Fill (Semi-transparent) + if (Width > 0 && Height > 0) + { + // Note: K2_DrawRect uses current Canvas position? No, it usually takes screen pos? + // Actually K2_DrawRect is tricky in UCanvas. + // Standard way: Canvas->K2_DrawTexture(WhiteTexture, ScreenPos, ScreenSize, ... Tint). + // If we don't have a WhiteTexture, Update: UCanvas::K2_DrawMaterial matches best? + // Let's use `Canvas->K2_DrawPolygon`? No. + + // Ah, `DrawRect` (C++ API): `Canvas->DrawTile(WhiteTexture, X, Y, W, H, ...)` + // Wait, let's look at `Canvas->K2_DrawRect`. It exists in `UCanvas`. + // `void UCanvas::K2_DrawRect(FLinearColor RenderTextureColor, FVector2D ScreenPosition, FVector2D ScreenSize)` + + DrawRect(SelectionBoxFillColor, MinX, MinY, Width, Height); + } + + // 2. Draw Borders const auto TopRight = FVector2D(SelectionEnd.X, SelectionStart.Y); const auto BottomLeft = FVector2D(SelectionStart.X, SelectionEnd.Y); - // Draw lines to form the selection rectangle. Canvas->K2_DrawLine(SelectionStart, TopRight, SelectionBoxThickness, SelectionBoxColor); Canvas->K2_DrawLine(TopRight, SelectionEnd, SelectionBoxThickness, SelectionBoxColor); Canvas->K2_DrawLine(SelectionEnd, BottomLeft, SelectionBoxThickness, SelectionBoxColor); @@ -66,41 +112,190 @@ void ARTSHUD::DrawSelectionBox_Implementation(const FVector2D& StartPoint, const } } +#include "RTSSelectionSubsystem.h" + // Default implementation of PerformSelection. Selects actors within the selection box. void ARTSHUD::PerformSelection_Implementation() { - // Array to store actors that are within the selection rectangle. - TArray SelectedActors; - GetActorsInSelectionRectangle(SelectionStart, SelectionEnd, SelectedActors, false, false); + // 1. Determine Selection Constraints & Modifier + bool bCanSelectActors = true; + bool bCanSelectMass = true; + ERTSSelectionModifier Modifier = ERTSSelectionModifier::Replace; + + URTSSelectionSubsystem* SelectionSubsystem = nullptr; + URTSSelector* SelectorComponent = nullptr; - // Filter for Selectable Actors - TArray ValidSelectableActors; - for (AActor* Actor : SelectedActors) + APlayerController* PC = GetOwningPlayerController(); + if (PC) { - if (Actor && Actor->FindComponentByClass()) + // Find Components/Subsystems + SelectorComponent = PC->FindComponentByClass(); + if (const ULocalPlayer* LP = PC->GetLocalPlayer()) { - ValidSelectableActors.Add(Actor); + SelectionSubsystem = LP->GetSubsystem(); + } + + // Check Input Modifier (Shift = Add) + if (PC->IsInputKeyDown(EKeys::LeftShift) || PC->IsInputKeyDown(EKeys::RightShift)) + { + Modifier = ERTSSelectionModifier::Add; + + // "Type Consistency" Logic: + // If we are ADDING to selection, we restrict new selection to match existing type. + if (SelectionSubsystem) + { + if (SelectionSubsystem->HasSelectedActors()) + { + bCanSelectMass = false; // Already have Actors -> Don't add Mass + } + else if (SelectionSubsystem->HasSelectedMass()) + { + bCanSelectActors = false; // Already have Mass -> Don't add Actors + } + } } } - // Find the URTSSelector component and pass the selected actors to it. - if (const auto PC = GetOwningPlayerController()) + // 2. Perform Actor Selection (if allowed) + TArray FinalActorSelection; + + if (bCanSelectActors) { - if (const auto SelectorComponent = PC->FindComponentByClass()) + TArray RawActors; + GetActorsInSelectionRectangle(SelectionStart, SelectionEnd, RawActors, false, false); + + // Filter for Selectable + for (AActor* Actor : RawActors) { - if (ValidSelectableActors.Num() > 0) + if (Actor && Actor->FindComponentByClass()) { - UE_LOG(LogTemp, Log, TEXT("RTSHUD: Found %d Selectable Actors, skipping Mass selection."), ValidSelectableActors.Num()); - SelectorComponent->HandleSelectedActors(ValidSelectableActors); + FinalActorSelection.Add(Actor); } - else + } + } + + // 3. Perform Mass Selection (Fallback logic: Only if No Actors found AND Mass is allowed) + TArray FinalMassSelection; + bool bActorsFound = FinalActorSelection.Num() > 0; + + if (!bActorsFound && bCanSelectMass) + { + // Only attempt Mass select if we didn't Pick Actors (Priority) AND Mass is allowed (Consistency) + PerformMassSelection(FinalMassSelection); + } + + // 4. Toggle Logic (Shift + Single Click = Deselect) + // 4. Toggle Logic (Shift + Single Click = Deselect) + // ONLY apply toggle if this was a Click (not a Box Drag). + // Threshold: MinSelectionSizeSq (Synced with Visuals). + float DragDistSq = FVector2D::DistSquared(SelectionStart, SelectionEnd); + + if (Modifier == ERTSSelectionModifier::Add && SelectionSubsystem) + { + UE_LOG(LogTemp, Log, TEXT("RTSHUD: Shift Action - DragDistSq: %f (Threshold: %f)"), DragDistSq, MinSelectionSizeSq); + + if (DragDistSq <= MinSelectionSizeSq) + { + // Case A: Single Actor Toggle + if (FinalActorSelection.Num() == 1 && FinalMassSelection.Num() == 0) + { + if (SelectionSubsystem->IsActorSelected(FinalActorSelection[0])) + { + Modifier = ERTSSelectionModifier::Remove; + UE_LOG(LogTemp, Log, TEXT("RTSHUD: Toggling Single Actor OFF (Remove).")); + } + } + // Case B: Single Mass Entity Toggle + else if (FinalActorSelection.Num() == 0 && FinalMassSelection.Num() == 1) + { + if (SelectionSubsystem->IsEntitySelected(FinalMassSelection[0])) + { + Modifier = ERTSSelectionModifier::Remove; + UE_LOG(LogTemp, Log, TEXT("RTSHUD: Toggling Single Entity OFF (Remove).")); + } + } + } + } + + // 5. Ctrl + Click (Select All of Same Type On Screen) + if (PC && (PC->IsInputKeyDown(EKeys::LeftControl) || PC->IsInputKeyDown(EKeys::RightControl))) + { + // Only apply if it was a Click (not a Box Drag) + if (DragDistSq <= MinSelectionSizeSq) + { + // Strategy: If we clicked a single unit, find all matching units on screen. + + // 1. Actor Group Selection + if (FinalActorSelection.Num() == 1) { - UE_LOG(LogTemp, Log, TEXT("RTSHUD: No Selectable Actors found, attempting Mass selection.")); - // Clear previous selection and try MassBattle selection - SelectorComponent->HandleSelectedActors(TArray()); - PerformMassSelection(); + AActor* TemplateActor = FinalActorSelection[0]; + if (TemplateActor) + { + UClass* MatchClass = TemplateActor->GetClass(); + + // Get Viewport Size + int32 ViewportX, ViewportY; + PC->GetViewportSize(ViewportX, ViewportY); + + // Select All in Viewport + TArray AllScreenActors; + GetActorsInSelectionRectangle(FVector2D(0,0), FVector2D(ViewportX, ViewportY), AllScreenActors, false, false); + + // Filter by Class + FinalActorSelection.Reset(); + for(AActor* Act : AllScreenActors) + { + if (Act && Act->GetClass() == MatchClass && Act->FindComponentByClass()) + { + FinalActorSelection.Add(Act); + } + } + + // Force Replace Mode for Group Select + Modifier = ERTSSelectionModifier::Replace; + // Clear Mass (prioritize Actor group) + FinalMassSelection.Reset(); + } } + // 2. Mass Entity Group Selection (Future TODO if specific Mass types needed) + else if (FinalMassSelection.Num() > 0) + { + // For Mass, we might need to check Archetype or Entity Config traits. + // For now, if we Ctrl+Click a Mass unit, we might select ALL Mass units on screen? + // Simplistic implementation: Select ALL valid Mass implementation if we clicked one. + + // Re-run PerformMassSelection with Full Screen? + // Note: Mass selection is expensive. Let's stick to Actor logic first as requested. + } + } + } + + // 6. Update Visuals & Subsystem + + // Visual Highlighting (Actors) + if (SelectorComponent) + { + if (bActorsFound) + { + UE_LOG(LogTemp, Log, TEXT("RTSHUD: Found %d Selectable Actors."), FinalActorSelection.Num()); + SelectorComponent->HandleSelectedActors(FinalActorSelection); } + else + { + // Clear Actor visuals (we either found nothing or found Mass) + SelectorComponent->HandleSelectedActors(TArray()); + + if (FinalMassSelection.Num() > 0) + { + UE_LOG(LogTemp, Log, TEXT("RTSHUD: Selected %d Mass Entities."), FinalMassSelection.Num()); + } + } + } + + // Update Data Store + if (SelectionSubsystem) + { + SelectionSubsystem->SetSelectedUnits(FinalActorSelection, FinalMassSelection, Modifier); } bIsPerformingSelection = false; @@ -109,8 +304,10 @@ void ARTSHUD::PerformSelection_Implementation() #include "MassBattleFuncLib.h" #include "MassBattleStructs.h" -void ARTSHUD::PerformMassSelection() +void ARTSHUD::PerformMassSelection(TArray& OutEntities) { + OutEntities.Reset(); + APlayerController* PC = GetOwningPlayerController(); if (!PC || !PC->PlayerCameraManager) return; @@ -159,7 +356,9 @@ void ARTSHUD::PerformMassSelection() UE_LOG(LogTemp, Log, TEXT("RTSHUD: Selected %d Mass Entities"), Results.Num()); for (const FTraceResult& Result : Results) { - UE_LOG(LogTemp, Log, TEXT(" - Entity Index: %d"), Result.Entity.Index); + OutEntities.Add(Result.Entity); + // Log abbreviated + // UE_LOG(LogTemp, Log, TEXT(" - Entity Index: %d"), Result.Entity.Index); } } else diff --git a/Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp b/Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp new file mode 100644 index 0000000..f575d68 --- /dev/null +++ b/Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp @@ -0,0 +1,302 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#include "RTSSelectionSubsystem.h" +#include "RTSSelectable.h" +#include "MassEntitySubsystem.h" +#include "MassEntityManager.h" +#include "Engine/World.h" +#include "GameFramework/Actor.h" + +void URTSSelectionSubsystem::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); +} + +void URTSSelectionSubsystem::Deinitialize() +{ + Super::Deinitialize(); +} + +// Note: In header, bKeepIndex defaults to false. In CPP, we don't repeat the default value. +void URTSSelectionSubsystem::SetSelectedUnits(const TArray& InActors, const TArray& InEntities, ERTSSelectionModifier Modifier) +{ + // 1. Update Internal State based on Modifier + if (Modifier == ERTSSelectionModifier::Replace) + { + SelectedActors = InActors; + SelectedEntities = InEntities; + } + else if (Modifier == ERTSSelectionModifier::Add) + { + // Append Actors (Unique) + for (AActor* Actor : InActors) + { + SelectedActors.AddUnique(Actor); + } + + // Append Entities (Unique) + for (const FEntityHandle& Handle : InEntities) + { + SelectedEntities.AddUnique(Handle); + } + } + else if (Modifier == ERTSSelectionModifier::Remove) + { + // Remove Actors + for (AActor* Actor : InActors) + { + SelectedActors.Remove(Actor); + } + + // Remove Entities + for (const FEntityHandle& Handle : InEntities) + { + SelectedEntities.Remove(Handle); + } + } + + // 2. Generate View Data + FRTSSelectionView View; + int32 TotalCount = SelectedActors.Num() + SelectedEntities.Num(); + + if (TotalCount == 0) + { + View.Mode = ERTSSelectionMode::Single; + } + else if (TotalCount == 1) + { + View.Mode = ERTSSelectionMode::Single; + if (SelectedActors.Num() > 0) + { + View.SingleUnit = CreateUnitDataFromActor(SelectedActors[0]); + View.Items.Add(View.SingleUnit); + } + else + { + View.SingleUnit = CreateUnitDataFromEntity(SelectedEntities[0]); + View.Items.Add(View.SingleUnit); + } + } + else if (TotalCount <= ListModeMaxCount) + { + View.Mode = ERTSSelectionMode::List; + + for (AActor* Actor : SelectedActors) + { + View.Items.Add(CreateUnitDataFromActor(Actor)); + } + + for (const FEntityHandle& Handle : SelectedEntities) + { + View.Items.Add(CreateUnitDataFromEntity(Handle)); + } + } + else + { + View.Mode = ERTSSelectionMode::Summary; + + // Map for grouping: TypeName -> GroupData + TMap GroupMap; + + for (AActor* Actor : SelectedActors) + { + FRTSUnitData Data = CreateUnitDataFromActor(Actor); + FRTSUnitData& Group = GroupMap.FindOrAdd(Data.Name); + if (Group.Count == 0 || Group.Name.IsEmpty()) + { + Group = Data; + Group.Count = 0; + } + Group.Count++; + } + + for (const FEntityHandle& Handle : SelectedEntities) + { + FRTSUnitData Data = CreateUnitDataFromEntity(Handle); + FRTSUnitData& Group = GroupMap.FindOrAdd(Data.Name); + if (Group.Count == 0 || Group.Name.IsEmpty()) + { + Group = Data; + Group.Count = 0; + } + Group.Count++; + } + + for (auto& Pair : GroupMap) + { + View.Items.Add(Pair.Value); + } + } + + // --- Tab Cycling Logic --- + AvailableGroupKeys.Reset(); + for(const auto& Item : View.Items) + { + AvailableGroupKeys.AddUnique(Item.Name); + } + AvailableGroupKeys.Sort(); + + // Validate CurrentGroupIndex + if (CurrentGroupIndex >= AvailableGroupKeys.Num()) + { + CurrentGroupIndex = 0; + } + + if (AvailableGroupKeys.IsValidIndex(CurrentGroupIndex)) + { + View.ActiveGroupKey = AvailableGroupKeys[CurrentGroupIndex]; + } + + // Broadcast + OnSelectionChanged.Broadcast(View); +} + +void URTSSelectionSubsystem::ClearSelection() +{ + SetSelectedUnits(TArray(), TArray()); +} + +// --- New Starcraft-style Controls --- + +void URTSSelectionSubsystem::CycleGroup() +{ + if (AvailableGroupKeys.Num() <= 1) return; + + CurrentGroupIndex++; + if (CurrentGroupIndex >= AvailableGroupKeys.Num()) + { + CurrentGroupIndex = 0; + } + + // Force Re-Generate View (Copying logic from SetSelectedUnits Step 2 for now) + // ideally this should be a helper function + + // Re-run SetSelectedUnits with "Replace" effectively regenerates view, but it's expensive to copy arrays. + // But since we can't easily add private methods to header without re-viewing, + // we will trigger a self-refresh using current selection. + SetSelectedUnits(SelectedActors, SelectedEntities, ERTSSelectionModifier::Replace); +} + +void URTSSelectionSubsystem::RemoveUnit(const FRTSUnitData& UnitData) +{ + // 1. Identify what to remove + TArray ActorsToRemove; + TArray EntitiesToRemove; + + // Check if this is a "Group" removal (Summary Mode) or "Single" removal + // In our data, if Element is from Summary, it might represent multiple. + // But FRTSUnitData passed from UI is just a copy. + // We check if we are in Summary Mode? + // Or simplistic approach: If UnitData has valid pointers, remove specific. + // If UnitData is a Summary representative (pointers might be null or first-of-group), + // we might need to remove by Name. + + bool bIsGroupRemoval = false; + // Check ID match in SelectedActors. + + if (UnitData.ActorPtr) + { + ActorsToRemove.Add(UnitData.ActorPtr); + } + else if (UnitData.EntityHandle.Index > 0) + { + EntitiesToRemove.Add(UnitData.EntityHandle); + } + else + { + // Fallback: Remove by Name (Type) if pointers missing (unlikely in List, possible in Summary?) + // Actually, let's enable "Remove Type" if user held Shift on a Summary Icon. + // For now, assume List Mode (Starcraft Wireframe). + // If Summary Mode, we should probably iterate all Selected and remove matching Name. + bIsGroupRemoval = true; + } + + if (bIsGroupRemoval) + { + // Iterate and collect all matching Name + for (AActor* Act : SelectedActors) + { + if (Act && Act->GetName() == UnitData.Name) ActorsToRemove.Add(Act); // Name match might need refinement + } + // Entities... + } + + SetSelectedUnits(ActorsToRemove, EntitiesToRemove, ERTSSelectionModifier::Remove); +} + +void URTSSelectionSubsystem::SelectGroup(const FString& GroupKey) +{ + // Filter Current Selection to ONLY keep this GroupKey + TArray NewActors; + TArray NewEntities; + + // Filter Actors + for (AActor* Act : SelectedActors) + { + if (Act) + { + // We need to match the key logic used in CreateUnitData + FRTSUnitData Data = CreateUnitDataFromActor(Act); + if (Data.Name == GroupKey) + { + NewActors.Add(Act); + } + } + } + + // Filter Entities + for (const FEntityHandle& Handle : SelectedEntities) + { + FRTSUnitData Data = CreateUnitDataFromEntity(Handle); + if (Data.Name == GroupKey) + { + NewEntities.Add(Handle); + } + } + + // Apply as Replace + SetSelectedUnits(NewActors, NewEntities, ERTSSelectionModifier::Replace); +} + +FRTSUnitData URTSSelectionSubsystem::CreateUnitDataFromActor(AActor* Actor) +{ + FRTSUnitData Data; + if (Actor) + { + // Group by Class Name (Type) instead of Instance Name (Label) + Data.Name = Actor->GetClass()->GetDisplayNameText().ToString(); + // Or GetName() + Remove _C? DisplayName is usually cleaner for UI. + + Data.ActorPtr = Actor; + Data.bIsMassEntity = false; + + // Retrieve Data from Component + if (auto Selectable = Actor->FindComponentByClass()) + { + Data.Icon = Selectable->Icon; + Data.Health = Selectable->Health; + Data.MaxHealth = Selectable->MaxHealth; + Data.Energy = Selectable->Energy; + Data.MaxEnergy = Selectable->MaxEnergy; + Data.Shield = Selectable->Shield; + Data.MaxShield = Selectable->MaxShield; + } + } + return Data; +} + +FRTSUnitData URTSSelectionSubsystem::CreateUnitDataFromEntity(const FEntityHandle& Handle) +{ + FRTSUnitData Data; + Data.bIsMassEntity = true; + Data.EntityHandle = Handle; + + // Default fallback + Data.Name = TEXT("Mass Unit"); + + // TODO: Use MassEntityManager to get fragments (Icon, Health, etc.) + // UMassEntitySubsystem* MassSubsystem = GetWorld()->GetSubsystem(); + // if (MassSubsystem) { FMassEntityManager& EntityManager = MassSubsystem->GetMutableEntityManager(); ... } + + return Data; +} diff --git a/Source/OpenRTSCamera/Private/UI/RTSActiveGroupWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSActiveGroupWidget.cpp new file mode 100644 index 0000000..fe66252 --- /dev/null +++ b/Source/OpenRTSCamera/Private/UI/RTSActiveGroupWidget.cpp @@ -0,0 +1,66 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#include "UI/RTSActiveGroupWidget.h" +#include "UI/RTSUnitIconWidget.h" +#include "RTSSelectionSubsystem.h" + +void URTSActiveGroupWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (APlayerController* PC = GetOwningPlayer()) + { + if (ULocalPlayer* LP = PC->GetLocalPlayer()) + { + if (URTSSelectionSubsystem* Subsystem = LP->GetSubsystem()) + { + Subsystem->OnSelectionChanged.AddDynamic(this, &URTSActiveGroupWidget::OnSelectionUpdated); + } + } + } +} + +void URTSActiveGroupWidget::OnSelectionUpdated(const FRTSSelectionView& View) +{ + const FRTSUnitData* ActiveData = nullptr; + FString ActiveKey = View.ActiveGroupKey; + + if (!ActiveKey.IsEmpty()) + { + ActiveData = View.Items.FindByPredicate([&](const FRTSUnitData& Item) { + return Item.Name == ActiveKey; + }); + } + + // Fallback: If no ActiveKey but items exist (e.g. Single Mode), use first item + if (!ActiveData && View.Items.Num() > 0) + { + ActiveData = &View.Items[0]; + } + + if (ActiveData) + { + // We have an active group/unit. + // If we wrap an internal icon widget, update it. + if (GroupIcon) + { + // Show Icon, Show Bars + GroupIcon->InitData(*ActiveData, true, true); + GroupIcon->SetIsActive(true); + } + + // Ensure self is visible (hit test invisible to allow tooltips on children) + SetVisibility(ESlateVisibility::SelfHitTestInvisible); + + // Notify BP + OnActiveGroupChanged(*ActiveData, true); + } + else + { + // No selection at all. + SetVisibility(ESlateVisibility::Hidden); + + // Notify BP (Empty Data) + OnActiveGroupChanged(FRTSUnitData(), false); + } +} diff --git a/Source/OpenRTSCamera/Private/UI/RTSSelectionWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSSelectionWidget.cpp new file mode 100644 index 0000000..409bf9f --- /dev/null +++ b/Source/OpenRTSCamera/Private/UI/RTSSelectionWidget.cpp @@ -0,0 +1,296 @@ +#include "UI/RTSSelectionWidget.h" +#include "UI/RTSUnitIconWidget.h" +#include "RTSSelectionSubsystem.h" +#include "Components/PanelWidget.h" +#include "Components/TextBlock.h" +#include "Components/ProgressBar.h" +#include "Components/UniformGridPanel.h" +#include "Components/UniformGridSlot.h" +#include "Components/GridPanel.h" +#include "Components/GridSlot.h" +#include "Components/WrapBox.h" +#include "Kismet/GameplayStatics.h" + +void URTSSelectionWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + // Template Extraction Logic + if (IconContainer) + { + int32 FoundMaxRow = 0; + int32 FoundMaxCol = 0; + bool bFoundGridSlot = false; + bool bUsedPanelSettings = false; + + // Strategy 1: Check GridPanel explicit configurations (RowFill / ColumnFill) + // This is the "Elegant" way if the user configured the Grid structure in Designer. + if (UGridPanel* GridPanel = Cast(IconContainer)) + { + if (GridPanel->RowFill.Num() > 0) + { + FoundMaxRow = GridPanel->RowFill.Num() - 1; + bFoundGridSlot = true; + bUsedPanelSettings = true; + } + if (GridPanel->ColumnFill.Num() > 0) + { + FoundMaxCol = GridPanel->ColumnFill.Num() - 1; + bFoundGridSlot = true; + bUsedPanelSettings = true; + } + } + + // Strategy 2: Scan Children for Occupancy (Fallback or for UniformGridPanel) + // If explicit settings were not found, we scan where the user placed the templates. + if (!bUsedPanelSettings) + { + const int32 ChildrenCount = IconContainer->GetChildrenCount(); + for (int32 i = 0; i < ChildrenCount; ++i) + { + UWidget* Child = IconContainer->GetChildAt(i); + if (!Child) continue; + + // --- 1. Class Detection --- + if (!IconWidgetClass) + { + if (URTSUnitIconWidget* IconWidget = Cast(Child)) + { + IconWidgetClass = IconWidget->GetClass(); + } + } + if (!CountWidgetClass) + { + if (UTextBlock* TextBlock = Cast(Child)) + { + CountWidgetClass = TextBlock->GetClass(); + } + } + + // --- 2. Grid Size Detection --- + if (UUniformGridSlot* USlot = Cast(Child->Slot)) + { + FoundMaxRow = FMath::Max(FoundMaxRow, USlot->GetRow()); + FoundMaxCol = FMath::Max(FoundMaxCol, USlot->GetColumn()); + bFoundGridSlot = true; + } + else if (UGridSlot* GSlot = Cast(Child->Slot)) + { + FoundMaxRow = FMath::Max(FoundMaxRow, GSlot->GetRow()); + FoundMaxCol = FMath::Max(FoundMaxCol, GSlot->GetColumn()); + bFoundGridSlot = true; + } + } + } + else + { + // If we used panel settings, we still need to scan for Class Detection! + const int32 ChildrenCount = IconContainer->GetChildrenCount(); + for (int32 i = 0; i < ChildrenCount; ++i) + { + UWidget* Child = IconContainer->GetChildAt(i); + if (!Child) continue; + + if (!IconWidgetClass) + { + if (URTSUnitIconWidget* IconWidget = Cast(Child)) + { + IconWidgetClass = IconWidget->GetClass(); + break; + } + } + } + } + + // Apply Detected Size + if (bFoundGridSlot) + { + MaxRows = FoundMaxRow + 1; // 0-based index to Count + MaxColumns = FoundMaxCol + 1; + UE_LOG(LogTemp, Log, TEXT("RTSSelectionWidget: Detected Grid Logic (%s): %d Rows x %d Cols"), + bUsedPanelSettings ? TEXT("Explicit Panel Config") : TEXT("Child Placement"), + MaxRows, MaxColumns); + } + + // Clear templates from view so we can populate fresh data + IconContainer->ClearChildren(); + } + + if (IconWidgetClass) + { + UE_LOG(LogTemp, Log, TEXT("RTSSelectionWidget: IconWidgetClass resolved to %s"), *IconWidgetClass->GetName()); + + // --- Initialize Widget Pool --- + // Calculate Total Slots based on Grid Dimensions + ItemsPerPage = MaxRows * MaxColumns; + UE_LOG(LogTemp, Log, TEXT("RTSSelectionWidget: Initializing Pool for %d x %d = %d slots."), MaxRows, MaxColumns, ItemsPerPage); + + IconSlots.Reset(); + CountSlots.Reset(); + + // Support for various container types + UUniformGridPanel* UniformGrid = Cast(IconContainer); + UGridPanel* GenericGrid = Cast(IconContainer); + UWrapBox* WrapBox = Cast(IconContainer); + + // Helper for layout + int32 CurrentCol = 0; + int32 CurrentRow = 0; + auto AdvanceCursor = [&]() { + CurrentCol++; + if (CurrentCol >= MaxColumns) + { + CurrentCol = 0; + CurrentRow++; + } + }; + + // Create Fixed Pool (Icon Only Mode for now - Summary Count complicates fixed slots) + // For simplicity, we create ItemsPerPage ICONS. + // If Summary mode needs CountText, we might dynamic spawn those or dual-pool. + // Given user requirement "Fixed Array", let's spawn the Max capacity. + + for (int32 i = 0; i < ItemsPerPage; i++) + { + if (IconWidgetClass->IsChildOf(UUserWidget::StaticClass())) + { + URTSUnitIconWidget* NewWidget = CreateWidget(this, IconWidgetClass); + if (NewWidget) + { + // Add to Container + if (UniformGrid) + { + UUniformGridSlot* NewSlot = UniformGrid->AddChildToUniformGrid(NewWidget, CurrentRow, CurrentCol); + if (NewSlot) { NewSlot->SetHorizontalAlignment(HAlign_Fill); NewSlot->SetVerticalAlignment(VAlign_Fill); } + AdvanceCursor(); + } + else if (GenericGrid) + { + UGridSlot* NewSlot = GenericGrid->AddChildToGrid(NewWidget, CurrentRow, CurrentCol); + if (NewSlot) { NewSlot->SetHorizontalAlignment(HAlign_Fill); NewSlot->SetVerticalAlignment(VAlign_Fill); } + AdvanceCursor(); + } + else if (WrapBox) + { + WrapBox->AddChildToWrapBox(NewWidget); + } + else + { + IconContainer->AddChild(NewWidget); + } + + // Store in Pool and Hide + NewWidget->SetVisibility(ESlateVisibility::Hidden); // Hidden preserves layout in Grid? No, Collapsed does? + // Actually, for UniformGrid, we want it to take space? + // If Hidden, specific implementations might vary, but usually Hidden = Takes Space, Invisible. + // Collapsed = No Space. + // User wants "Fixed Array", implies fixed layout structure. Hidden is correct. + + IconSlots.Add(NewWidget); + } + } + } + UE_LOG(LogTemp, Log, TEXT("RTSSelectionWidget: Initialized Pool with %d widgets."), IconSlots.Num()); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("RTSSelectionWidget: IconWidgetClass is NULL! Grid will be empty. Set it in Details or add a template child.")); + } + + if (APlayerController* PC = GetOwningPlayer()) + { + if (ULocalPlayer* LP = PC->GetLocalPlayer()) + { + if (URTSSelectionSubsystem* Subsystem = LP->GetSubsystem()) + { + Subsystem->OnSelectionChanged.AddDynamic(this, &URTSSelectionWidget::OnSelectionUpdated); + } + } + } +} + +void URTSSelectionWidget::NativeDestruct() +{ +// ... +} + +// ... + + +void URTSSelectionWidget::OnSelectionUpdated(const FRTSSelectionView& View) +{ + RefreshGrid(View); +} + +void URTSSelectionWidget::RefreshGrid(const FRTSSelectionView& View) +{ + const TArray& AllItems = View.Items; + ERTSSelectionMode Mode = View.Mode; + FString ActiveKey = View.ActiveGroupKey; + + UE_LOG(LogTemp, Log, TEXT("RTSSelectionWidget::RefreshGrid - Mode: %d, Items: %d, ActiveKey: %s"), (int32)Mode, AllItems.Num(), *ActiveKey); + + // --- 1. Handle Panel Visibility (Single vs Grid) --- + bool bShowDetail = (Mode == ERTSSelectionMode::Single && SingleUnitDetail != nullptr); + + if (bShowDetail) + { + // Show Detail, Hide Grid + if (SingleUnitDetail) SingleUnitDetail->SetVisibility(ESlateVisibility::SelfHitTestInvisible); + if (IconContainer) IconContainer->SetVisibility(ESlateVisibility::Collapsed); + + // Grid population is skipped because we are showing the detail view + return; + } + else + { + // Show Grid, Hide Detail (or if Detail is null, we cleanup anyway) + if (SingleUnitDetail) SingleUnitDetail->SetVisibility(ESlateVisibility::Collapsed); + if (IconContainer) IconContainer->SetVisibility(ESlateVisibility::Visible); + } + + // --- 3. Update Grid from Pool --- + if (!IconContainer || IconSlots.Num() == 0) + { + // UE_LOG(LogTemp, Warning, TEXT("RTSSelectionWidget: internal pool empty or container missing.")); + return; + } + + int32 StartIndex = 0; + // We can only show as many items as we have slots + int32 MaxVisible = FMath::Min(AllItems.Num(), IconSlots.Num()); + + for (int32 i = 0; i < IconSlots.Num(); i++) + { + URTSUnitIconWidget* SlotWidget = IconSlots[i]; + if (!SlotWidget) continue; + + int32 DataIndex = StartIndex + i; // Simple linear mapping + + if (DataIndex < AllItems.Num()) + { + // Valid Item + const FRTSUnitData& Data = AllItems[DataIndex]; + + // Update Data + // Config: Show Icon, Show Bars + SlotWidget->InitData(Data, true, true); + + // Highlight Logic + bool bIsActive = ActiveKey.IsEmpty() || (Data.Name == ActiveKey); + SlotWidget->SetIsActive(bIsActive); + + // Visible + SlotWidget->SetVisibility(ESlateVisibility::Visible); // or SelfHitTestInvisible + } + else + { + // Empty Slot + SlotWidget->SetVisibility(ESlateVisibility::Hidden); // Hidden = Layout Reserved. Collapsed = Gone. + } + } + + // Note: Summary Mode separate count text is temporarily disabled in Fixed Pool mode. + // If you need counts, consider re-adding CountText to RTSUnitIconWidget or using an Overlay. + +} diff --git a/Source/OpenRTSCamera/Private/UI/RTSUnitIconWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSUnitIconWidget.cpp new file mode 100644 index 0000000..475098c --- /dev/null +++ b/Source/OpenRTSCamera/Private/UI/RTSUnitIconWidget.cpp @@ -0,0 +1,152 @@ +#include "UI/RTSUnitIconWidget.h" +#include "Components/Image.h" +#include "Components/ProgressBar.h" + +void URTSUnitIconWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (!UnitIcon) + { + UE_LOG(LogTemp, Warning, TEXT("RTSUnitIconWidget: 'UnitIcon' (Image) is NOT bound! Check your WBP naming. Expecting variable named 'UnitIcon'.")); + } + else + { + UE_LOG(LogTemp, Log, TEXT("RTSUnitIconWidget: NativeConstruct - UnitIcon is bound.")); + } +} + +void URTSUnitIconWidget::InitData(const FRTSUnitData& Data, bool bShowIcon, bool bShowBars) +{ + // Set Icon + if (UnitIcon) + { + if (!bShowIcon) + { + UnitIcon->SetVisibility(ESlateVisibility::Collapsed); + } + else + { + UnitIcon->SetVisibility(ESlateVisibility::Visible); + + if (Data.Icon) + { + UnitIcon->SetBrushFromTexture(Data.Icon); + // Reset color to white (in case it was tinted differently) + UnitIcon->SetColorAndOpacity(FLinearColor::White); + } + else + { + // No specific icon data? Show default (White square as user expects, or BP default) + // We don't change the brush, so it keeps the Designer's default. + // Optionally set a debug color? + UE_LOG(LogTemp, Warning, TEXT("RTSUnitIconWidget: Data.Icon is null for %s. Showing default placeholder."), *Data.Name); + } + } + } + + // Update Status Bars + if (bShowBars) + { + UpdateBar(HealthBar, Data.Health, Data.MaxHealth); + UpdateBar(EnergyBar, Data.Energy, Data.MaxEnergy); + UpdateBar(ShieldBar, Data.Shield, Data.MaxShield); + } + else + { + if(HealthBar) HealthBar->SetVisibility(ESlateVisibility::Collapsed); + if(EnergyBar) EnergyBar->SetVisibility(ESlateVisibility::Collapsed); + if(ShieldBar) ShieldBar->SetVisibility(ESlateVisibility::Collapsed); + } + + // Store for Interaction + StoredData = Data; + + // Tooltip + FString Tooltip = Data.Name; + if (Data.MaxHealth > 0) Tooltip += FString::Printf(TEXT("\nHP: %.0f/%.0f"), Data.Health, Data.MaxHealth); + if (Data.MaxEnergy > 0) Tooltip += FString::Printf(TEXT("\nMP: %.0f/%.0f"), Data.Energy, Data.MaxEnergy); + if (Data.MaxShield > 0) Tooltip += FString::Printf(TEXT("\nSP: %.0f/%.0f"), Data.Shield, Data.MaxShield); + SetToolTipText(FText::FromString(Tooltip)); +} + +void URTSUnitIconWidget::SetIsActive(bool bActive) +{ + // Visual feedback for Active vs Inactive group + // Starcraft style: Inactive groups are dimmed. + SetRenderOpacity(bActive ? 1.0f : 0.3f); +} + +void URTSUnitIconWidget::UpdateBar(UProgressBar* Bar, float Current, float Max) +{ + if (!Bar) return; + + if (Max > 0.0f) + { + Bar->SetPercent(FMath::Clamp(Current / Max, 0.0f, 1.0f)); + Bar->SetVisibility(ESlateVisibility::Visible); + } + else + { + Bar->SetVisibility(ESlateVisibility::Collapsed); + } +} + +FReply URTSUnitIconWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + // Check for Left Click + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) + { + if (APlayerController* PC = GetOwningPlayer()) + { + if (ULocalPlayer* LP = PC->GetLocalPlayer()) + { + if (URTSSelectionSubsystem* Subsystem = LP->GetSubsystem()) + { + // --- Starcraft Logic --- + + // Shift + Click = Remove (Exclude) + if (InMouseEvent.IsShiftDown()) + { + Subsystem->RemoveUnit(StoredData); + return FReply::Handled(); + } + + // Ctrl + Click = Select Type (Keep only this group) + if (InMouseEvent.IsControlDown()) + { + Subsystem->SelectGroup(StoredData.Name); + return FReply::Handled(); + } + + // Normal Click = Select This Unit (Exclusive) + // We need to construct a single selection. + TArray NewActors; + TArray NewEntities; + + if (StoredData.ActorPtr) NewActors.Add(StoredData.ActorPtr); + if (StoredData.EntityHandle.Index > 0) NewEntities.Add(StoredData.EntityHandle); + + // If Summary Item (Count > 1), normal click usually Selects the GROUP? + // In SC2: + // - Wireframe (List): Click selects unit. + // - Summary: Click selects ALL of that type (same as Ctrl+Click in Wireframe). + // If Count > 1, treating as Ctrl+Click (Group Select). + + if (StoredData.Count > 1) + { + Subsystem->SelectGroup(StoredData.Name); + } + else + { + Subsystem->SetSelectedUnits(NewActors, NewEntities, ERTSSelectionModifier::Replace); + } + + return FReply::Handled(); + } + } + } + } + + return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent); +} diff --git a/Source/OpenRTSCamera/Public/RTSHUD.h b/Source/OpenRTSCamera/Public/RTSHUD.h index 95ef004..8d97fde 100644 --- a/Source/OpenRTSCamera/Public/RTSHUD.h +++ b/Source/OpenRTSCamera/Public/RTSHUD.h @@ -19,6 +19,12 @@ class OPENRTSCAMERA_API ARTSHUD : public AHUD UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Selection Box") float SelectionBoxThickness; + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Selection Box") + FLinearColor SelectionBoxFillColor; + + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Selection Box") + float MinSelectionSizeSq; + UFUNCTION(BlueprintCallable, Category = "Selection Box") void BeginSelection(const FVector2D& StartPoint); @@ -38,7 +44,7 @@ class OPENRTSCAMERA_API ARTSHUD : public AHUD virtual void DrawHUD() override; private: - void PerformMassSelection(); + void PerformMassSelection(TArray& OutEntities); bool bIsDrawingSelectionBox; bool bIsPerformingSelection; diff --git a/Source/OpenRTSCamera/Public/RTSSelectable.h b/Source/OpenRTSCamera/Public/RTSSelectable.h index dc7420b..94a4f25 100644 --- a/Source/OpenRTSCamera/Public/RTSSelectable.h +++ b/Source/OpenRTSCamera/Public/RTSSelectable.h @@ -11,4 +11,24 @@ class OPENRTSCAMERA_API URTSSelectable : public UActorComponent UFUNCTION(BlueprintCallable, BlueprintImplementableEvent, Category = "RTS Selection") void OnDeselected(); + + // --- Visual Data --- + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Data") + UTexture2D* Icon; + + // --- Status Data (Standard RTS) --- + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Data") + float Health = 100.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Data") + float MaxHealth = 100.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Data") + float Energy = 0.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Data") + float MaxEnergy = 0.0f; + + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Data") + float Shield = 0.0f; + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Data") + float MaxShield = 0.0f; }; diff --git a/Source/OpenRTSCamera/Public/RTSSelectionStructs.h b/Source/OpenRTSCamera/Public/RTSSelectionStructs.h new file mode 100644 index 0000000..694cd58 --- /dev/null +++ b/Source/OpenRTSCamera/Public/RTSSelectionStructs.h @@ -0,0 +1,99 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "MassEntityTypes.h" +#include "MassAPIStructs.h" +#include "RTSSelectionStructs.generated.h" + +UENUM(BlueprintType) +enum class ERTSSelectionMode : uint8 +{ + Single UMETA(DisplayName = "Single Unit"), + List UMETA(DisplayName = "Unit List"), // < 12 units + Summary UMETA(DisplayName = "Group Summary") // > 12 units +}; + +UENUM(BlueprintType) +enum class ERTSSelectionModifier : uint8 +{ + Replace UMETA(DisplayName = "Replace Selection"), + Add UMETA(DisplayName = "Add to Selection"), + Remove UMETA(DisplayName = "Remove from Selection") +}; + +/** + * Unified data structure representing a single selectable unit OR a group summary. + */ +USTRUCT(BlueprintType) +struct FRTSUnitData +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + FString Name; + + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + UTexture2D* Icon = nullptr; + + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + int32 Count = 1; // 1 for individual unit, >1 for group summary + + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + float Health = 0.0f; + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + float MaxHealth = 0.0f; + + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + float Energy = 0.0f; + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + float MaxEnergy = 0.0f; + + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + float Shield = 0.0f; + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + float MaxShield = 0.0f; + + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + bool bIsMassEntity = false; + + // Optional: Raw pointers/handles if UI needs to command them back + // Only valid if Count == 1 + UPROPERTY() + AActor* ActorPtr = nullptr; + + UPROPERTY() + FEntityHandle EntityHandle; + + // Default constructor for "Empty/Unknown" state + FRTSUnitData() + { + Name = TEXT("Unknown"); + } +}; + +/** + * The snapshot sent to the UI. + */ +USTRUCT(BlueprintType) +struct FRTSSelectionView +{ + GENERATED_BODY() + + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + ERTSSelectionMode Mode = ERTSSelectionMode::Single; + + // Used when Mode == Single. Contains detailed info. + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + FRTSUnitData SingleUnit; + + // Used when Mode == List (Individual items, Count=1) OR Summary (Grouped items, Count>1) + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + TArray Items; + + // The key of the currently active sub-group (e.g. "Marine") + // Used for highlighting and tab-cycling + UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") + FString ActiveGroupKey; +}; diff --git a/Source/OpenRTSCamera/Public/RTSSelectionSubsystem.h b/Source/OpenRTSCamera/Public/RTSSelectionSubsystem.h new file mode 100644 index 0000000..c221891 --- /dev/null +++ b/Source/OpenRTSCamera/Public/RTSSelectionSubsystem.h @@ -0,0 +1,98 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Subsystems/LocalPlayerSubsystem.h" +#include "RTSSelectionStructs.h" +#include "MassEntityTypes.h" +#include "MassAPIStructs.h" +#include "RTSSelectionSubsystem.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSelectionChanged, const FRTSSelectionView&, SelectionView); + +/** + * Manages RTS selection state and formats data for the UI. + */ +UCLASS() +class OPENRTSCAMERA_API URTSSelectionSubsystem : public ULocalPlayerSubsystem +{ + GENERATED_BODY() + +public: + // Subsystem interface + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override; + + /** + * Main entry point for selection updates. + * Calculates the view mode (Single/List/Summary) and broadcasts OnSelectionChanged. + */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void SetSelectedUnits(const TArray& InActors, const TArray& InEntities, ERTSSelectionModifier Modifier = ERTSSelectionModifier::Replace); + + /** + * Clears current selection. + */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void ClearSelection(); + + /** + * Cycles focus to the next available sub-group. + * (Tab functionality) + */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void CycleGroup(); + + /** + * Removes a specific unit/group from the current selection. + * (Shift-Click UI functionality) + */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void RemoveUnit(const FRTSUnitData& UnitData); + + /** + * Restricts selection to ONLY units of the specified group key. + * (Ctrl-Click UI functionality) + */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void SelectGroup(const FString& GroupKey); + + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + bool HasSelectedActors() const { return SelectedActors.Num() > 0; } + + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + bool HasSelectedMass() const { return SelectedEntities.Num() > 0; } + + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + bool IsActorSelected(const AActor* Actor) const { return SelectedActors.Contains(Actor); } + + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + bool IsEntitySelected(const FEntityHandle& Handle) const { return SelectedEntities.Contains(Handle); } + + /** + * Event fired when selection changes. UI should bind to this. + */ + UPROPERTY(BlueprintAssignable, Category = "RTS Selection") + FOnSelectionChanged OnSelectionChanged; + +private: + // Raw State + UPROPERTY() + TArray SelectedActors; + + TArray SelectedEntities; + + // Cycle State + UPROPERTY() + TArray AvailableGroupKeys; + + int32 CurrentGroupIndex = 0; + + // Helpers + FRTSUnitData CreateUnitDataFromActor(AActor* Actor); + FRTSUnitData CreateUnitDataFromEntity(const FEntityHandle& Handle); + + // Thresholds + const int32 ListModeMaxCount = 12; +}; diff --git a/Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h b/Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h new file mode 100644 index 0000000..7c146f1 --- /dev/null +++ b/Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h @@ -0,0 +1,45 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "RTSSelectionStructs.h" +#include "RTSSelectionSubsystem.h" +#include "RTSActiveGroupWidget.generated.h" + +class URTSUnitIconWidget; + +/** + * A standalone widget that displays the currently active sub-group (Leader/Avatar). + * Listen to RTSSelectionSubsystem directly. + */ +UCLASS(BlueprintType, Blueprintable) +class OPENRTSCAMERA_API URTSActiveGroupWidget : public UUserWidget +{ + GENERATED_BODY() + +protected: + virtual void NativeConstruct() override; + + UFUNCTION() + void OnSelectionUpdated(const FRTSSelectionView& View); + + // Optional: If bound, we forward the data to this internal widget. + // This allows users to wrap our logic in a text/border/etc. + // Or users can just inherit this class in their WBP_Avatar + UPROPERTY(meta = (BindWidgetOptional)) + URTSUnitIconWidget* GroupIcon; + + // Optional: A text block for the name? (Or let GroupIcon handle it?) + // Let's keep it simple: It mostly wraps functionality. + + /** + * Event fired when the active group data changes. + * Implement this in Blueprint to add custom logic (e.g. update 3D Avatar, play sound). + * @param Data The data of the active unit/group. + * @param bHasData True if there is a valid selection, False if empty. + */ + UFUNCTION(BlueprintImplementableEvent, Category = "RTS Selection") + void OnActiveGroupChanged(const FRTSUnitData& Data, bool bHasData); +}; diff --git a/Source/OpenRTSCamera/Public/UI/RTSSelectionWidget.h b/Source/OpenRTSCamera/Public/UI/RTSSelectionWidget.h new file mode 100644 index 0000000..c7dd237 --- /dev/null +++ b/Source/OpenRTSCamera/Public/UI/RTSSelectionWidget.h @@ -0,0 +1,93 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "RTSSelectionStructs.h" +#include "RTSSelectionWidget.generated.h" + +class UPanelWidget; +class UTextBlock; +class URTSUnitIconWidget; +class UProgressBar; + +/** + * Main Selection Panel. Handles Single, List, and Summary views. + */ +UCLASS() +class OPENRTSCAMERA_API URTSSelectionWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + virtual void NativeConstruct() override; + virtual void NativeDestruct() override; + +protected: + UFUNCTION() + void OnSelectionUpdated(const FRTSSelectionView& View); + + /** + * Class of the item widget to spawn in the list. + * Must be set in Blueprint (WBP_RTSUnitIcon). + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Selection") + TSubclassOf UnitIconClass; + + // The class to use for each unit icon. + // If set in Editor, we use this. If nullptr, we try to detect from the first child in Designer. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Selection") + TSubclassOf IconWidgetClass; + + // Optional: The class for the "Count" widget in Summary mode. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Selection") + TSubclassOf CountWidgetClass; + + // -- Bind Widgets -- + + /** + * Max items to show in the grid. + * Calculated automatically as MaxRows * MaxColumns. + */ + UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "RTS Selection") + int32 ItemsPerPage = 12; + + /** + * Max columns for the grid. Defaults to 6. + * If a GridPanel template is detected, this will be overwritten by the max column index found. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Selection") + int32 MaxColumns = 6; + + /** + * Max rows for the grid. Defaults to 2. + * If a GridPanel template is detected, this will be overwritten by the max row index found. + */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "RTS Selection") + int32 MaxRows = 2; + + // Container for the icons (e.g., WrapBox or GridPanel) + // User should place template widgets inside this in the Editor. + // Child 0: Unit Icon Template + // Child 1 (Optional): Count Text Template + UPROPERTY(meta = (BindWidget)) + UPanelWidget* IconContainer; + + // Optional: Container to show when only 1 unit is selected. + // If bound, this will be Visible in Single Mode, and Collapsed in List/Summary Mode. + // The IconContainer will be the inverse. + UPROPERTY(meta = (BindWidgetOptional)) + UPanelWidget* SingleUnitDetail; + +private: + // Pool of re-usable icon widgets + UPROPERTY() + TArray IconSlots; + + // Pool of re-usable count widgets (for Summary mode) + UPROPERTY() + TArray CountSlots; + + void RefreshGrid(const FRTSSelectionView& View); +}; diff --git a/Source/OpenRTSCamera/Public/UI/RTSUnitIconWidget.h b/Source/OpenRTSCamera/Public/UI/RTSUnitIconWidget.h new file mode 100644 index 0000000..7a792ca --- /dev/null +++ b/Source/OpenRTSCamera/Public/UI/RTSUnitIconWidget.h @@ -0,0 +1,68 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "RTSSelectionStructs.h" +#include "RTSUnitIconWidget.generated.h" + +class UImage; +class UTextBlock; + +/** + * Represents a single unit icon or group summary icon in the selection panel. + */ +UCLASS() +class OPENRTSCAMERA_API URTSUnitIconWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + /** + * Updates the widget with data. + * @param bShowIcon If true, forces icon visibility (if valid). If false, hides icon. + * @param bShowBars If true, shows status bars. + */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void InitData(const FRTSUnitData& Data, bool bShowIcon = true, bool bShowBars = true); + + /** + * Sets the visual active state (e.g. for Tab toggling). + * Active: Default appearance. + * Inactive: Dimmed opacity. + */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void SetIsActive(bool bActive); + +protected: + virtual void NativeConstruct() override; + + // Input Handling + virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + + // UI Bindings + UPROPERTY(meta = (BindWidgetOptional)) + class UImage* UnitIcon; + + // -- Status Bars (Optional) -- + + // -- Status Bars (Optional) -- + // If the unit has valid MaxHealth/Energy/Shield, these bars will update. + // Otherwise they will be hidden. + + UPROPERTY(meta = (BindWidgetOptional)) + class UProgressBar* HealthBar; + + UPROPERTY(meta = (BindWidgetOptional)) + class UProgressBar* EnergyBar; + + UPROPERTY(meta = (BindWidgetOptional)) + class UProgressBar* ShieldBar; + +private: + // Internal Copy of Data for Interaction + FRTSUnitData StoredData; + + void UpdateBar(class UProgressBar* Bar, float Current, float Max); +}; From d03c04c32a2af2eedb09d4cd09132ef85076f80e Mon Sep 17 00:00:00 2001 From: winyunq Date: Mon, 16 Feb 2026 15:03:01 +0800 Subject: [PATCH 09/12] =?UTF-8?q?-=20=E5=AE=9E=E7=8E=B0=20Starcraft=20?= =?UTF-8?q?=E9=A3=8E=E6=A0=BC=E7=9A=84=E9=80=89=E6=8B=A9=E4=BA=A4=E4=BA=92?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=9A=20=20=20-=20=E5=B7=A6=E9=94=AE?= =?UTF-8?q?=E7=82=B9=E5=87=BB=EF=BC=9A=E5=8D=95=E9=80=89=20(Exclusive=20Se?= =?UTF-8?q?lect)=E3=80=82=20=20=20-=20=E5=B7=A6=E9=94=AE=E6=A1=86=E9=80=89?= =?UTF-8?q?=EF=BC=9A=E5=A4=9A=E9=80=89=20(Box=20Select)=E3=80=82=20=20=20-?= =?UTF-8?q?=20Shift=20+=20=E7=82=B9=E5=87=BB=EF=BC=9A=E5=8F=8D=E9=80=89/?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=20(Toggle/Remove)=E3=80=82=20=20=20-=20Shift?= =?UTF-8?q?=20+=20=E6=A1=86=E9=80=89=EF=BC=9A=E8=BF=BD=E5=8A=A0=E9=80=89?= =?UTF-8?q?=E6=8B=A9=20(Append)=EF=BC=8C=E4=B8=94=E5=85=B7=E5=A4=87?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E4=B8=80=E8=87=B4=E6=80=A7=E4=BF=9D=E6=8A=A4?= =?UTF-8?q?=20(Type=20Consistency)=E3=80=82=20=20=20-=20Ctrl=20+=20?= =?UTF-8?q?=E7=82=B9=E5=87=BB=EF=BC=9A=E5=85=A8=E5=B1=8F=E5=90=8C=E7=B1=BB?= =?UTF-8?q?=E9=80=89=E6=8B=A9=20(Select=20All=20On=20Screen)=E3=80=82=20?= =?UTF-8?q?=20=20-=20Tab=EF=BC=9A=E5=9C=A8=E9=80=89=E4=B8=AD=E5=8D=95?= =?UTF-8?q?=E4=BD=8D=E7=9A=84=E5=AD=90=E7=BB=84=E9=97=B4=E5=BE=AA=E7=8E=AF?= =?UTF-8?q?=E5=88=87=E6=8D=A2=20(Cycle=20Subgroup)=E3=80=82=20-=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=A1=86=E9=80=89=E5=88=A4=E5=AE=9A=E9=98=88?= =?UTF-8?q?=E5=80=BC=EF=BC=9A=20=20=20-=20=E5=BC=95=E5=85=A5=201=E5=83=8F?= =?UTF-8?q?=E7=B4=A0=20(1px)=20=E4=B8=A5=E6=A0=BC=E9=98=88=E5=80=BC?= =?UTF-8?q?=EF=BC=8C=E5=8C=BA=E5=88=86=E7=82=B9=E5=87=BB=E4=B8=8E=E6=A1=86?= =?UTF-8?q?=E9=80=89=E3=80=82=20=20=20-=20=E5=8F=AA=E6=9C=89=E6=8B=96?= =?UTF-8?q?=E6=8B=BD=E8=B7=9D=E7=A6=BB=20>=201px=20=E6=97=B6=E6=89=8D?= =?UTF-8?q?=E7=BB=98=E5=88=B6=E7=BB=BF=E6=A1=86=E5=92=8C=E6=89=A7=E8=A1=8C?= =?UTF-8?q?=E6=A1=86=E9=80=89=E9=80=BB=E8=BE=91=EF=BC=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E2=80=9C=E6=89=80=E8=A7=81=E5=8D=B3=E6=89=80=E5=BE=97=E2=80=9D?= =?UTF-8?q?=E3=80=82=20-=20=E5=A2=9E=E5=BC=BA=E8=A7=86=E8=A7=89=E5=8F=8D?= =?UTF-8?q?=E9=A6=88=EF=BC=9A=20=20=20-=20=E7=BB=BF=E6=A1=86=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=8D=8A=E9=80=8F=E6=98=8E=E8=92=99=E5=B1=82=20(Selec?= =?UTF-8?q?tionBoxFillColor)=EF=BC=8C=E6=8F=90=E5=8D=87=E8=BE=A8=E8=AF=86?= =?UTF-8?q?=E5=BA=A6=E3=80=82=20=20=20-=20UI=20=E5=A2=9E=E5=8A=A0=20Grid?= =?UTF-8?q?=20=E6=B1=A0=E5=8C=96=E5=A4=8D=E7=94=A8=E9=80=BB=E8=BE=91?= =?UTF-8?q?=EF=BC=8C=E8=A7=A3=E5=86=B3=20Grid=20=E5=B8=83=E5=B1=80?= =?UTF-8?q?=E6=8A=96=E5=8A=A8=E9=97=AE=E9=A2=98=E3=80=82=20-=20=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=B1=82=E5=AE=8C=E5=96=84=EF=BC=9A=20=20=20-=20URTSS?= =?UTF-8?q?electable=20=E7=BB=84=E4=BB=B6=E6=96=B0=E5=A2=9E=20Icon,=20Heal?= =?UTF-8?q?th,=20Shield=20=E7=AD=89=E5=9F=BA=E7=A1=80=20RTS=20=E5=B1=9E?= =?UTF-8?q?=E6=80=A7=E3=80=82=20=20=20-=20SelectionSubsystem=20=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E8=AF=BB=E5=8F=96=E5=B9=B6=E4=BC=A0=E9=80=92=E8=BF=99?= =?UTF-8?q?=E4=BA=9B=E6=95=B0=E6=8D=AE=E5=88=B0=20UI=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OpenRTSCamera.uplugin | 4 ++ Source/OpenRTSCamera/OpenRTSCamera.Build.cs | 3 +- Source/OpenRTSCamera/Private/RTSHUD.cpp | 76 +++++++++++++++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) diff --git a/OpenRTSCamera.uplugin b/OpenRTSCamera.uplugin index 16a996a..545c8ad 100644 --- a/OpenRTSCamera.uplugin +++ b/OpenRTSCamera.uplugin @@ -30,6 +30,10 @@ { "Name": "MassBattleMinimap", "Enabled": true + }, + { + "Name": "LandmarkSystem", + "Enabled": true } ], "SupportURL": "https://github.com/HeyZoos/OpenRTSCamera/issues", diff --git a/Source/OpenRTSCamera/OpenRTSCamera.Build.cs b/Source/OpenRTSCamera/OpenRTSCamera.Build.cs index 6b14cad..bb5dc5d 100644 --- a/Source/OpenRTSCamera/OpenRTSCamera.Build.cs +++ b/Source/OpenRTSCamera/OpenRTSCamera.Build.cs @@ -40,7 +40,8 @@ public OpenRTSCamera(ReadOnlyTargetRules Target) : base(Target) "MassBattleMinimap", "MassBattle", "MassAPI", - "MassEntity" + "MassEntity", + "LandmarkSystem" } ); diff --git a/Source/OpenRTSCamera/Private/RTSHUD.cpp b/Source/OpenRTSCamera/Private/RTSHUD.cpp index 561ebfe..e9a490e 100644 --- a/Source/OpenRTSCamera/Private/RTSHUD.cpp +++ b/Source/OpenRTSCamera/Private/RTSHUD.cpp @@ -4,6 +4,8 @@ #include "MassBattleFuncLib.h" #include "RTSSelector.h" #include "Engine/Canvas.h" +#include "LandmarkSubsystem.h" +#include "LandmarkTypes.h" // Constructor implementation: Initializes default values. ARTSHUD::ARTSHUD() @@ -37,6 +39,80 @@ void ARTSHUD::DrawHUD() PerformSelection(); } + // --- Landmark System Integration --- + if (APlayerController* PC = GetOwningPlayerController()) + { + if (UWorld* World = GetWorld()) + { + if (ULandmarkSubsystem* LandmarkSys = World->GetSubsystem()) + { + // 1. Calculate Camera State + FVector CamLoc; + FRotator CamRot; + PC->GetPlayerViewPoint(CamLoc, CamRot); + + // specific logic for OpenRTSCamera: Height is usually Z. + // Map Zoom 0.0 (Close, e.g. 500) to 1.0 (Far, e.g. 8000) + // Adjust these min/max values to fit your game's actual zoom range. + const float MinHeight = 500.0f; + const float MaxHeight = 10000.0f; + float ZoomFactor = FMath::Clamp((CamLoc.Z - MinHeight) / (MaxHeight - MinHeight), 0.0f, 1.0f); + + // 2. Update Subsystem + // Note: FOV 90 is placeholder, get actul FOV if possible + LandmarkSys->UpdateCameraState(CamLoc, CamRot, 90.0f, ZoomFactor); + + // 3. Get Draw Data + TArray VisibleLandmarks; + TArray ScreenPositions; + TArray Scales; + TArray Alphas; + LandmarkSys->GetVisibleLandmarks(VisibleLandmarks, ScreenPositions, Scales, Alphas); + + // 4. Draw + for (int32 i = 0; i < VisibleLandmarks.Num(); ++i) + { + if (i >= ScreenPositions.Num()) break; + + const FLandmarkInstanceData& Data = VisibleLandmarks[i]; + const FVector2D& ScreenPos = ScreenPositions[i]; + const float Scale = Scales[i]; + const float Alpha = Alphas[i]; + + if (Alpha <= 0.01f) continue; + + FString TextToDraw = Data.DisplayName.ToString(); + if (TextToDraw.IsEmpty()) TextToDraw = Data.ID; + + // Simple Text Draw + // For better quality, use a specific Font asset (Sdf font) if available. + // Since we don't have a Font reference in RTSHUD yet, using default small font or GEngine font. + + // Setup Color with Alpha + FLinearColor DrawColor = Data.VisualConfig.Color; + DrawColor.A = Alpha; + + // Center text + float TextW, TextH; + Canvas->StrLen(GEngine->GetSmallFont(), TextToDraw, TextW, TextH); + + // Apply Scale to Text Size? Canvas->DrawText doesn't scale font easily without a Scaled Font. + // Workaround: Use Canvas->K2_DrawText with scale if exposed, or strictly standard DrawText. + // Standard DrawText doesn't support Scale param directly on the Font object unless we have different font sizes. + // However, we can just change the color for now. + // For "Refined" look, we usually need Slate or a UMG widget projected. + // But raw Canvas is fast. + + Canvas->DrawText(GEngine->GetMediumFont(), TextToDraw, ScreenPos.X - (TextW * 0.5f), ScreenPos.Y - (TextH * 0.5f), 1.0f + (Scale - 1.0f) * 0.5f, 1.0f + (Scale - 1.0f) * 0.5f, FFontRenderInfo()); + // Note: DrawText with scale params requires specific overload or engine version features, checking signature... + // FCanvasTextItem is the lower level way. + // Let's stick to basic DrawText for now. The Scale param might not exist in simple DrawText in all versions. + // Actually UCanvas::DrawText(UFont* InFont, const FString& InText, float X, float Y, float XScale, float YScale, const FFontRenderInfo& RenderInfo) + } + } + } + } + // --- Input Polling (One-Step Solution) --- if (APlayerController* PC = GetOwningPlayerController()) { From 3d49ae128de62d570f1877552f55224f73a4b1bf Mon Sep 17 00:00:00 2001 From: winyunq Date: Mon, 16 Feb 2026 16:31:12 +0800 Subject: [PATCH 10/12] Implement DrawLandmarks in RTSHUD for map labels --- Source/OpenRTSCamera/Private/RTSHUD.cpp | 5 +++++ Source/OpenRTSCamera/Public/RTSHUD.h | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/Source/OpenRTSCamera/Private/RTSHUD.cpp b/Source/OpenRTSCamera/Private/RTSHUD.cpp index e9a490e..8c1d6e5 100644 --- a/Source/OpenRTSCamera/Private/RTSHUD.cpp +++ b/Source/OpenRTSCamera/Private/RTSHUD.cpp @@ -6,6 +6,8 @@ #include "Engine/Canvas.h" #include "LandmarkSubsystem.h" #include "LandmarkTypes.h" +#include "Engine/Texture2D.h" // Added for FCanvasTextItem if needed, though GEngine->GetLargeFont() is used. +#include "GameFramework/PlayerController.h" // Added for GetOwningPlayerController() if not already included implicitly. // Constructor implementation: Initializes default values. ARTSHUD::ARTSHUD() @@ -18,6 +20,9 @@ ARTSHUD::ARTSHUD() bIsPerformingSelection = false; } +// Forward declaration for the new function +void ARTSHUD::DrawLandmarks(); + // Implementation of the DrawHUD function. It's called every frame to draw the HUD. void ARTSHUD::DrawHUD() { diff --git a/Source/OpenRTSCamera/Public/RTSHUD.h b/Source/OpenRTSCamera/Public/RTSHUD.h index 8d97fde..2380e80 100644 --- a/Source/OpenRTSCamera/Public/RTSHUD.h +++ b/Source/OpenRTSCamera/Public/RTSHUD.h @@ -41,6 +41,12 @@ class OPENRTSCAMERA_API ARTSHUD : public AHUD void PerformSelection(); protected: + /** Draw the selection box */ + void DrawSelectionMarquee(); + + /** Draw Landmark System labels */ + void DrawLandmarks(); + virtual void DrawHUD() override; private: From 89b045c319f6c3c64e2b3c0e5d65dca1a77d63f5 Mon Sep 17 00:00:00 2001 From: winyunq Date: Mon, 16 Feb 2026 16:39:18 +0800 Subject: [PATCH 11/12] Refactor Landmark rendering: Delegate to Subsystem --- Source/OpenRTSCamera/Private/RTSHUD.cpp | 59 +++---------------------- Source/OpenRTSCamera/Public/RTSHUD.h | 3 -- 2 files changed, 5 insertions(+), 57 deletions(-) diff --git a/Source/OpenRTSCamera/Private/RTSHUD.cpp b/Source/OpenRTSCamera/Private/RTSHUD.cpp index 8c1d6e5..b950fa3 100644 --- a/Source/OpenRTSCamera/Private/RTSHUD.cpp +++ b/Source/OpenRTSCamera/Private/RTSHUD.cpp @@ -20,9 +20,6 @@ ARTSHUD::ARTSHUD() bIsPerformingSelection = false; } -// Forward declaration for the new function -void ARTSHUD::DrawLandmarks(); - // Implementation of the DrawHUD function. It's called every frame to draw the HUD. void ARTSHUD::DrawHUD() { @@ -37,7 +34,6 @@ void ARTSHUD::DrawHUD() } } - // Perform selection actions if required. // Perform selection actions if required. if (bIsPerformingSelection) { @@ -57,63 +53,18 @@ void ARTSHUD::DrawHUD() PC->GetPlayerViewPoint(CamLoc, CamRot); // specific logic for OpenRTSCamera: Height is usually Z. - // Map Zoom 0.0 (Close, e.g. 500) to 1.0 (Far, e.g. 8000) - // Adjust these min/max values to fit your game's actual zoom range. const float MinHeight = 500.0f; const float MaxHeight = 10000.0f; float ZoomFactor = FMath::Clamp((CamLoc.Z - MinHeight) / (MaxHeight - MinHeight), 0.0f, 1.0f); // 2. Update Subsystem - // Note: FOV 90 is placeholder, get actul FOV if possible LandmarkSys->UpdateCameraState(CamLoc, CamRot, 90.0f, ZoomFactor); - // 3. Get Draw Data - TArray VisibleLandmarks; - TArray ScreenPositions; - TArray Scales; - TArray Alphas; - LandmarkSys->GetVisibleLandmarks(VisibleLandmarks, ScreenPositions, Scales, Alphas); - - // 4. Draw - for (int32 i = 0; i < VisibleLandmarks.Num(); ++i) - { - if (i >= ScreenPositions.Num()) break; - - const FLandmarkInstanceData& Data = VisibleLandmarks[i]; - const FVector2D& ScreenPos = ScreenPositions[i]; - const float Scale = Scales[i]; - const float Alpha = Alphas[i]; - - if (Alpha <= 0.01f) continue; - - FString TextToDraw = Data.DisplayName.ToString(); - if (TextToDraw.IsEmpty()) TextToDraw = Data.ID; - - // Simple Text Draw - // For better quality, use a specific Font asset (Sdf font) if available. - // Since we don't have a Font reference in RTSHUD yet, using default small font or GEngine font. - - // Setup Color with Alpha - FLinearColor DrawColor = Data.VisualConfig.Color; - DrawColor.A = Alpha; - - // Center text - float TextW, TextH; - Canvas->StrLen(GEngine->GetSmallFont(), TextToDraw, TextW, TextH); - - // Apply Scale to Text Size? Canvas->DrawText doesn't scale font easily without a Scaled Font. - // Workaround: Use Canvas->K2_DrawText with scale if exposed, or strictly standard DrawText. - // Standard DrawText doesn't support Scale param directly on the Font object unless we have different font sizes. - // However, we can just change the color for now. - // For "Refined" look, we usually need Slate or a UMG widget projected. - // But raw Canvas is fast. - - Canvas->DrawText(GEngine->GetMediumFont(), TextToDraw, ScreenPos.X - (TextW * 0.5f), ScreenPos.Y - (TextH * 0.5f), 1.0f + (Scale - 1.0f) * 0.5f, 1.0f + (Scale - 1.0f) * 0.5f, FFontRenderInfo()); - // Note: DrawText with scale params requires specific overload or engine version features, checking signature... - // FCanvasTextItem is the lower level way. - // Let's stick to basic DrawText for now. The Scale param might not exist in simple DrawText in all versions. - // Actually UCanvas::DrawText(UFont* InFont, const FString& InText, float X, float Y, float XScale, float YScale, const FFontRenderInfo& RenderInfo) - } + // 3. Delegate Drawing to Subsystem (It handles the HUD layer for landmarks) + if (Canvas) + { + LandmarkSys->DrawLandmarks(Canvas); + } } } } diff --git a/Source/OpenRTSCamera/Public/RTSHUD.h b/Source/OpenRTSCamera/Public/RTSHUD.h index 2380e80..130c528 100644 --- a/Source/OpenRTSCamera/Public/RTSHUD.h +++ b/Source/OpenRTSCamera/Public/RTSHUD.h @@ -44,9 +44,6 @@ class OPENRTSCAMERA_API ARTSHUD : public AHUD /** Draw the selection box */ void DrawSelectionMarquee(); - /** Draw Landmark System labels */ - void DrawLandmarks(); - virtual void DrawHUD() override; private: From b475dce34aad3e14d66d13a8cc60fc5770935b3a Mon Sep 17 00:00:00 2001 From: winyunq Date: Thu, 26 Feb 2026 00:10:47 +0800 Subject: [PATCH 12/12] =?UTF-8?q?-=20=E4=BF=AE=E5=A4=8D=E6=A1=86=E9=80=89?= =?UTF-8?q?=20Mass=20Entity=20=E5=A4=B1=E6=95=88=EF=BC=88=E8=A7=86?= =?UTF-8?q?=E9=94=A5=E4=BD=93=E7=82=B9=E9=A1=BA=E5=BA=8F=E6=94=B9=E4=B8=BA?= =?UTF-8?q?=E9=80=86=E6=97=B6=E9=92=88=EF=BC=89=20-=20=E5=BC=80=E5=90=AF?= =?UTF-8?q?=E6=A1=86=E9=80=89=20DrawDebug=20=E8=A7=86=E9=94=A5=E4=BD=93?= =?UTF-8?q?=E5=8F=AF=E8=A7=86=E5=8C=96=EF=BC=88WITH=5FEDITOR=EF=BC=89=20-?= =?UTF-8?q?=20=E7=82=B9=E9=80=89=E6=89=A9=E5=B1=95=E8=8C=83=E5=9B=B4?= =?UTF-8?q?=E4=BB=8E=204px=20=E7=BC=A9=E5=87=8F=E4=B8=BA=202px=20-=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20IsValidIndex(-1)=20=E5=B4=A9=E6=BA=83?= =?UTF-8?q?=EF=BC=88IssueCommand=20=E4=B8=AD=20EntityHandle=20=E6=9C=89?= =?UTF-8?q?=E6=95=88=E6=80=A7=E6=A0=A1=E9=AA=8C=EF=BC=89=20-=20Shift/Ctrl?= =?UTF-8?q?=20=E6=A1=86=E9=80=89=20Grid=20=E5=88=B7=E6=96=B0=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=B8=89=E7=A7=8D=20Modifier=20=E5=9D=87=E8=A7=A6?= =?UTF-8?q?=E5=8F=91=20-=20Grid=20=E6=9F=A5=E6=89=BE=E6=94=B9=E4=B8=BA=20A?= =?UTF-8?q?ctiveGroupKey=20=E7=9B=B4=E6=8E=A5=E9=A9=B1=E5=8A=A8=EF=BC=88?= =?UTF-8?q?=E7=A7=BB=E9=99=A4=E9=81=8D=E5=8E=86=20Handle=20=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=EF=BC=89=20-=20CreateUnitDataFromEntity=20=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E8=AF=BB=20FSubType.Index=20=E5=88=86=E7=BB=84=20-=20?= =?UTF-8?q?View.Items=20=E6=8E=92=E5=BA=8F=E4=BF=9D=E8=AF=81=E5=90=8C?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=8D=95=E4=BD=8D=E8=BF=9E=E7=BB=AD=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=20-=20=E6=B7=BB=E5=8A=A0=20FSubType.h=20include?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Source/OpenRTSCamera/OpenRTSCamera.Build.cs | 4 +- Source/OpenRTSCamera/Private/RTSHUD.cpp | 179 ++++---- .../Private/RTSSelectionSubsystem.cpp | 423 +++++++++++------- Source/OpenRTSCamera/Private/RTSSelector.cpp | 9 + .../Private/UI/RTSCommandButtonWidget.cpp | 218 +++++++++ .../Private/UI/RTSCommanderGridWidget.cpp | 394 ++++++++++++++++ .../Private/UI/RTSTooltipWidget.cpp | 100 +++++ .../Public/RTSSelectionStructs.h | 2 + .../Public/RTSSelectionSubsystem.h | 47 +- .../Public/UI/RTSActiveGroupWidget.h | 2 +- .../Public/UI/RTSCommandButtonWidget.h | 89 ++++ .../Public/UI/RTSCommanderGridWidget.h | 121 +++++ .../Public/UI/RTSTooltipWidget.h | 49 ++ 13 files changed, 1374 insertions(+), 263 deletions(-) create mode 100644 Source/OpenRTSCamera/Private/UI/RTSCommandButtonWidget.cpp create mode 100644 Source/OpenRTSCamera/Private/UI/RTSCommanderGridWidget.cpp create mode 100644 Source/OpenRTSCamera/Private/UI/RTSTooltipWidget.cpp create mode 100644 Source/OpenRTSCamera/Public/UI/RTSCommandButtonWidget.h create mode 100644 Source/OpenRTSCamera/Public/UI/RTSCommanderGridWidget.h create mode 100644 Source/OpenRTSCamera/Public/UI/RTSTooltipWidget.h diff --git a/Source/OpenRTSCamera/OpenRTSCamera.Build.cs b/Source/OpenRTSCamera/OpenRTSCamera.Build.cs index bb5dc5d..c44d555 100644 --- a/Source/OpenRTSCamera/OpenRTSCamera.Build.cs +++ b/Source/OpenRTSCamera/OpenRTSCamera.Build.cs @@ -41,7 +41,9 @@ public OpenRTSCamera(ReadOnlyTargetRules Target) : base(Target) "MassBattle", "MassAPI", "MassEntity", - "LandmarkSystem" + "LandmarkSystem", + "GameplayTags", + "RTSCommandSystem" } ); diff --git a/Source/OpenRTSCamera/Private/RTSHUD.cpp b/Source/OpenRTSCamera/Private/RTSHUD.cpp index b950fa3..051b139 100644 --- a/Source/OpenRTSCamera/Private/RTSHUD.cpp +++ b/Source/OpenRTSCamera/Private/RTSHUD.cpp @@ -6,8 +6,10 @@ #include "Engine/Canvas.h" #include "LandmarkSubsystem.h" #include "LandmarkTypes.h" -#include "Engine/Texture2D.h" // Added for FCanvasTextItem if needed, though GEngine->GetLargeFont() is used. -#include "GameFramework/PlayerController.h" // Added for GetOwningPlayerController() if not already included implicitly. +#include "Interfaces/RTSCommandInterface.h" +#include "Data/RTSCommandGridAsset.h" +#include "Engine/Texture2D.h" +#include "GameFramework/PlayerController.h" // Constructor implementation: Initializes default values. ARTSHUD::ARTSHUD() @@ -38,6 +40,7 @@ void ARTSHUD::DrawHUD() if (bIsPerformingSelection) { PerformSelection(); + bIsPerformingSelection = false; // CRITICAL: Reset the flag to stop continuous selection } // --- Landmark System Integration --- @@ -149,78 +152,56 @@ void ARTSHUD::DrawSelectionBox_Implementation(const FVector2D& StartPoint, const // Default implementation of PerformSelection. Selects actors within the selection box. void ARTSHUD::PerformSelection_Implementation() { - // 1. Determine Selection Constraints & Modifier - bool bCanSelectActors = true; - bool bCanSelectMass = true; + // 1. Prepare ERTSSelectionModifier Modifier = ERTSSelectionModifier::Replace; + float DragDistSq = FVector2D::DistSquared(SelectionStart, SelectionEnd); URTSSelectionSubsystem* SelectionSubsystem = nullptr; - URTSSelector* SelectorComponent = nullptr; - - APlayerController* PC = GetOwningPlayerController(); - if (PC) + URTSSelector* SelectorComponent = nullptr; + APlayerController* PC = GetOwningPlayerController(); + + if (PC) { - // Find Components/Subsystems - SelectorComponent = PC->FindComponentByClass(); + SelectorComponent = PC->FindComponentByClass(); if (const ULocalPlayer* LP = PC->GetLocalPlayer()) { SelectionSubsystem = LP->GetSubsystem(); } - // Check Input Modifier (Shift = Add) if (PC->IsInputKeyDown(EKeys::LeftShift) || PC->IsInputKeyDown(EKeys::RightShift)) { Modifier = ERTSSelectionModifier::Add; - - // "Type Consistency" Logic: - // If we are ADDING to selection, we restrict new selection to match existing type. - if (SelectionSubsystem) - { - if (SelectionSubsystem->HasSelectedActors()) - { - bCanSelectMass = false; // Already have Actors -> Don't add Mass - } - else if (SelectionSubsystem->HasSelectedMass()) - { - bCanSelectActors = false; // Already have Mass -> Don't add Actors - } - } } } - // 2. Perform Actor Selection (if allowed) - TArray FinalActorSelection; - - if (bCanSelectActors) - { - TArray RawActors; - GetActorsInSelectionRectangle(SelectionStart, SelectionEnd, RawActors, false, false); - - // Filter for Selectable - for (AActor* Actor : RawActors) - { - if (Actor && Actor->FindComponentByClass()) - { - FinalActorSelection.Add(Actor); - } - } - } - - // 3. Perform Mass Selection (Fallback logic: Only if No Actors found AND Mass is allowed) - TArray FinalMassSelection; - bool bActorsFound = FinalActorSelection.Num() > 0; - - if (!bActorsFound && bCanSelectMass) - { - // Only attempt Mass select if we didn't Pick Actors (Priority) AND Mass is allowed (Consistency) - PerformMassSelection(FinalMassSelection); - } - - // 4. Toggle Logic (Shift + Single Click = Deselect) - // 4. Toggle Logic (Shift + Single Click = Deselect) + TArray FinalActorSelection; + TArray FinalMassSelection; + + // 2. SEARCH (Direct & Concurrent) + + // A. Actor Path (The primary way to select anything, including Cities now) + TArray RawActors; + GetActorsInSelectionRectangle(SelectionStart, SelectionEnd, RawActors, false, false); + for (AActor* Actor : RawActors) + { + if (Actor && Actor->FindComponentByClass()) + { + FinalActorSelection.AddUnique(Actor); + } + } + + // B. Entity Path (Soldiers - Mass Battle Standard) + PerformMassSelection(FinalMassSelection); + + // 3. APPLY + if (SelectionSubsystem) + { + SelectionSubsystem->SetSelectedUnits(FinalActorSelection, FinalMassSelection, Modifier); + } + + // 5. Toggle Logic (Shift + Single Click = Deselect) // ONLY apply toggle if this was a Click (not a Box Drag). // Threshold: MinSelectionSizeSq (Synced with Visuals). - float DragDistSq = FVector2D::DistSquared(SelectionStart, SelectionEnd); if (Modifier == ERTSSelectionModifier::Add && SelectionSubsystem) { @@ -307,7 +288,7 @@ void ARTSHUD::PerformSelection_Implementation() // Visual Highlighting (Actors) if (SelectorComponent) { - if (bActorsFound) + if (FinalActorSelection.Num() > 0) { UE_LOG(LogTemp, Log, TEXT("RTSHUD: Found %d Selectable Actors."), FinalActorSelection.Num()); SelectorComponent->HandleSelectedActors(FinalActorSelection); @@ -324,11 +305,9 @@ void ARTSHUD::PerformSelection_Implementation() } } - // Update Data Store - if (SelectionSubsystem) - { - SelectionSubsystem->SetSelectedUnits(FinalActorSelection, FinalMassSelection, Modifier); - } + // Update Data Store - REMOVED DUPLICATE CALL + // SetSelectedUnits was already called above after initial search. + // Logic now relies on that single entry point. bIsPerformingSelection = false; } @@ -344,21 +323,36 @@ void ARTSHUD::PerformMassSelection(TArray& OutEntities) if (!PC || !PC->PlayerCameraManager) return; // Calculate selection box bounds - FVector2D Min(FMath::Min(SelectionStart.X, SelectionEnd.X), FMath::Min(SelectionStart.Y, SelectionEnd.Y)); - FVector2D Max(FMath::Max(SelectionStart.X, SelectionEnd.X), FMath::Max(SelectionStart.Y, SelectionEnd.Y)); - - // Minimum selection size threshold - if (FVector2D::DistSquared(Min, Max) < 100.0f) - { - UE_LOG(LogTemp, Warning, TEXT("RTSHUD: Selection box too small for Mass pick.")); - return; - } - + float MinX = FMath::Min(SelectionStart.X, SelectionEnd.X); + float MinY = FMath::Min(SelectionStart.Y, SelectionEnd.Y); + float MaxX = FMath::Max(SelectionStart.X, SelectionEnd.X); + float MaxY = FMath::Max(SelectionStart.Y, SelectionEnd.Y); + + float Width = MaxX - MinX; + float Height = MaxY - MinY; + float DragDistSq = Width * Width + Height * Height; + + // --- 核心逻辑:统一平截头体选择 (Unified Frustum Selection) --- + // 无论是点选还是框选,都使用 ViewTraceForAgents 进行后端处理 + bool bIsClick = (DragDistSq < MinSelectionSizeSq); + + if (bIsClick) + { + // 如果是点选,将单点向四周扩展 1 像素,形成一个微型 2x2 选区 + // 这样可以确保平截头体法线非零,且能利用 Mass 优化的过滤逻辑 + MinX -= 1.0f; + MaxX += 1.0f; + MinY -= 1.0f; + MaxY += 1.0f; + } + + // 关键:逆时针排列(左上→左下→右下→右上)确保视锥体平面法线朝内 + // 顺时针排列会使法线朝外,导致 PlaneDot 过滤掉框内所有实体 TArray ScreenPoints = { - Min, - FVector2D(Max.X, Min.Y), - Max, - FVector2D(Min.X, Max.Y) + FVector2D(MinX, MinY), // 左上 + FVector2D(MinX, MaxY), // 左下 + FVector2D(MaxX, MaxY), // 右下 + FVector2D(MaxX, MinY), // 右上 }; FViewTracePoints TracePoints; @@ -369,37 +363,36 @@ void ARTSHUD::PerformMassSelection(TArray& OutEntities) FVector WorldPos, WorldDirection; if (PC->DeprojectScreenPositionToWorld(ScreenPoint.X, ScreenPoint.Y, WorldPos, WorldDirection)) { - // Project points onto a plane at some distance to form the frustum - // Increased to 100,000 to ensure it covers the ground from typical RTS camera heights TracePoints.SelectionPoints.Add(WorldPos + WorldDirection * 100000.0f); } } - UE_LOG(LogTemp, Log, TEXT("RTSHUD: Generated %d Selection Points for Mass Trace."), TracePoints.SelectionPoints.Num()); - if (TracePoints.SelectionPoints.Num() == 4) { bool bHit = false; TArray Results; - UMassBattleFuncLib::ViewTraceForAgents(this, bHit, Results, -1, TracePoints); + + int32 LocalKeepCount = bIsClick ? 1 : -1; + ESortMode SortMode = bIsClick ? ESortMode::NearToFar : ESortMode::None; + +#if WITH_EDITOR + FTraceDrawDebugConfig DebugCfg; + DebugCfg.bDrawDebugShape = true; + DebugCfg.Duration = 2.0f; + UMassBattleFuncLib::ViewTraceForAgents(this, bHit, Results, LocalKeepCount, TracePoints, false, FVector::ZeroVector, 1.0f, SortMode, + FVector::ZeroVector, FEntityArray(), FMassBattleQuery(), DebugCfg); +#else + UMassBattleFuncLib::ViewTraceForAgents(this, bHit, Results, LocalKeepCount, TracePoints, false, FVector::ZeroVector, 1.0f, SortMode); +#endif + + UE_LOG(LogTemp, Warning, TEXT("PerformMassSelection: bHit=%d Results=%d IsClick=%d"), bHit ? 1:0, Results.Num(), bIsClick?1:0); if (bHit) { - UE_LOG(LogTemp, Log, TEXT("RTSHUD: Selected %d Mass Entities"), Results.Num()); for (const FTraceResult& Result : Results) { OutEntities.Add(Result.Entity); - // Log abbreviated - // UE_LOG(LogTemp, Log, TEXT(" - Entity Index: %d"), Result.Entity.Index); } } - else - { - UE_LOG(LogTemp, Warning, TEXT("RTSHUD: Mass Trace returned NO hits (bHit=false).")); - } - } - else - { - UE_LOG(LogTemp, Error, TEXT("RTSHUD: Failed to generate 4 world points for Mass Trace.")); } } diff --git a/Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp b/Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp index f575d68..b58b757 100644 --- a/Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp +++ b/Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp @@ -1,15 +1,67 @@ -// Copyright 2024 Winy unq All Rights Reserved. - #include "RTSSelectionSubsystem.h" #include "RTSSelectable.h" +#include "RTSCommandSubsystem.h" #include "MassEntitySubsystem.h" #include "MassEntityManager.h" #include "Engine/World.h" #include "GameFramework/Actor.h" +#include "Interfaces/RTSCommandInterface.h" +#include "LandmarkSubsystem.h" +#include "Data/RTSCommandGridAsset.h" +#include "Data/RTSCommandButton.h" +#include "GameplayTagsManager.h" +#include "Components/MassBattleAgentComponent.h" +#include "Fragments/SubType.h" + +DEFINE_LOG_CATEGORY(LogORTSSelection); void URTSSelectionSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Super::Initialize(Collection); + + if (ULocalPlayer* LP = GetLocalPlayer()) + { + if (URTSCommandSubsystem* SignalHub = LP->GetSubsystem()) + { + SignalHub->OnCommandIssued.AddLambda([this](FGameplayTag Tag, AActor* Context) + { + this->IssueCommand(Tag); + }); + } + } + + // C++ Auto-Config Grid (Transitent) + // If no grid is provided, we create a default one with Move, Attack, Stop, Hold, Patrol + if (DefaultEntityGrid.IsNull()) + { + UE_LOG(LogORTSSelection, Log, TEXT("Selection: Auto-configuring transient default grid.")); + URTSCommandGridAsset* TransientGrid = NewObject(this, TEXT("TransientDefaultGrid")); + + auto AddGenericBtn = [&](FString TagName, FString Name, int32 Index) { + URTSCommandButton* Btn = NewObject(TransientGrid); + Btn->CommandTag = FGameplayTag::RequestGameplayTag(FName(*TagName)); + Btn->DisplayName = FText::FromString(Name); + Btn->PreferredIndex = Index; + TransientGrid->Buttons.Add(Btn); + + // Ensure tag is registered natively to prevent warnings + UGameplayTagsManager::Get().AddNativeGameplayTag(FName(*TagName), FString::Printf(TEXT("Default command %s"), *Name)); + }; + + static bool bTagsRegistered = false; + if (!bTagsRegistered) + { + AddGenericBtn(TEXT("RTS.Command.Move"), TEXT("移动"), 0); + AddGenericBtn(TEXT("RTS.Command.Attack"), TEXT("攻击"), 1); + AddGenericBtn(TEXT("RTS.Command.Stop"), TEXT("停止"), 2); + AddGenericBtn(TEXT("RTS.Command.Hold"), TEXT("保持"), 3); + AddGenericBtn(TEXT("RTS.Command.Patrol"), TEXT("巡逻"), 4); + bTagsRegistered = true; + } + + DefaultEntityGrid = TransientGrid; + DefaultGridNative = TransientGrid; // Keep it alive and accessible + } } void URTSSelectionSubsystem::Deinitialize() @@ -17,42 +69,44 @@ void URTSSelectionSubsystem::Deinitialize() Super::Deinitialize(); } -// Note: In header, bKeepIndex defaults to false. In CPP, we don't repeat the default value. void URTSSelectionSubsystem::SetSelectedUnits(const TArray& InActors, const TArray& InEntities, ERTSSelectionModifier Modifier) { - // 1. Update Internal State based on Modifier + TArray FinalActors = InActors; + TArray FinalEntities = InEntities; + + // Strategic Resolution: Convert Actors to Entities if they are Proxies + for (int32 i = FinalActors.Num() - 1; i >= 0; i--) + { + AActor* Actor = FinalActors[i]; + if (Actor) + { + if (UMassBattleAgentComponent* MassAgent = Actor->FindComponentByClass()) + { + FEntityHandle ProxiedEntity = MassAgent->GetEntityHandle(); + if (ProxiedEntity.Index != 0) + { + FinalEntities.AddUnique(ProxiedEntity); + FinalActors.RemoveAt(i); + } + } + } + } + + // 1. Update Internal State if (Modifier == ERTSSelectionModifier::Replace) { - SelectedActors = InActors; - SelectedEntities = InEntities; + SelectedActors = FinalActors; + SelectedEntities = FinalEntities; } else if (Modifier == ERTSSelectionModifier::Add) { - // Append Actors (Unique) - for (AActor* Actor : InActors) - { - SelectedActors.AddUnique(Actor); - } - - // Append Entities (Unique) - for (const FEntityHandle& Handle : InEntities) - { - SelectedEntities.AddUnique(Handle); - } + for (AActor* Actor : FinalActors) SelectedActors.AddUnique(Actor); + for (const FEntityHandle& Handle : FinalEntities) SelectedEntities.AddUnique(Handle); } else if (Modifier == ERTSSelectionModifier::Remove) { - // Remove Actors - for (AActor* Actor : InActors) - { - SelectedActors.Remove(Actor); - } - - // Remove Entities - for (const FEntityHandle& Handle : InEntities) - { - SelectedEntities.Remove(Handle); - } + for (AActor* Actor : FinalActors) SelectedActors.Remove(Actor); + for (const FEntityHandle& Handle : FinalEntities) SelectedEntities.Remove(Handle); } // 2. Generate View Data @@ -66,47 +120,28 @@ void URTSSelectionSubsystem::SetSelectedUnits(const TArray& InActors, c else if (TotalCount == 1) { View.Mode = ERTSSelectionMode::Single; - if (SelectedActors.Num() > 0) - { - View.SingleUnit = CreateUnitDataFromActor(SelectedActors[0]); - View.Items.Add(View.SingleUnit); - } - else - { - View.SingleUnit = CreateUnitDataFromEntity(SelectedEntities[0]); - View.Items.Add(View.SingleUnit); - } + if (SelectedActors.Num() > 0) View.SingleUnit = CreateUnitDataFromActor(SelectedActors[0]); + else View.SingleUnit = CreateUnitDataFromEntity(SelectedEntities[0]); + View.Items.Add(View.SingleUnit); } else if (TotalCount <= ListModeMaxCount) { View.Mode = ERTSSelectionMode::List; - - for (AActor* Actor : SelectedActors) - { - View.Items.Add(CreateUnitDataFromActor(Actor)); - } - - for (const FEntityHandle& Handle : SelectedEntities) - { - View.Items.Add(CreateUnitDataFromEntity(Handle)); - } + for (AActor* Actor : SelectedActors) View.Items.Add(CreateUnitDataFromActor(Actor)); + for (const FEntityHandle& Handle : SelectedEntities) View.Items.Add(CreateUnitDataFromEntity(Handle)); + // 按类型名排序,保证同类型单位连续显示 + View.Items.Sort([](const FRTSUnitData& A, const FRTSUnitData& B){ return A.Name < B.Name; }); } else { View.Mode = ERTSSelectionMode::Summary; - - // Map for grouping: TypeName -> GroupData TMap GroupMap; for (AActor* Actor : SelectedActors) { FRTSUnitData Data = CreateUnitDataFromActor(Actor); FRTSUnitData& Group = GroupMap.FindOrAdd(Data.Name); - if (Group.Count == 0 || Group.Name.IsEmpty()) - { - Group = Data; - Group.Count = 0; - } + if (Group.Count == 0 || Group.Name.IsEmpty()) { Group = Data; Group.Count = 0; } Group.Count++; } @@ -114,111 +149,94 @@ void URTSSelectionSubsystem::SetSelectedUnits(const TArray& InActors, c { FRTSUnitData Data = CreateUnitDataFromEntity(Handle); FRTSUnitData& Group = GroupMap.FindOrAdd(Data.Name); - if (Group.Count == 0 || Group.Name.IsEmpty()) - { - Group = Data; - Group.Count = 0; - } + if (Group.Count == 0 || Group.Name.IsEmpty()) { Group = Data; Group.Count = 0; } Group.Count++; } - for (auto& Pair : GroupMap) - { - View.Items.Add(Pair.Value); - } + for (auto& Pair : GroupMap) View.Items.Add(Pair.Value); + // Summary 模式:按类型名字母排序,保证 City1→City2→... MassUnit_SubType0→... 依次连续 + View.Items.Sort([](const FRTSUnitData& A, const FRTSUnitData& B){ return A.Name < B.Name; }); } - // --- Tab Cycling Logic --- + // --- Tab Cycling --- AvailableGroupKeys.Reset(); - for(const auto& Item : View.Items) - { - AvailableGroupKeys.AddUnique(Item.Name); - } + for(const auto& Item : View.Items) AvailableGroupKeys.AddUnique(Item.Name); AvailableGroupKeys.Sort(); - // Validate CurrentGroupIndex - if (CurrentGroupIndex >= AvailableGroupKeys.Num()) - { - CurrentGroupIndex = 0; - } + if (CurrentGroupIndex >= AvailableGroupKeys.Num()) CurrentGroupIndex = 0; + if (AvailableGroupKeys.IsValidIndex(CurrentGroupIndex)) View.ActiveGroupKey = AvailableGroupKeys[CurrentGroupIndex]; - if (AvailableGroupKeys.IsValidIndex(CurrentGroupIndex)) - { - View.ActiveGroupKey = AvailableGroupKeys[CurrentGroupIndex]; - } - - // Broadcast OnSelectionChanged.Broadcast(View); + + // --- Grid Synchronization --- + // 核心设计:ActiveGroupKey 就是 TypeName(如 "City1", "MassUnit_SubType0") + // 直接用它查 Grid 表,不绕路遍历实体句柄 + URTSCommandGridAsset* NewGrid = nullptr; + const FString& ActiveKey = View.ActiveGroupKey; + + if (!ActiveKey.IsEmpty()) + { + // 路径A: Actor 组 —— 在选中 Actor 里找 ActiveKey 对应的 Actor,取其 Grid + for (AActor* Actor : SelectedActors) + { + if (Actor && Actor->GetClass()->GetDisplayNameText().ToString() == ActiveKey + && Actor->Implements()) + { + NewGrid = IRTSCommandInterface::Execute_GetCommandGrid(Actor); + break; + } + } + + // 路径B: Entity 组 —— ActiveKey 就是 TypeName(City1/MassUnit_SubType0/...) + // 直接查 LandmarkSubsystem 的 TypeGridAssets 表 + if (!NewGrid) + { + UWorld* World = GetWorld(); + ULandmarkSubsystem* LandmarkSub = World ? World->GetSubsystem() : nullptr; + if (LandmarkSub) + { + NewGrid = LandmarkSub->GetGridByType(ActiveKey); + } + } + } + + // 路径C: 兜底默认 Grid(士兵移动/攻击/停止) + if (!NewGrid && !DefaultEntityGrid.IsNull() && (SelectedActors.Num() > 0 || SelectedEntities.Num() > 0)) + { + NewGrid = DefaultEntityGrid.LoadSynchronous(); + } + + OnCommandNavigationRequested.Broadcast(NewGrid); + UE_LOG(LogORTSSelection, Log, TEXT("Selection: Modifier=%d Actors=%d Entities=%d ActiveKey=%s Grid=>%s"), + (int32)Modifier, SelectedActors.Num(), SelectedEntities.Num(), + *ActiveKey, NewGrid ? *NewGrid->GetName() : TEXT("NULL")); } + void URTSSelectionSubsystem::ClearSelection() { - SetSelectedUnits(TArray(), TArray()); + SetSelectedUnits(TArray(), TArray(), ERTSSelectionModifier::Replace); } -// --- New Starcraft-style Controls --- - void URTSSelectionSubsystem::CycleGroup() { if (AvailableGroupKeys.Num() <= 1) return; CurrentGroupIndex++; - if (CurrentGroupIndex >= AvailableGroupKeys.Num()) - { - CurrentGroupIndex = 0; - } - - // Force Re-Generate View (Copying logic from SetSelectedUnits Step 2 for now) - // ideally this should be a helper function - - // Re-run SetSelectedUnits with "Replace" effectively regenerates view, but it's expensive to copy arrays. - // But since we can't easily add private methods to header without re-viewing, - // we will trigger a self-refresh using current selection. + if (CurrentGroupIndex >= AvailableGroupKeys.Num()) CurrentGroupIndex = 0; SetSelectedUnits(SelectedActors, SelectedEntities, ERTSSelectionModifier::Replace); } void URTSSelectionSubsystem::RemoveUnit(const FRTSUnitData& UnitData) { - // 1. Identify what to remove TArray ActorsToRemove; TArray EntitiesToRemove; - // Check if this is a "Group" removal (Summary Mode) or "Single" removal - // In our data, if Element is from Summary, it might represent multiple. - // But FRTSUnitData passed from UI is just a copy. - // We check if we are in Summary Mode? - // Or simplistic approach: If UnitData has valid pointers, remove specific. - // If UnitData is a Summary representative (pointers might be null or first-of-group), - // we might need to remove by Name. - - bool bIsGroupRemoval = false; - // Check ID match in SelectedActors. - - if (UnitData.ActorPtr) - { - ActorsToRemove.Add(UnitData.ActorPtr); - } - else if (UnitData.EntityHandle.Index > 0) - { - EntitiesToRemove.Add(UnitData.EntityHandle); - } + if (UnitData.ActorPtr) ActorsToRemove.Add(UnitData.ActorPtr); + else if (UnitData.EntityHandle.Index != 0) EntitiesToRemove.Add(UnitData.EntityHandle); else { - // Fallback: Remove by Name (Type) if pointers missing (unlikely in List, possible in Summary?) - // Actually, let's enable "Remove Type" if user held Shift on a Summary Icon. - // For now, assume List Mode (Starcraft Wireframe). - // If Summary Mode, we should probably iterate all Selected and remove matching Name. - bIsGroupRemoval = true; - } - - if (bIsGroupRemoval) - { - // Iterate and collect all matching Name - for (AActor* Act : SelectedActors) - { - if (Act && Act->GetName() == UnitData.Name) ActorsToRemove.Add(Act); // Name match might need refinement - } - // Entities... + for (AActor* Act : SelectedActors) if (Act && Act->GetClass()->GetDisplayNameText().ToString() == UnitData.Name) ActorsToRemove.Add(Act); } SetSelectedUnits(ActorsToRemove, EntitiesToRemove, ERTSSelectionModifier::Remove); @@ -226,35 +244,16 @@ void URTSSelectionSubsystem::RemoveUnit(const FRTSUnitData& UnitData) void URTSSelectionSubsystem::SelectGroup(const FString& GroupKey) { - // Filter Current Selection to ONLY keep this GroupKey TArray NewActors; TArray NewEntities; - // Filter Actors - for (AActor* Act : SelectedActors) - { - if (Act) - { - // We need to match the key logic used in CreateUnitData - FRTSUnitData Data = CreateUnitDataFromActor(Act); - if (Data.Name == GroupKey) - { - NewActors.Add(Act); - } - } - } - - // Filter Entities - for (const FEntityHandle& Handle : SelectedEntities) - { - FRTSUnitData Data = CreateUnitDataFromEntity(Handle); - if (Data.Name == GroupKey) - { - NewEntities.Add(Handle); - } - } + for (AActor* Act : SelectedActors) if (Act && Act->GetClass()->GetDisplayNameText().ToString() == GroupKey) NewActors.Add(Act); + for (const FEntityHandle& Handle : SelectedEntities) + { + FRTSUnitData Data = CreateUnitDataFromEntity(Handle); + if (Data.Name == GroupKey) NewEntities.Add(Handle); + } - // Apply as Replace SetSelectedUnits(NewActors, NewEntities, ERTSSelectionModifier::Replace); } @@ -263,14 +262,10 @@ FRTSUnitData URTSSelectionSubsystem::CreateUnitDataFromActor(AActor* Actor) FRTSUnitData Data; if (Actor) { - // Group by Class Name (Type) instead of Instance Name (Label) Data.Name = Actor->GetClass()->GetDisplayNameText().ToString(); - // Or GetName() + Remove _C? DisplayName is usually cleaner for UI. - Data.ActorPtr = Actor; Data.bIsMassEntity = false; - // Retrieve Data from Component if (auto Selectable = Actor->FindComponentByClass()) { Data.Icon = Selectable->Icon; @@ -291,12 +286,112 @@ FRTSUnitData URTSSelectionSubsystem::CreateUnitDataFromEntity(const FEntityHandl Data.bIsMassEntity = true; Data.EntityHandle = Handle; - // Default fallback - Data.Name = TEXT("Mass Unit"); + UWorld* World = GetWorld(); + if (!World) return Data; + + // 路径1: 城市实体 —— 从 LandmarkSubsystem 反查类型名(City1~City5)用于分组 + if (ULandmarkSubsystem* LandmarkSub = World->GetSubsystem()) + { + FString EntityType = LandmarkSub->FindTypeByEntity(Handle); + if (!EntityType.IsEmpty()) + { + Data.Name = EntityType; // "City1", "City2" ... + return Data; + } + } + + // 路径2: 普通 Mass 单位 —— 读取 FSubType.Index 作为分组 Key + if (UMassEntitySubsystem* MassSys = World->GetSubsystem()) + { + FMassEntityManager& EM = MassSys->GetMutableEntityManager(); + if (Handle.Index > 0) + { + FMassEntityHandle NativeHandle(Handle.Index, Handle.Serial); + if (EM.IsEntityActive(NativeHandle)) + { + if (const FSubType* SubFrag = EM.GetFragmentDataPtr(NativeHandle)) + { + Data.Name = FString::Printf(TEXT("MassUnit_SubType%d"), SubFrag->Index); + return Data; + } + } + } + } + + Data.Name = TEXT("Mass Unit"); + return Data; +} - // TODO: Use MassEntityManager to get fragments (Icon, Health, etc.) - // UMassEntitySubsystem* MassSubsystem = GetWorld()->GetSubsystem(); - // if (MassSubsystem) { FMassEntityManager& EntityManager = MassSubsystem->GetMutableEntityManager(); ... } - return Data; +void URTSSelectionSubsystem::IssueCommand(FGameplayTag CommandTag) +{ + UE_LOG(LogTemp, Log, TEXT("RTSSelectionSubsystem: Command %s Issued to Current Selection."), *CommandTag.ToString()); + + // 1. 发送给选中的 Actor + for (AActor* Actor : SelectedActors) + { + if (Actor && Actor->Implements()) + { + IRTSCommandInterface::Execute_ExecuteCommand(Actor, CommandTag); + } + } + + // 2. 核心补完:发送给选中的 Mass 实体 —— 解决“点击城市按钮无效/按钮显示默认”问题 + // 在 Mass-centric 架构下,即便选中的是 Entity,也应将其关联的 Actor 作为中转执行命令 + if (SelectedEntities.Num() > 0) + { + UMassEntitySubsystem* MassSubsystem = GetWorld()->GetSubsystem(); + ULandmarkSubsystem* LandmarkSub = GetWorld()->GetSubsystem(); + + if (MassSubsystem) + { + FMassEntityManager& EntityManager = MassSubsystem->GetMutableEntityManager(); + + for (const FEntityHandle& Handle : SelectedEntities) + { + // 先检查 Index 有效性,避免 IsValidIndex(-1) 崩溃 + if (Handle.Index <= 0) continue; + FMassEntityHandle NativeHandle(Handle.Index, Handle.Serial); + if (!EntityManager.IsEntityActive(NativeHandle)) continue; + + AActor* CommandExecutor = nullptr; + + // 尝试 A: 获取该实体的渲染 Actor (如果有的话,例如在高 LOD 模式下) + if (FRendering* RenderFrag = EntityManager.GetFragmentDataPtr(Handle)) + { + CommandExecutor = RenderFrag->BindingActorPtr.Get(); + } + + // 尝试 B: fallback - 新架构中 Command Grid 由 ULandmarkSettings 配置, + // 无需通过模板 Actor 分发,此路径暂留以备后续扩展 + // if (!CommandExecutor && LandmarkSub) + // { + // FString EntityType = LandmarkSub->FindTypeByEntity(Handle); + // CommandExecutor = LandmarkSub->GetTemplateActorByType(EntityType); // 已废弃 + // } + + // 执行指令 + if (CommandExecutor && CommandExecutor->Implements()) + { + IRTSCommandInterface::Execute_ExecuteCommand(CommandExecutor, CommandTag); + } + } + } + } + + RequestCommandRefresh(); +} + +AActor* URTSSelectionSubsystem::GetActiveActor() const +{ + if (SelectedActors.Num() == 0) return nullptr; + if (AvailableGroupKeys.IsValidIndex(CurrentGroupIndex)) + { + const FString& ActiveKey = AvailableGroupKeys[CurrentGroupIndex]; + for (AActor* Actor : SelectedActors) + { + if (Actor && Actor->GetClass()->GetDisplayNameText().ToString() == ActiveKey) return Actor; + } + } + return SelectedActors[0]; } diff --git a/Source/OpenRTSCamera/Private/RTSSelector.cpp b/Source/OpenRTSCamera/Private/RTSSelector.cpp index 7d01817..fdfcda1 100644 --- a/Source/OpenRTSCamera/Private/RTSSelector.cpp +++ b/Source/OpenRTSCamera/Private/RTSSelector.cpp @@ -93,6 +93,15 @@ void URTSSelector::CollectComponentDependencyReferences() { this->PlayerController = PlayerControllerRef; this->HUD = Cast(PlayerControllerRef->GetHUD()); + if (this->HUD) + { + UE_LOG(LogTemp, Warning, TEXT("[RTSSelector] HUD found OK: %s"), *this->HUD->GetClass()->GetName()); + } + else + { + UE_LOG(LogTemp, Error, TEXT("[RTSSelector] HUD cast FAILED! PC HUD class is: %s"), + PlayerControllerRef->GetHUD() ? *PlayerControllerRef->GetHUD()->GetClass()->GetName() : TEXT("NULL")); + } } else { diff --git a/Source/OpenRTSCamera/Private/UI/RTSCommandButtonWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSCommandButtonWidget.cpp new file mode 100644 index 0000000..e28d607 --- /dev/null +++ b/Source/OpenRTSCamera/Private/UI/RTSCommandButtonWidget.cpp @@ -0,0 +1,218 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#include "UI/RTSCommandButtonWidget.h" +#include "UI/RTSTooltipWidget.h" +#include "Components/ProgressBar.h" +#include "Components/TextBlock.h" +#include "Components/Image.h" +#include "Interfaces/RTSCommandInterface.h" +#include "UI/RTSCommanderGridWidget.h" + +void URTSCommandButtonWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + if (MainButton) + { + MainButton->OnClicked.AddDynamic(this, &URTSCommandButtonWidget::HandleClicked); + } +} + +void URTSCommandButtonWidget::Init(URTSCommandButton* InData, AActor* InContext, FKey InOverrideHotkey) +{ + ButtonData = InData; + ContextActor = InContext; + + if (ButtonData) + { + UE_LOG(LogTemp, Log, TEXT("Button Init: %s (Tag: %s)"), *ButtonData->DisplayName.ToString(), *ButtonData->CommandTag.ToString()); + + // Set Icon + if (IconImage) + { + if (ButtonData->Icon) + { + IconImage->SetBrushFromTexture(ButtonData->Icon); + } + IconImage->SetVisibility(ESlateVisibility::HitTestInvisible); + } + + // Set Hotkey Display + if (HotkeyText) + { + FKey TargetKey = InOverrideHotkey.IsValid() ? InOverrideHotkey : ButtonData->Hotkey; + + // Check if key is valid + if (!TargetKey.IsValid()) + { + HotkeyText->SetVisibility(ESlateVisibility::Collapsed); + } + else + { + HotkeyText->SetText(TargetKey.GetDisplayName()); + HotkeyText->SetVisibility(ESlateVisibility::HitTestInvisible); + } + } + + // Reset State + bIsCooldownActive = false; + if (CooldownImage) + { + CooldownImage->SetVisibility(ESlateVisibility::Hidden); + if (!CooldownMaterial) + { + CooldownMaterial = CooldownImage->GetDynamicMaterial(); + } + } + + if (AutoCastBorder) + { + AutoCastBorder->SetVisibility(ESlateVisibility::Hidden); + } + + SetVisibility(ESlateVisibility::Visible); + + // Remove Standard Tooltip to allow shared logic + if (MainButton) + { + MainButton->SetToolTip(nullptr); + if (!MainButton->OnHovered.IsAlreadyBound(this, &URTSCommandButtonWidget::HandleHovered)) + MainButton->OnHovered.AddDynamic(this, &URTSCommandButtonWidget::HandleHovered); + if (!MainButton->OnUnhovered.IsAlreadyBound(this, &URTSCommandButtonWidget::HandleUnhovered)) + MainButton->OnUnhovered.AddDynamic(this, &URTSCommandButtonWidget::HandleUnhovered); + } + } + else + { + // Null data means empty slot + if (MainButton) MainButton->SetToolTip(nullptr); + SetVisibility(ESlateVisibility::Hidden); + } +} + +void URTSCommandButtonWidget::HandleHovered() +{ + // Notify Parent Grid + if (GetOuter()) + { + if (URTSCommanderGridWidget* Grid = GetTypedOuter()) + { + Grid->NotifyButtonHovered(this, ButtonData); + } + } +} + +void URTSCommandButtonWidget::HandleUnhovered() +{ + if (GetOuter()) + { + if (URTSCommanderGridWidget* Grid = GetTypedOuter()) + { + Grid->NotifyButtonUnhovered(this); + } + } +} + +void URTSCommandButtonWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) +{ +Super::NativeTick(MyGeometry, InDeltaTime); + +// Update Availability, Cooldown & AutoCast State from Context +if (ButtonData && ContextActor.IsValid() && ContextActor->Implements()) +{ + // 0. Availability & Visibility logic (Scheme A) + bool bAvailable = IRTSCommandInterface::Execute_IsCommandAvailable(ContextActor.Get(), ButtonData->CommandTag); + + if (!bAvailable) + { + if (ButtonData->bHideIfUnavailable) + { + SetVisibility(ESlateVisibility::Collapsed); + } + else + { + SetVisibility(ESlateVisibility::Visible); + SetIsDisabled(true); + } + } + else + { + SetVisibility(ESlateVisibility::Visible); + SetIsDisabled(false); + } + + // 1. Cooldown Logic + float Remaining = IRTSCommandInterface::Execute_GetCooldownRemaining(ContextActor.Get(), ButtonData->CommandTag); + bool bCurrentlyCooling = Remaining > 0.0f; + + // Edge Detection: Cooldown Started (or Widget just initialized on active CD) + if (bCurrentlyCooling && !bIsCooldownActive) + { + if (CooldownMaterial && ButtonData->DefaultCooldown > 0.1f) + { + // Optional: Send Total Duration if needed for other effects (like shimmer speed) + CooldownMaterial->SetScalarParameterValue(FName("CD_TotalDuration"), ButtonData->DefaultCooldown); + } + + if (CooldownImage) CooldownImage->SetVisibility(ESlateVisibility::HitTestInvisible); + } + // Edge Detection: Cooldown Ended + else if (!bCurrentlyCooling && bIsCooldownActive) + { + if (CooldownImage) CooldownImage->SetVisibility(ESlateVisibility::Hidden); + } + + if (bCurrentlyCooling && CooldownMaterial) + { + // Calculate Phase (0.0 to 1.0) + float Total = FMath::Max(ButtonData->DefaultCooldown, 0.001f); + float Phase = FMath::Clamp(Remaining / Total, 0.0f, 1.0f); + + // Protocol Change: Send normalized "CD_Phase" + CooldownMaterial->SetScalarParameterValue(FName("CD_Phase"), Phase); + } + + bIsCooldownActive = bCurrentlyCooling; + + + // 2. Auto-Cast + if (ButtonData->bAllowAutoCast && AutoCastBorder) + { + bool bEnabled = IRTSCommandInterface::Execute_IsAutoCastEnabled(ContextActor.Get(), ButtonData->CommandTag); + // Flash or Show + AutoCastBorder->SetVisibility(bEnabled ? ESlateVisibility::HitTestInvisible : ESlateVisibility::Hidden); + } + } +} + +FReply URTSCommandButtonWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + // Handle Right Click for Auto-Cast + if (InMouseEvent.GetEffectingButton() == EKeys::RightMouseButton) + { + if (ButtonData && ButtonData->bAllowAutoCast && ContextActor.IsValid() && ContextActor->Implements()) + { + IRTSCommandInterface::Execute_ToggleAutoCast(ContextActor.Get(), ButtonData->CommandTag); + return FReply::Handled(); + } + } + + return Super::NativeOnMouseButtonDown(InGeometry, InMouseEvent); +} + +void URTSCommandButtonWidget::SetIsDisabled(bool bDisabled) +{ + if (MainButton) + { + MainButton->SetIsEnabled(!bDisabled); + } +} + +void URTSCommandButtonWidget::HandleClicked() +{ + if (ButtonData) + { + UE_LOG(LogTemp, Verbose, TEXT("RTSCommandButtonWidget: Clicked %s"), *ButtonData->CommandTag.ToString()); + OnCommandClicked.Broadcast(ButtonData->CommandTag); + } +} diff --git a/Source/OpenRTSCamera/Private/UI/RTSCommanderGridWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSCommanderGridWidget.cpp new file mode 100644 index 0000000..7da27c4 --- /dev/null +++ b/Source/OpenRTSCamera/Private/UI/RTSCommanderGridWidget.cpp @@ -0,0 +1,394 @@ +#include "UI/RTSCommanderGridWidget.h" +#include "Components/UniformGridSlot.h" +#include "RTSSelectionSubsystem.h" +#include "Interfaces/RTSCommandInterface.h" + + +void URTSCommanderGridWidget::NativePreConstruct() +{ + Super::NativePreConstruct(); +} + +void URTSCommanderGridWidget::SynchronizeProperties() +{ + Super::SynchronizeProperties(); + InitGridSlots(); +} + +void URTSCommanderGridWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + InitGridSlots(); + + // 绑定全局通知(模块间解耦的通信枢纽) + if (UWorld* World = GetWorld()) + { + if (ULocalPlayer* LP = World->GetFirstLocalPlayerFromController()) + { + if (URTSSelectionSubsystem* Selection = LP->GetSubsystem()) + { + Selection->OnCommandRefreshRequested.AddUniqueDynamic(this, &URTSCommanderGridWidget::OnActorGridChanged); + Selection->OnCommandNavigationRequested.AddUniqueDynamic(this, &URTSCommanderGridWidget::OnCommandNavigationRequested); + // 绑定选择变化通知,驱动 ActiveActorPtr 更新和 Grid 刷新 + Selection->OnSelectionChanged.AddUniqueDynamic(this, &URTSCommanderGridWidget::OnSelectionUpdated); + } + + // 监听低层级指令系统的导航请求 (二进制导航) + if (URTSCommandSubsystem* SignalHub = LP->GetSubsystem()) + { + SignalHub->OnNavigationRequested.AddLambda([this](URTSCommandGridAsset* NewGrid, AActor* Context) + { + this->UpdateGrid(NewGrid); + }); + } + } + } + + // If Debug Asset is set, load it immediately for testing + if (DebugGridAsset) + { + // RefreshGrid(DebugGridAsset->GetAllButtons()); // Need better logic here for sparse array + } +} + +void URTSCommanderGridWidget::InitGridSlots() +{ + if (!CommandGridPanel) + { + UE_LOG(LogTemp, Warning, TEXT("RTSCommanderGridWidget: CommandGridPanel is NULL!")); + return; + } + + if (!ButtonParams) + { + UE_LOG(LogTemp, Warning, TEXT("RTSCommanderGridWidget: ButtonParams is NULL! Please assign a WBP_CommandButton class in the Widget Blueprint Details.")); + return; + } + + CommandGridPanel->ClearChildren(); + GridButtons.Empty(); + + CommandGridPanel->SetSlotPadding(SlotPadding); + CommandGridPanel->SetMinDesiredSlotWidth(ButtonSize.X); + CommandGridPanel->SetMinDesiredSlotHeight(ButtonSize.Y); + + // Create 15 slots (3 rows x 5 columns) + for (int32 Row = 0; Row < 3; ++Row) + { + for (int32 Col = 0; Col < 5; ++Col) + { + URTSCommandButtonWidget* Btn = CreateWidget(this, ButtonParams); + if (Btn) + { + UUniformGridSlot* GridSlot = CommandGridPanel->AddChildToUniformGrid(Btn, Row, Col); + if (GridSlot) + { + GridSlot->SetHorizontalAlignment(EHorizontalAlignment::HAlign_Fill); + GridSlot->SetVerticalAlignment(EVerticalAlignment::VAlign_Fill); + } + + if (!IsDesignTime()) + { + Btn->OnCommandClicked.AddDynamic(this, &URTSCommanderGridWidget::OnGridButtonClicked); + } + GridButtons.Add(Btn); // Index = Row*5 + Col + } + } + } +} + +void URTSCommanderGridWidget::OnSelectionUpdated(const FRTSSelectionView& View) +{ + Super::OnSelectionUpdated(View); + LastSelectionView = View; + + URTSCommandGridAsset* BaseGrid = nullptr; + + // --- 核心逻辑变更:基于类型(ActiveGroupKey)获取命令面板 --- + if (ULandmarkSubsystem* LandmarkSys = GetWorld()->GetSubsystem()) + { + BaseGrid = LandmarkSys->GetGridByType(View.ActiveGroupKey); + } + + // 如果 Subsystem 没找到映射,尝试从 ActiveActor 兜底(为了兼容非地标单位,如普通士兵) + if (!BaseGrid) + { + if (ULocalPlayer* LP = GetOwningLocalPlayer()) + { + if (URTSSelectionSubsystem* Selection = LP->GetSubsystem()) + { + AActor* ActiveActor = Selection->GetActiveActor(); + if (ActiveActor && ActiveActor->Implements()) + { + BaseGrid = IRTSCommandInterface::Execute_GetCommandGrid(ActiveActor); + } + } + } + } + + // 更新网格(UpdateGrid 内部会处理 BaseGrid 是否变化的逻辑) + UpdateGrid(BaseGrid); +} + +void URTSCommanderGridWidget::UpdateGrid(URTSCommandGridAsset* NewGrid) +{ + // 如果是 NULL,即执行 Reset 操作 + CurrentGridAsset = NewGrid; + + TArray SparseList; + SparseList.Init(nullptr, 15); + if (NewGrid) + { + PopulateSparseButtons(NewGrid, SparseList); + } + + RefreshGrid(SparseList); + + if (NewGrid) + { + UE_LOG(LogTemp, Log, TEXT("UI-Grid: Set Grid Asset: %s"), *NewGrid->GetName()); + } + else + { + UE_LOG(LogTemp, Log, TEXT("UI-Grid: Grid Reset (Set NULL)")); + } +} + +void URTSCommanderGridWidget::RefreshVisuals() +{ + if (!CurrentGridAsset.IsValid()) return; + + const FKey GridKeys[] = { + EKeys::Q, EKeys::W, EKeys::E, EKeys::R, EKeys::T, + EKeys::A, EKeys::S, EKeys::D, EKeys::F, EKeys::G, + EKeys::Z, EKeys::X, EKeys::C, EKeys::V, EKeys::B + }; + + TArray SparseList; + PopulateSparseButtons(CurrentGridAsset.Get(), SparseList); + + for (int32 i = 0; i < 15; ++i) + { + if (GridButtons.IsValidIndex(i) && GridButtons[i]) + { + // 增量刷新时必须保留布局决定的快捷键(Q/W/E),否则会被重置为 None + GridButtons[i]->Init(SparseList[i], ActiveActorPtr.Get(), GridKeys[i]); + } + } + UE_LOG(LogTemp, Verbose, TEXT("UI-Grid: Visuals Refreshed.")); +} + +void URTSCommanderGridWidget::PopulateSparseButtons(URTSCommandGridAsset* Grid, TArray& OutButtons) +{ + if (!Grid) return; + OutButtons.Init(nullptr, 15); + + // 1. 获取所有按钮(支持虚函数重写,覆盖了单例面板和普通资产面板) + TArray AllButtons = Grid->GetAllButtons(); + + // 2. 先尝试放入 PreferredIndex 位置 + TArray Untracked; + for (URTSCommandButton* Btn : AllButtons) + { + if (!Btn) continue; + + int32 Idx = Btn->PreferredIndex; + if (Idx >= 0 && Idx < 15 && OutButtons[Idx] == nullptr) + { + OutButtons[Idx] = Btn; + } + else + { + Untracked.Add(Btn); + } + } + + // 3. 将没有固定位置(或位置冲突)的按钮放入空位 + int32 StartSearch = 0; + for (URTSCommandButton* Btn : Untracked) + { + for (int32 i = StartSearch; i < 15; ++i) + { + if (OutButtons[i] == nullptr) + { + OutButtons[i] = Btn; + break; + } + } + } +} + +void URTSCommanderGridWidget::RefreshGrid(const TArray& Buttons) +{ + if (Buttons.Num() != 15) return; + + const FKey GridKeys[] = { + EKeys::Q, EKeys::W, EKeys::E, EKeys::R, EKeys::T, + EKeys::A, EKeys::S, EKeys::D, EKeys::F, EKeys::G, + EKeys::Z, EKeys::X, EKeys::C, EKeys::V, EKeys::B + }; + + for (int32 i = 0; i < 15; ++i) + { + if (GridButtons.IsValidIndex(i) && GridButtons[i]) + { + GridButtons[i]->Init(Buttons[i], ActiveActorPtr.Get(), GridKeys[i]); + } + } +} + +void URTSCommanderGridWidget::OnActorGridChanged() +{ + // 该函数现在映射为 RefreshVisuals 以保持向后兼容 + RefreshVisuals(); +} + +void URTSCommanderGridWidget::OnCommandNavigationRequested(URTSCommandGridAsset* NewGrid) +{ + // 直接从 Subsystem 拿当前激活 Actor,不走 ActiveActorPtr 中间状态 + if (ULocalPlayer* LP = GetOwningLocalPlayer()) + { + if (URTSSelectionSubsystem* Selection = LP->GetSubsystem()) + { + ActiveActorPtr = Selection->GetActiveActor(); + } + } + UE_LOG(LogTemp, Warning, TEXT("[Grid] Widget recv Navigation: Grid=%s Actor=%s"), + NewGrid ? *NewGrid->GetName() : TEXT("NULL"), + ActiveActorPtr.IsValid() ? *ActiveActorPtr->GetName() : TEXT("NULL")); + UpdateGrid(NewGrid); +} + +#include "Data/RTSCmd_SubMenu.h" + +void URTSCommanderGridWidget::OnGridButtonClicked(const FGameplayTag& CommandTag) +{ + // 二进制核心:直接执行 + // 理由:虽然 UI 代理传回的是 Tag,但我们立即将其还原回 Button 对象, + // 以便执行其包含完整 C++ 逻辑的回调函数(Execute),彻底废除“Actor 查找”链路。 + URTSCommandButton* ClickedData = nullptr; + for (URTSCommandButtonWidget* BtnWidget : GridButtons) + { + if (BtnWidget && BtnWidget->GetVisibility() == ESlateVisibility::Visible) + { + if (URTSCommandButton* Data = BtnWidget->GetData()) + { + if (Data->CommandTag.MatchesTagExact(CommandTag)) + { + ClickedData = Data; + break; + } + } + } + } + + if (!ClickedData) return; + + if (ULocalPlayer* LP = GetOwningLocalPlayer()) + { + if (URTSSelectionSubsystem* Selection = LP->GetSubsystem()) + { + // 对于非针对单位的逻辑(如顾问、科技),Actor 指针可能为空, + // 但对于“兴奋剂”等单位技能,我们需要传入正确的执行者。 + AActor* ActiveActor = Selection->GetActiveActor(); + + // 战术直达:按钮逻辑自决 (Pure Callback) + // 基础按钮会发 Tag 给 Actor,子菜单按钮会命令 UI 导航。 + ClickedData->Execute(ActiveActor); + } + } +} + +// --- Shared Tooltip Implementation --- + +void URTSCommanderGridWidget::NotifyButtonHovered(URTSCommandButtonWidget* Btn, URTSCommandButton* Data) +{ + if (!Data) return; + + // Lazy Create + if (!SharedTooltip && TooltipClass) + { + SharedTooltip = CreateWidget(GetOwningPlayer(), TooltipClass); + if (SharedTooltip) + { + SharedTooltip->AddToViewport(100); // High Z-Order + SharedTooltip->SetVisibility(ESlateVisibility::Collapsed); + UE_LOG(LogTemp, Log, TEXT("Shared Tooltip Created.")); + } + else + { + UE_LOG(LogTemp, Warning, TEXT("Failed to create Shared Tooltip! Check TooltipClass is valid.")); + } + } + else if (!TooltipClass) + { + UE_LOG(LogTemp, Warning, TEXT("TooltipClass is NULL in RTSCommanderGridWidget! Please assign WBP_Tooltip.")); + } + + if (SharedTooltip) + { + SharedTooltip->UpdateTooltip(Data); + SharedTooltip->SetVisibility(ESlateVisibility::SelfHitTestInvisible); + UE_LOG(LogTemp, Verbose, TEXT("Showing Tooltip for: %s"), *Data->DisplayName.ToString()); + } +} + +void URTSCommanderGridWidget::NotifyButtonUnhovered(URTSCommandButtonWidget* Btn) +{ + if (SharedTooltip) + { + SharedTooltip->SetVisibility(ESlateVisibility::Collapsed); + } +} + +void URTSCommanderGridWidget::NativeTick(const FGeometry& MyGeometry, float InDeltaTime) +{ + Super::NativeTick(MyGeometry, InDeltaTime); + + if (SharedTooltip && SharedTooltip->GetVisibility() == ESlateVisibility::SelfHitTestInvisible) + { + if (bFixedTooltipAboveGrid) + { + // Position it above the CommandGridPanel + // Use the geometry of this widget to find the screen pos + FVector2D GridPos = MyGeometry.GetAbsolutePosition(); + + FVector2D TooltipSize = SharedTooltip->GetDesiredSize(); + if (TooltipSize.IsZero()) TooltipSize = FVector2D(400.0f, 250.0f); + + // Left-Align with the Grid (Standard RTS Style) + FVector2D FinalPos; + FinalPos.X = GridPos.X; + FinalPos.Y = GridPos.Y - TooltipSize.Y + TooltipYOffset; + + SharedTooltip->SetPositionInViewport(FinalPos); + } + else + { + // Follow Mouse Logic (Existing) + FVector2D MousePos; + if (GetOwningPlayer() && GetOwningPlayer()->GetMousePosition(MousePos.X, MousePos.Y)) + { + FVector2D ViewportSize; + if (GEngine && GEngine->GameViewport) + { + GEngine->GameViewport->GetViewportSize(ViewportSize); + } + + FVector2D TooltipSize = SharedTooltip->GetDesiredSize(); + if (TooltipSize.IsZero()) TooltipSize = FVector2D(400.0f, 300.0f); + + FVector2D FinalPos = MousePos; + + if (MousePos.X > ViewportSize.X * 0.5f) FinalPos.X -= TooltipSize.X + 10.0f; + else FinalPos.X += 40.0f; + + if (MousePos.Y > ViewportSize.Y * 0.5f) FinalPos.Y -= TooltipSize.Y + 10.0f; + else FinalPos.Y += 40.0f; + + SharedTooltip->SetPositionInViewport(FinalPos); + } + } + } +} diff --git a/Source/OpenRTSCamera/Private/UI/RTSTooltipWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSTooltipWidget.cpp new file mode 100644 index 0000000..fb420e9 --- /dev/null +++ b/Source/OpenRTSCamera/Private/UI/RTSTooltipWidget.cpp @@ -0,0 +1,100 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#include "UI/RTSTooltipWidget.h" +#include "Components/TextBlock.h" +#include "Components/RichTextBlock.h" +#include "Components/Image.h" + +void URTSTooltipWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + // Enforce Default Style if user hasn't customized it heavily + // Note: FSlateFontInfo is needed. We'll just set Size for now, keeping typeface default. + + if (TitleText) + { + FSlateFontInfo Font = TitleText->GetFont(); + Font.Size = DefaultFontSize; + TitleText->SetFont(Font); + } + + // RichText doesn't use simple "SetFont", it uses a TextStyleSet/DataTable. + // But we can try setting the "DefaultTextStyle" + if (DescriptionText) + { + // For RichText, dynamic style modification is harder without a tailored Decorator. + // However, we can set the "DefaultTextStyle" font size if exposed, + // or we just trust the user to set it in UMG since RichText is complex. + + // Let's at least try to set the default text style if possible + // FTextBlockStyle DefaultStyle = DescriptionText->GetDefaultTextStyle(); + // DefaultStyle.Font.Size = DefaultFontSize; + // DescriptionText->SetDefaultTextStyle(DefaultStyle); + + // Actually, easiest way is to wrap it in a ScaleBox if we just want size, + // but let's leave RichText alone for now as it relies on DataTables (GlobalStyle). + // User asked for "Roboto 32". + } +} + +void URTSTooltipWidget::UpdateTooltip(URTSCommandButton* Data) +{ + if (!Data) return; + + if (TitleText) + { + TitleText->SetText(Data->DisplayName); + + // Re-apply font size in case it reset (unlikely but safe) + FSlateFontInfo Font = TitleText->GetFont(); + Font.Size = DefaultFontSize; + TitleText->SetFont(Font); + } + + if (DescriptionText) + { + DescriptionText->SetText(Data->Description); + } + + if (IconImage) + { + if (Data->Icon) + { + IconImage->SetBrushFromTexture(Data->Icon); + IconImage->SetVisibility(ESlateVisibility::HitTestInvisible); + } + else + { + IconImage->SetVisibility(ESlateVisibility::Collapsed); + } + } + + // Cost logic + if (CostText) + { + FString CostStr; + if (Data->LowValueCost > 0) + { + CostStr += FString::Printf(TEXT("%d 资金"), Data->LowValueCost); + } + if (Data->HighValueCost > 0) + { + if (!CostStr.IsEmpty()) CostStr += TEXT(" / "); // Separator + CostStr += FString::Printf(TEXT("%d 军需"), Data->HighValueCost); + } + + CostText->SetText(FText::FromString(CostStr)); + + // Hide if free + CostText->SetVisibility(CostStr.IsEmpty() ? ESlateVisibility::Collapsed : ESlateVisibility::Visible); + + // Force font? Or rely on UMG + // FSlateFontInfo CostFont = CostText->GetFont(); + // CostFont.Size = 24; // Smaller than title + // CostText->SetFont(CostFont); + } + + // Logic for Cost/Cooldown/etc. would go here if we had that data in DataAsset + // For now we just show title and desc. +} diff --git a/Source/OpenRTSCamera/Public/RTSSelectionStructs.h b/Source/OpenRTSCamera/Public/RTSSelectionStructs.h index 694cd58..5a258fb 100644 --- a/Source/OpenRTSCamera/Public/RTSSelectionStructs.h +++ b/Source/OpenRTSCamera/Public/RTSSelectionStructs.h @@ -66,6 +66,7 @@ struct FRTSUnitData UPROPERTY() FEntityHandle EntityHandle; + // Default constructor for "Empty/Unknown" state FRTSUnitData() { @@ -84,6 +85,7 @@ struct FRTSSelectionView UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") ERTSSelectionMode Mode = ERTSSelectionMode::Single; + // Used when Mode == Single. Contains detailed info. UPROPERTY(BlueprintReadOnly, Category = "RTS Selection") FRTSUnitData SingleUnit; diff --git a/Source/OpenRTSCamera/Public/RTSSelectionSubsystem.h b/Source/OpenRTSCamera/Public/RTSSelectionSubsystem.h index c221891..632344d 100644 --- a/Source/OpenRTSCamera/Public/RTSSelectionSubsystem.h +++ b/Source/OpenRTSCamera/Public/RTSSelectionSubsystem.h @@ -9,7 +9,11 @@ #include "MassAPIStructs.h" #include "RTSSelectionSubsystem.generated.h" +DECLARE_LOG_CATEGORY_EXTERN(LogORTSSelection, Log, All); + DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnSelectionChanged, const FRTSSelectionView&, SelectionView); +DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnCommandRefreshRequested); +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCommandNavigationRequested, class URTSCommandGridAsset*, NewGrid); /** * Manages RTS selection state and formats data for the UI. @@ -20,14 +24,23 @@ class OPENRTSCAMERA_API URTSSelectionSubsystem : public ULocalPlayerSubsystem GENERATED_BODY() public: + /** 广播给 UI,请求刷新当前的指令网格(当单位内部状态改变时,如 CD 结束) */ + UPROPERTY(BlueprintAssignable, Category = "RTS Selection") + FOnCommandRefreshRequested OnCommandRefreshRequested; + + /** 广播给 UI,请求导航到一个特定的指令网格(瞬态导航,如打开子菜单) */ + UPROPERTY(BlueprintAssignable, Category = "RTS Selection") + FOnCommandNavigationRequested OnCommandNavigationRequested; + + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void RequestCommandRefresh() { OnCommandRefreshRequested.Broadcast(); } + + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void RequestGridNavigation(class URTSCommandGridAsset* NewGrid) { OnCommandNavigationRequested.Broadcast(NewGrid); } // Subsystem interface virtual void Initialize(FSubsystemCollectionBase& Collection) override; virtual void Deinitialize() override; - /** - * Main entry point for selection updates. - * Calculates the view mode (Single/List/Summary) and broadcasts OnSelectionChanged. - */ UFUNCTION(BlueprintCallable, Category = "RTS Selection") void SetSelectedUnits(const TArray& InActors, const TArray& InEntities, ERTSSelectionModifier Modifier = ERTSSelectionModifier::Replace); @@ -37,6 +50,12 @@ class OPENRTSCAMERA_API URTSSelectionSubsystem : public ULocalPlayerSubsystem UFUNCTION(BlueprintCallable, Category = "RTS Selection") void ClearSelection(); + /** + * Fallback Command Grid used when an entity (like a soldier) does not have its own grid. + */ + UPROPERTY(EditAnywhere, Category = "RTS Selection") + TSoftObjectPtr DefaultEntityGrid; + /** * Cycles focus to the next available sub-group. * (Tab functionality) @@ -58,6 +77,12 @@ class OPENRTSCAMERA_API URTSSelectionSubsystem : public ULocalPlayerSubsystem UFUNCTION(BlueprintCallable, Category = "RTS Selection") void SelectGroup(const FString& GroupKey); + /** + * Issues an instant command to all selected units. + */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + void IssueCommand(FGameplayTag CommandTag); + UFUNCTION(BlueprintCallable, Category = "RTS Selection") bool HasSelectedActors() const { return SelectedActors.Num() > 0; } @@ -70,6 +95,17 @@ class OPENRTSCAMERA_API URTSSelectionSubsystem : public ULocalPlayerSubsystem UFUNCTION(BlueprintCallable, Category = "RTS Selection") bool IsEntitySelected(const FEntityHandle& Handle) const { return SelectedEntities.Contains(Handle); } + /** 返回当前选中的 Actor 列表 (只读访问) */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + const TArray& GetSelectedActors() const { return SelectedActors; } + + /** + * 获取当前“激活”的 Actor (即当前选中组的代表) + * 在 Direct Callback 模式下,它作为按钮执行的主要上下文。 + */ + UFUNCTION(BlueprintCallable, Category = "RTS Selection") + AActor* GetActiveActor() const; + /** * Event fired when selection changes. UI should bind to this. */ @@ -87,6 +123,9 @@ class OPENRTSCAMERA_API URTSSelectionSubsystem : public ULocalPlayerSubsystem UPROPERTY() TArray AvailableGroupKeys; + UPROPERTY() + TObjectPtr DefaultGridNative; + int32 CurrentGroupIndex = 0; // Helpers diff --git a/Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h b/Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h index 7c146f1..9f008f6 100644 --- a/Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h +++ b/Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h @@ -23,7 +23,7 @@ class OPENRTSCAMERA_API URTSActiveGroupWidget : public UUserWidget virtual void NativeConstruct() override; UFUNCTION() - void OnSelectionUpdated(const FRTSSelectionView& View); + virtual void OnSelectionUpdated(const FRTSSelectionView& View); // Optional: If bound, we forward the data to this internal widget. // This allows users to wrap our logic in a text/border/etc. diff --git a/Source/OpenRTSCamera/Public/UI/RTSCommandButtonWidget.h b/Source/OpenRTSCamera/Public/UI/RTSCommandButtonWidget.h new file mode 100644 index 0000000..5be811d --- /dev/null +++ b/Source/OpenRTSCamera/Public/UI/RTSCommandButtonWidget.h @@ -0,0 +1,89 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "Data/RTSCommandButton.h" +#include "Components/Button.h" +#include "Components/Image.h" +#include "RTSCommandButtonWidget.generated.h" + +DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCommandButtonClicked, const FGameplayTag&, CommandTag); + +/** + * A Single Button in the Command Grid. + * Displays Icon, handles clicks, shows tooltip. + */ +UCLASS() +class OPENRTSCAMERA_API URTSCommandButtonWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + + virtual void NativeConstruct() override; + virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; + virtual FReply NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) override; + + UFUNCTION(BlueprintCallable, Category = "RTS Command") + void Init(URTSCommandButton* InData, AActor* InContext = nullptr, FKey InOverrideHotkey = FKey()); + + /** Returns the underlying data asset for this button. */ + UFUNCTION(BlueprintCallable, Category = "RTS Command") + URTSCommandButton* GetData() const { return ButtonData; } + + UFUNCTION(BlueprintCallable, Category = "RTS Command") + void SetIsDisabled(bool bDisabled); + + // Event for click + UPROPERTY(BlueprintAssignable, Category = "RTS Command") + FOnCommandButtonClicked OnCommandClicked; + +protected: + + UPROPERTY(meta = (BindWidget)) + TObjectPtr MainButton; + + UPROPERTY(meta = (BindWidget)) + TObjectPtr IconImage; + + // Cooldown Overlay (Image with Dynamic Material) + UPROPERTY(meta = (BindWidget)) + TObjectPtr CooldownImage; + + // Hotkey Display + UPROPERTY(meta = (BindWidget)) + TObjectPtr HotkeyText; + + // Auto-Cast Border (Image or Border) + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr AutoCastBorder; + + // The data asset backing this button + UPROPERTY() + TObjectPtr ButtonData; + + UPROPERTY() + TObjectPtr CooldownMaterial; + + // State tracking for efficient updates + bool bIsCooldownActive = false; + + // The context actor (to query state) + UPROPERTY() + TWeakObjectPtr ContextActor; + + // The class to use for tooltips - MOVED TO GRID + // UPROPERTY(EditAnywhere, Category = "UI") + // TSubclassOf TooltipWidgetClass; + + UFUNCTION() + void HandleClicked(); + + UFUNCTION() + void HandleHovered(); + + UFUNCTION() + void HandleUnhovered(); +}; diff --git a/Source/OpenRTSCamera/Public/UI/RTSCommanderGridWidget.h b/Source/OpenRTSCamera/Public/UI/RTSCommanderGridWidget.h new file mode 100644 index 0000000..2147767 --- /dev/null +++ b/Source/OpenRTSCamera/Public/UI/RTSCommanderGridWidget.h @@ -0,0 +1,121 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "Components/UniformGridPanel.h" +#include "Data/RTSCommandGridAsset.h" +#include "RTSCommandButtonWidget.h" +#include "RTSActiveGroupWidget.h" +#include "RTSCommanderGridWidget.generated.h" + +/** + * The 3x5 Grid Container. + * Manages 15 RTSCommandButtonWidgets. + */ +UCLASS() +class OPENRTSCAMERA_API URTSCommanderGridWidget : public URTSActiveGroupWidget +{ + GENERATED_BODY() + +public: + + virtual void NativeConstruct() override; + virtual void NativePreConstruct() override; + virtual void SynchronizeProperties() override; + + // Override to update grid when active group changes + virtual void OnSelectionUpdated(const FRTSSelectionView& View) override; + +protected: + + // The Grid Panel to hold buttons + UPROPERTY(meta = (BindWidget)) + TObjectPtr CommandGridPanel; + + // Class to spawn for each button. + // MUST be set to a valid WBP in the details panel for the grid to appear. + UPROPERTY(EditAnywhere, Category = "RTS Grid") + TSubclassOf ButtonParams; + + // Padding between buttons + UPROPERTY(EditAnywhere, Category = "RTS Grid") + FMargin SlotPadding = FMargin(4.0f); + + // Desired size for buttons (if enforced by logic, though usually WBP controls this) + UPROPERTY(EditAnywhere, Category = "RTS Grid") + FVector2D ButtonSize = FVector2D(128.0f, 128.0f); + + // Internal list of buttons (Keys = Index 0-14) + UPROPERTY() + TArray> GridButtons; + + // Populate the grid based on data + void RefreshGrid(const TArray& Buttons); + + // Generate the 15 empty slots on Init + void InitGridSlots(); + + // 统一网格填充逻辑(支持 PreferredIndex 与自动空位) + void PopulateSparseButtons(URTSCommandGridAsset* Grid, TArray& OutButtons); + + UFUNCTION() + void OnGridButtonClicked(const FGameplayTag& CommandTag); + + /** 响应 Actor 内部触发的网格变更通知 */ + UFUNCTION() + void OnActorGridChanged(); + + /** 响应全局导航请求(瞬态子菜单) */ + UFUNCTION() + void OnCommandNavigationRequested(URTSCommandGridAsset* NewGrid); + + // Command resolver: Find asset for given unit ID + // For now, this will just use a hardcoded reference or basic logic until we add the Interface/Trait + URTSCommandGridAsset* ResolveGridForUnit(const FString& UnitName); + + // 纯 Push (Set/Reset) 模型辅助函数 + UFUNCTION(BlueprintCallable, Category = "RTS Grid") + void UpdateGrid(URTSCommandGridAsset* NewGrid); + + UFUNCTION(BlueprintCallable, Category = "RTS Grid") + void RefreshVisuals(); + + // Cache the active actor for context + TWeakObjectPtr ActiveActorPtr; + + /** 当前正在显示的网格资产 (托管状态) */ + UPROPERTY() + TWeakObjectPtr CurrentGridAsset; + + // 保存当前的视图数据,以便在点击子网格后刷新时复用 + FRTSSelectionView LastSelectionView; + + // Test Asset for debugging + UPROPERTY(EditAnywhere, Category = "Debug") + TObjectPtr DebugGridAsset; + + // --- Shared Tooltip Logic --- +protected: + UPROPERTY(EditAnywhere, Category = "UI") + TSubclassOf TooltipClass; + + // If true, tooltip stays at a fixed offset from the Grid instead of following mouse + UPROPERTY(EditAnywhere, Category = "UI") + bool bFixedTooltipAboveGrid = true; + + // Y-Offset from Grid top when bFixedTooltipAboveGrid is true + UPROPERTY(EditAnywhere, Category = "UI") + float TooltipYOffset = -20.0f; + + UPROPERTY() + TObjectPtr SharedTooltip; + +public: + // Called by child buttons + void NotifyButtonHovered(URTSCommandButtonWidget* Btn, URTSCommandButton* Data); + void NotifyButtonUnhovered(URTSCommandButtonWidget* Btn); + + virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override; +}; diff --git a/Source/OpenRTSCamera/Public/UI/RTSTooltipWidget.h b/Source/OpenRTSCamera/Public/UI/RTSTooltipWidget.h new file mode 100644 index 0000000..a080dd0 --- /dev/null +++ b/Source/OpenRTSCamera/Public/UI/RTSTooltipWidget.h @@ -0,0 +1,49 @@ +// Copyright 2024 Winy unq All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Blueprint/UserWidget.h" +#include "Data/RTSCommandButton.h" +#include "RTSTooltipWidget.generated.h" + +class UTextBlock; +class UImage; + +/** + * A Rich Tooltip for RTS Commands. + * Displays Name, Description, Cost, Cooldown. + */ +UCLASS() +class OPENRTSCAMERA_API URTSTooltipWidget : public UUserWidget +{ + GENERATED_BODY() + +public: + + // Update Tooltip UI from Data + UFUNCTION(BlueprintCallable, Category = "RTS Tooltip") + void UpdateTooltip(URTSCommandButton* Data); + +protected: + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr TitleText; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr DescriptionText; + + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr CostText; // "100 M / 50 G" + + // Optional icon + UPROPERTY(meta = (BindWidgetOptional)) + TObjectPtr IconImage; + + // --- Style Config (Start) --- + // User requested "Roboto 32". We can force this here or just expose it. + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Style") + int32 DefaultFontSize = 32; + + virtual void NativeConstruct() override; +};