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/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 27e74e4..545c8ad 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.3.0", "FileVersion": 3, "FriendlyName": "OpenRTSCamera", "Installed": false, @@ -27,6 +26,14 @@ { "Name": "EnhancedInput", "Enabled": true + }, + { + "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 1d91624..c44d555 100644 --- a/Source/OpenRTSCamera/OpenRTSCamera.Build.cs +++ b/Source/OpenRTSCamera/OpenRTSCamera.Build.cs @@ -35,7 +35,15 @@ public OpenRTSCamera(ReadOnlyTargetRules Target) : base(Target) "EnhancedInput", "Slate", "SlateCore", - "UMG" + "UMG", + "InputCore", + "MassBattleMinimap", + "MassBattle", + "MassAPI", + "MassEntity", + "LandmarkSystem", + "GameplayTags", + "RTSCommandSystem" } ); 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 e5606c8..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,566 +18,736 @@ URTSCamera::URTSCamera() { +// ... (previous lines remain unchanged) + /// 设置组件基本生存期属性 PrimaryComponentTick.bCanEverTick = true; - 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; - this->EnableDynamicCameraHeight = true; - this->EnableEdgeScrolling = true; - this->FindGroundTraceLength = 100000; - this->MaximumZoomLength = 5000; - this->MinimumZoomLength = 500; - this->MoveSpeed = 50; - 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")); - - 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->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; + 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")); + 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(); - if (NetMode != NM_DedicatedServer && this->PlayerController->GetViewTarget() == this->Owner) + const auto netMode = this->GetNetMode(); + + /// 仅在大客户端中处理相机位置插值与状态同步 + 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->handleTargetArmLengthInterpolation(); + this->updateFollowPositionIfTargetActive(); + this->applyBoundaryConstraints(); } } -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 speedAlpha = (this->desiredZoomLength - this->minimumZoomLength) / (this->maximumZoomLength - this->minimumZoomLength); + this->currentMovementSpeed = FMath::Lerp(this->minMovementSpeed, this->maxMovementSpeed, speedAlpha); + + /// 缩放意图发生的瞬间即时投射視野框,确保 UI 的战略响应无延迟 + this->updateMinimapFrustum(); } -void URTSCamera::OnRotateCamera(const FInputActionValue& Value) +void URTSCamera::onRotateCameraActionTriggered(const FInputActionValue& value) { - const auto WorldRotation = this->Root->GetComponentRotation(); - this->Root->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(); } -void URTSCamera::OnTurnCameraLeft(const FInputActionValue&) +void URTSCamera::onTurnCameraLeftActionTriggered(const FInputActionValue&) { - const auto WorldRotation = this->Root->GetRelativeRotation(); - this->Root->SetRelativeRotation( + /// 向左执行定量的步进式偏转 + const FRotator relativeRotation = this->rootComponent->GetRelativeRotation(); + this->rootComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( - WorldRotation.Euler().X, - WorldRotation.Euler().Y, - WorldRotation.Euler().Z - this->RotateSpeed + relativeRotation.Euler().X, + relativeRotation.Euler().Y, + relativeRotation.Euler().Z - this->rotationSpeed ) ) ); + this->updateMinimapFrustum(); } -void URTSCamera::OnTurnCameraRight(const FInputActionValue&) +void URTSCamera::onTurnCameraRightActionTriggered(const FInputActionValue&) { - const auto WorldRotation = this->Root->GetRelativeRotation(); - this->Root->SetRelativeRotation( + /// 向右执行定量的步进式偏转 + const FRotator relativeRotation = this->rootComponent->GetRelativeRotation(); + this->rootComponent->SetRelativeRotation( FRotator::MakeFromEuler( FVector( - WorldRotation.Euler().X, - WorldRotation.Euler().Y, - WorldRotation.Euler().Z + this->RotateSpeed + relativeRotation.Euler().X, + relativeRotation.Euler().Y, + relativeRotation.Euler().Z + this->rotationSpeed ) ) ); + this->updateMinimapFrustum(); } -void URTSCamera::OnMoveCameraYAxis(const FInputActionValue& Value) +void URTSCamera::onMoveCameraYAxisActionTriggered(const FInputActionValue& value) { - this->RequestMoveCamera( - this->SpringArm->GetForwardVector().X, - this->SpringArm->GetForwardVector().Y, - Value.Get() + /// 处理纵向平移请求 + this->requestCameraMovement( + this->rootComponent->GetForwardVector().X, + this->rootComponent->GetForwardVector().Y, + value.Get() ); } -void URTSCamera::OnMoveCameraXAxis(const FInputActionValue& Value) +void URTSCamera::onMoveCameraXAxisActionTriggered(const FInputActionValue& value) { - this->RequestMoveCamera( - this->SpringArm->GetRightVector().X, - this->SpringArm->GetRightVector().Y, - Value.Get() + /// 处理横向平移请求 + this->requestCameraMovement( + this->rootComponent->GetRightVector().X, + this->rootComponent->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->SpringArm->GetRightVector().X, - this->SpringArm->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->rootComponent->GetRightVector().X, + this->rootComponent->GetRightVector().Y, + dragDelta.X ); - this->RequestMoveCamera( - this->SpringArm->GetForwardVector().X, - this->SpringArm->GetForwardVector().Y, - Delta.Y * -1 + this->requestCameraMovement( + this->rootComponent->GetForwardVector().X, + this->rootComponent->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 xAxisValue, const float yAxisValue, const float movementScale) { - FMoveCameraCommand MoveCameraCommand; - MoveCameraCommand.X = X; - MoveCameraCommand.Y = Y; - MoveCameraCommand.Scale = Scale; - MoveCameraCommands.Push(MoveCameraCommand); + /// 将运动请求压入队列,供 Tick 阶段统一消化 + FMoveCameraCommand movementCommand; + movementCommand.xAxisValue = xAxisValue; + movementCommand.yAxisValue = yAxisValue; + movementCommand.movementScale = movementScale; + this->pendingMovementCommands.Push(movementCommand); } -void URTSCamera::ApplyMoveCameraCommands() +void URTSCamera::applyAccumulatedMovementCommands() { - for (const auto& [X, Y, Scale] : this->MoveCameraCommands) + /// 执行帧内所有挂起的平移指令并清空 + for (const auto& [xAxisValue, yAxisValue, movementScale] : this->pendingMovementCommands) { - auto Movement = FVector2D(X, Y); - Movement.Normalize(); - Movement *= this->MoveSpeed * Scale * this->DeltaSeconds; - this->Root->SetWorldLocation( - this->Root->GetComponentLocation() + FVector(Movement.X, Movement.Y, 0.0f) + FVector2D directionVector(xAxisValue, yAxisValue); + directionVector.Normalize(); + directionVector *= this->currentMovementSpeed * movementScale * this->deltaSeconds; + + 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->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->realTimeStrategyPlayerController = UGameplayStatics::GetPlayerController(this->GetWorld(), 0); } -void URTSCamera::ConfigureSpringArm() +void URTSCamera::setupInitialSpringArmState() { - this->DesiredZoomLength = this->MaximumZoomLength; - this->SpringArm->TargetArmLength = this->DesiredZoomLength; - this->SpringArm->bDoCollisionTest = false; - this->SpringArm->bEnableCameraLag = this->EnableCameraLag; - this->SpringArm->bEnableCameraRotationLag = this->EnableCameraRotationLag; - this->SpringArm->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 ) ) ); + + 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::TryToFindBoundaryVolumeReference() +void URTSCamera::locateMapBoundaryVolumeByTag() { - TArray BlockingVolumes; - UGameplayStatics::GetAllActorsOfClassWithTag( + /// 直接在世界中通过类类型检索 AMinimapRegion + TArray foundActors; + UGameplayStatics::GetAllActorsOfClass( this->GetWorld(), - AActor::StaticClass(), - this->CameraBlockingVolumeTag, - BlockingVolumes + AMinimapRegion::StaticClass(), + foundActors ); - if (BlockingVolumes.Num() > 0) + if (foundActors.Num() > 0) { - this->BoundaryVolume = BlockingVolumes[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(); } } -void URTSCamera::ConditionallyEnableEdgeScrolling() const +void URTSCamera::configureInputModeForEdgeScrolling() { - if (this->EnableEdgeScrolling) + /// 当鼠标用于边缘滚动时,强制应用视口锁定策略 + if (this->enableEdgeScrolling) { - FInputModeGameAndUI InputMode; - InputMode.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways); - InputMode.SetHideCursorDuringCapture(false); - this->PlayerController->SetInputMode(InputMode); + FInputModeGameAndUI gameModeSettings; + gameModeSettings.SetLockMouseToViewportBehavior(EMouseLockMode::LockAlways); + gameModeSettings.SetHideCursorDuringCapture(false); + this->realTimeStrategyPlayerController->SetInputMode(gameModeSettings); } } -void URTSCamera::CheckForEnhancedInputComponent() const +void URTSCamera::validateEnhancedInputAvailability() { - if (Cast(this->PlayerController->InputComponent) == nullptr) + /// 校验当前的 InputComponent 是否兼容 Enhanced Input 语法 + if (Cast(this->realTimeStrategyPlayerController->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("Warning: RTSCamera requires Enhanced Input Component! Check Project Settings."), true, true, FLinearColor::Red, 100 ); } } -void URTSCamera::BindInputMappingContext() const +void URTSCamera::registerInputMappingContext() { - if (PlayerController && PlayerController->GetLocalPlayer()) + /// 向玩家输入子系统注册相机专用的映射上下文 + if (this->realTimeStrategyPlayerController && this->realTimeStrategyPlayerController->GetLocalPlayer()) { - if (const auto Input = PlayerController->GetLocalPlayer()->GetSubsystem()) + if (const auto inputSystem = this->realTimeStrategyPlayerController->GetLocalPlayer()->GetSubsystem()) { - PlayerController->bShowMouseCursor = true; + this->realTimeStrategyPlayerController->bShowMouseCursor = true; - // Check if the context is already bound to prevent double binding - 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->PlayerController->InputComponent)) + /// 执行运动指令与 C++ 响应函数的逻辑挂挂接 + if (const auto enhancedInputComponent = 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 - ); + 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); } } -void URTSCamera::SetActiveCamera() const +void URTSCamera::setActiveCamera() { - this->PlayerController->SetViewTarget(this->GetOwner()); + /// 将玩家当前的渲染视角强制聚焦于此组件 + this->realTimeStrategyPlayerController->SetViewTarget(this->GetOwner()); } -void URTSCamera::JumpTo(const FVector Position) const +void URTSCamera::jumpTo(const FVector position) { - this->Root->SetWorldLocation(Position); + /// 执行瞬时的视变换同步,并触发视野投影点手动刷新 + float cachedZ = this->rootComponent->GetComponentLocation().Z; + this->rootComponent->SetWorldLocation(FVector(position.X, position.Y, cachedZ)); + this->applyBoundaryConstraints(); + this->updateMinimapFrustum(); } -void URTSCamera::ConditionallyPerformEdgeScrolling() const +void URTSCamera::executeEdgeScrollingEvaluation() { - if (this->EnableEdgeScrolling && !this->IsDragging) + /// 仅在功能开启且未进行拖拽干扰时,执行屏幕边缘检测 + if (this->enableEdgeScrolling && !this->isDragging) { - this->EdgeScrollLeft(); - this->EdgeScrollRight(); - this->EdgeScrollUp(); - this->EdgeScrollDown(); + const FVector locationBeforePush = this->rootComponent->GetComponentLocation(); + + this->performEdgeScrollLeft(); + this->performEdgeScrollRight(); + this->performEdgeScrollUp(); + this->performEdgeScrollDown(); + + if (!this->rootComponent->GetComponentLocation().Equals(locationBeforePush, 0.1f)) + { + this->updateMinimapFrustum(); + } } } -void URTSCamera::EdgeScrollLeft() const +void URTSCamera::performEdgeScrollLeft() { - const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedMousePosition = 1 - UKismetMathLibrary::NormalizeToRange( - MousePosition.X, - 0.0f, - ViewportSize.X * this->DistanceFromEdgeThreshold - ); - - const auto Movement = UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); + /// 基于鼠标左偏移计算平移推力 + 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); - this->Root->AddRelativeLocation( - -1 * this->Root->GetRightVector() * Movement * this->EdgeScrollSpeed * this->DeltaSeconds - ); + const float alpha = UKismetMathLibrary::FClamp(normalizedValue, 0.0, 1.0); + this->rootComponent->AddRelativeLocation(-1 * this->rootComponent->GetRightVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } -void URTSCamera::EdgeScrollRight() const +void URTSCamera::performEdgeScrollRight() { - const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedMousePosition = UKismetMathLibrary::NormalizeToRange( - MousePosition.X, - ViewportSize.X * (1 - this->DistanceFromEdgeThreshold), - ViewportSize.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 auto Movement = UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); - this->Root->AddRelativeLocation( - this->Root->GetRightVector() * Movement * this->EdgeScrollSpeed * this->DeltaSeconds - ); + const float alpha = UKismetMathLibrary::FClamp(normalizedValue, 0.0, 1.0); + this->rootComponent->AddRelativeLocation(this->rootComponent->GetRightVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } -void URTSCamera::EdgeScrollUp() const +void URTSCamera::performEdgeScrollUp() { - const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedMousePosition = UKismetMathLibrary::NormalizeToRange( - MousePosition.Y, - 0.0f, - ViewportSize.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 auto Movement = 1 - UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); - this->Root->AddRelativeLocation( - this->Root->GetForwardVector() * Movement * this->EdgeScrollSpeed * this->DeltaSeconds - ); + const float alpha = 1 - UKismetMathLibrary::FClamp(normalizedValue, 0.0, 1.0); + this->rootComponent->AddRelativeLocation(this->rootComponent->GetForwardVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } -void URTSCamera::EdgeScrollDown() const +void URTSCamera::performEdgeScrollDown() { - const auto MousePosition = UWidgetLayoutLibrary::GetMousePositionOnViewport(this->GetWorld()); - const auto ViewportSize = UWidgetLayoutLibrary::GetViewportWidgetGeometry(this->GetWorld()).GetLocalSize(); - const auto NormalizedMousePosition = UKismetMathLibrary::NormalizeToRange( - MousePosition.Y, - ViewportSize.Y * (1 - this->DistanceFromEdgeThreshold), - ViewportSize.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 auto Movement = UKismetMathLibrary::FClamp(NormalizedMousePosition, 0.0, 1.0); - this->Root->AddRelativeLocation( - -1 * this->Root->GetForwardVector() * Movement * this->EdgeScrollSpeed * this->DeltaSeconds - ); + const float alpha = UKismetMathLibrary::FClamp(normalizedValue, 0.0, 1.0); + this->rootComponent->AddRelativeLocation(-1 * this->rootComponent->GetForwardVector() * alpha * this->currentMovementSpeed * this->deltaSeconds); } -void URTSCamera::FollowTargetIfSet() const +void URTSCamera::updateFollowPositionIfTargetActive() { - if (this->CameraFollowTarget != nullptr) + /// 将相机根坐标强行锚定在追随目标之上 + if (this->activeCameraFollowTarget != nullptr) { - this->Root->SetWorldLocation(this->CameraFollowTarget->GetActorLocation()); + this->jumpTo(this->activeCameraFollowTarget->GetActorLocation()); } } -void URTSCamera::SmoothTargetArmLengthToDesiredZoom() const +void URTSCamera::handleTargetArmLengthInterpolation() { - this->SpringArm->TargetArmLength = FMath::FInterpTo( - this->SpringArm->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 auto RootWorldLocation = this->Root->GetComponentLocation(); - const TArray ActorsToIgnore; + const FVector currentRootXYZ = this->rootComponent->GetComponentLocation(); + const TArray excludedActors; - auto HitResult = FHitResult(); - auto DidHit = UKismetSystemLibrary::LineTraceSingle( + FHitResult floorHit; + const 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, + excludedActors, + EDrawDebugTrace::None, + floorHit, true ); - if (DidHit) + if (bValidFloor) + { + this->rootComponent->SetWorldLocation(FVector(floorHit.Location.X, floorHit.Location.Y, floorHit.Location.Z)); + } + } +} + + +void URTSCamera::updateMinimapFrustum() +{ + /// 战略視野投影核心逻辑:直接基于相机的世界位姿计算四个角点。 + if (this->cameraComponent == nullptr || this->rootComponent == nullptr) + { + return; + } + + // 核心:强制触发 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 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 horizontalFieldOfView = FMath::DegreesToRadians(fieldOfViewValue) / 2.0f; + const float verticalFieldOfView = FMath::Atan(FMath::Tan(horizontalFieldOfView) / aspectRatioValue); + + const float tangentHorizontal = FMath::Tan(horizontalFieldOfView); + const float tangentVertical = FMath::Tan(verticalFieldOfView); + + const FVector forwardVector = cameraRotation.Vector(); + const FVector rightVector = FRotationMatrix(cameraRotation).GetScaledAxis(EAxis::Y); + const FVector upVector = FRotationMatrix(cameraRotation).GetScaledAxis(EAxis::Z); + + /// 计算四个边界射线 + 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 groundAltitude = this->rootComponent->GetComponentLocation().Z; + + auto calculateIntersection = [&](const FVector& rayOrigin, const FVector& rayDirection) -> FVector + { + if (rayDirection.Z >= -0.001f) { - this->Root->SetWorldLocation( - FVector( - HitResult.Location.X, - HitResult.Location.Y, - HitResult.Location.Z - ) - ); + return rayOrigin + rayDirection * 100000.0f; } - else if (!this->IsCameraOutOfBoundsErrorAlreadyDisplayed) + const float timeToIntersection = (groundAltitude - rayOrigin.Z) / rayDirection.Z; + if (timeToIntersection < 0.0f) { - 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 - ); + return rayOrigin + rayDirection * 100000.0f; } + + return rayOrigin + rayDirection * timeToIntersection; + }; + + /// 填充战略投影点数组 + this->minimapFrustumPoints[0] = calculateIntersection(cameraLocation, topLeftDirection); + this->minimapFrustumPoints[1] = calculateIntersection(cameraLocation, topRightDirection); + this->minimapFrustumPoints[2] = calculateIntersection(cameraLocation, bottomRightDirection); + this->minimapFrustumPoints[3] = calculateIntersection(cameraLocation, bottomLeftDirection); + + /// 计算完成后发起视野更新广播 + 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); } -void URTSCamera::ConditionallyApplyCameraBounds() const +float URTSCamera::calculateYOffset(float worldY) const { - if (this->BoundaryVolume != nullptr) + 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 auto RootWorldLocation = this->Root->GetComponentLocation(); - FVector Origin; - FVector Extents; - this->BoundaryVolume->GetActorBounds(false, Origin, Extents); - this->Root->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 - ) - ); + 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/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/RTSHUD.cpp b/Source/OpenRTSCamera/Private/RTSHUD.cpp index 5a00793..051b139 100644 --- a/Source/OpenRTSCamera/Private/RTSHUD.cpp +++ b/Source/OpenRTSCamera/Private/RTSHUD.cpp @@ -1,12 +1,23 @@ #include "RTSHUD.h" +#include "RTSSelectionSubsystem.h" +#include "RTSSelectable.h" +#include "MassBattleFuncLib.h" #include "RTSSelector.h" #include "Engine/Canvas.h" +#include "LandmarkSubsystem.h" +#include "LandmarkTypes.h" +#include "Interfaces/RTSCommandInterface.h" +#include "Data/RTSCommandGridAsset.h" +#include "Engine/Texture2D.h" +#include "GameFramework/PlayerController.h" // Constructor implementation: Initializes default values. 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,16 +27,64 @@ 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. if (bIsPerformingSelection) { PerformSelection(); + bIsPerformingSelection = false; // CRITICAL: Reset the flag to stop continuous selection + } + + // --- 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. + 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 + LandmarkSys->UpdateCameraState(CamLoc, CamRot, 90.0f, ZoomFactor); + + // 3. Delegate Drawing to Subsystem (It handles the HUD layer for landmarks) + if (Canvas) + { + LandmarkSys->DrawLandmarks(Canvas); + } + } + } + } + + // --- 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(); + } + } + } } } @@ -33,6 +92,7 @@ void ARTSHUD::DrawHUD() void ARTSHUD::BeginSelection(const FVector2D& StartPoint) { SelectionStart = StartPoint; + SelectionEnd = StartPoint; // Initialize End to Start to avoid stale data bIsDrawingSelectionBox = true; } @@ -54,11 +114,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,21 +147,252 @@ 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. Prepare + ERTSSelectionModifier Modifier = ERTSSelectionModifier::Replace; + float DragDistSq = FVector2D::DistSquared(SelectionStart, SelectionEnd); + + URTSSelectionSubsystem* SelectionSubsystem = nullptr; + URTSSelector* SelectorComponent = nullptr; + APlayerController* PC = GetOwningPlayerController(); + + if (PC) + { + SelectorComponent = PC->FindComponentByClass(); + if (const ULocalPlayer* LP = PC->GetLocalPlayer()) + { + SelectionSubsystem = LP->GetSubsystem(); + } + + if (PC->IsInputKeyDown(EKeys::LeftShift) || PC->IsInputKeyDown(EKeys::RightShift)) + { + Modifier = ERTSSelectionModifier::Add; + } + } + + TArray FinalActorSelection; + TArray FinalMassSelection; - // Find the URTSSelector component and pass the selected actors to it. - if (const auto PC = GetOwningPlayerController()) + // 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). + + if (Modifier == ERTSSelectionModifier::Add && SelectionSubsystem) { - if (const auto SelectorComponent = PC->FindComponentByClass()) + UE_LOG(LogTemp, Log, TEXT("RTSHUD: Shift Action - DragDistSq: %f (Threshold: %f)"), DragDistSq, MinSelectionSizeSq); + + if (DragDistSq <= MinSelectionSizeSq) { - SelectorComponent->HandleSelectedActors(SelectedActors); + // 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) + { + 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 (FinalActorSelection.Num() > 0) + { + 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 - REMOVED DUPLICATE CALL + // SetSelectedUnits was already called above after initial search. + // Logic now relies on that single entry point. + bIsPerformingSelection = false; } + +#include "MassBattleFuncLib.h" +#include "MassBattleStructs.h" + +void ARTSHUD::PerformMassSelection(TArray& OutEntities) +{ + OutEntities.Reset(); + + APlayerController* PC = GetOwningPlayerController(); + if (!PC || !PC->PlayerCameraManager) return; + + // Calculate selection box bounds + 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 = { + FVector2D(MinX, MinY), // 左上 + FVector2D(MinX, MaxY), // 左下 + FVector2D(MaxX, MaxY), // 右下 + FVector2D(MaxX, MinY), // 右上 + }; + + FViewTracePoints TracePoints; + TracePoints.ViewPoint = PC->PlayerCameraManager->GetCameraLocation(); + + for (const FVector2D& ScreenPoint : ScreenPoints) + { + FVector WorldPos, WorldDirection; + if (PC->DeprojectScreenPositionToWorld(ScreenPoint.X, ScreenPoint.Y, WorldPos, WorldDirection)) + { + TracePoints.SelectionPoints.Add(WorldPos + WorldDirection * 100000.0f); + } + } + + if (TracePoints.SelectionPoints.Num() == 4) + { + bool bHit = false; + TArray Results; + + 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) + { + for (const FTraceResult& Result : Results) + { + OutEntities.Add(Result.Entity); + } + } + } +} 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/Private/RTSSelectionSubsystem.cpp b/Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp new file mode 100644 index 0000000..b58b757 --- /dev/null +++ b/Source/OpenRTSCamera/Private/RTSSelectionSubsystem.cpp @@ -0,0 +1,397 @@ +#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() +{ + Super::Deinitialize(); +} + +void URTSSelectionSubsystem::SetSelectedUnits(const TArray& InActors, const TArray& InEntities, ERTSSelectionModifier 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 = FinalActors; + SelectedEntities = FinalEntities; + } + else if (Modifier == ERTSSelectionModifier::Add) + { + for (AActor* Actor : FinalActors) SelectedActors.AddUnique(Actor); + for (const FEntityHandle& Handle : FinalEntities) SelectedEntities.AddUnique(Handle); + } + else if (Modifier == ERTSSelectionModifier::Remove) + { + for (AActor* Actor : FinalActors) SelectedActors.Remove(Actor); + for (const FEntityHandle& Handle : FinalEntities) 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]); + 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)); + // 按类型名排序,保证同类型单位连续显示 + View.Items.Sort([](const FRTSUnitData& A, const FRTSUnitData& B){ return A.Name < B.Name; }); + } + else + { + View.Mode = ERTSSelectionMode::Summary; + 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); + // Summary 模式:按类型名字母排序,保证 City1→City2→... MassUnit_SubType0→... 依次连续 + View.Items.Sort([](const FRTSUnitData& A, const FRTSUnitData& B){ return A.Name < B.Name; }); + } + + // --- Tab Cycling --- + AvailableGroupKeys.Reset(); + for(const auto& Item : View.Items) AvailableGroupKeys.AddUnique(Item.Name); + AvailableGroupKeys.Sort(); + + if (CurrentGroupIndex >= AvailableGroupKeys.Num()) CurrentGroupIndex = 0; + if (AvailableGroupKeys.IsValidIndex(CurrentGroupIndex)) View.ActiveGroupKey = AvailableGroupKeys[CurrentGroupIndex]; + + 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(), ERTSSelectionModifier::Replace); +} + +void URTSSelectionSubsystem::CycleGroup() +{ + if (AvailableGroupKeys.Num() <= 1) return; + + CurrentGroupIndex++; + if (CurrentGroupIndex >= AvailableGroupKeys.Num()) CurrentGroupIndex = 0; + SetSelectedUnits(SelectedActors, SelectedEntities, ERTSSelectionModifier::Replace); +} + +void URTSSelectionSubsystem::RemoveUnit(const FRTSUnitData& UnitData) +{ + TArray ActorsToRemove; + TArray EntitiesToRemove; + + if (UnitData.ActorPtr) ActorsToRemove.Add(UnitData.ActorPtr); + else if (UnitData.EntityHandle.Index != 0) EntitiesToRemove.Add(UnitData.EntityHandle); + else + { + for (AActor* Act : SelectedActors) if (Act && Act->GetClass()->GetDisplayNameText().ToString() == UnitData.Name) ActorsToRemove.Add(Act); + } + + SetSelectedUnits(ActorsToRemove, EntitiesToRemove, ERTSSelectionModifier::Remove); +} + +void URTSSelectionSubsystem::SelectGroup(const FString& GroupKey) +{ + TArray NewActors; + TArray NewEntities; + + 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); + } + + SetSelectedUnits(NewActors, NewEntities, ERTSSelectionModifier::Replace); +} + +FRTSUnitData URTSSelectionSubsystem::CreateUnitDataFromActor(AActor* Actor) +{ + FRTSUnitData Data; + if (Actor) + { + Data.Name = Actor->GetClass()->GetDisplayNameText().ToString(); + Data.ActorPtr = Actor; + Data.bIsMassEntity = false; + + 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; + + 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; +} + + +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/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/RTSCameraMinimapWidget.cpp b/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp new file mode 100644 index 0000000..3ea88c1 --- /dev/null +++ b/Source/OpenRTSCamera/Private/UI/RTSCameraMinimapWidget.cpp @@ -0,0 +1,251 @@ +// 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) +{ + /// 初始化控件可见性,并默认设置绘制线宽 + this->SetVisibility(ESlateVisibility::Visible); +} + +void URTSCameraMinimapWidget::NativeConstruct() +{ + Super::NativeConstruct(); + + /// 锁定交互属性并执行初始控制器搜索 + this->SetVisibility(ESlateVisibility::Visible); + this->SetIsFocusable(true); + + this->InitializeController(); +} + +void URTSCameraMinimapWidget::InitializeController() +{ + if (APlayerController* playerController = this->GetOwningPlayer()) + { + playerController->bEnableClickEvents = true; + playerController->bEnableMouseOverEvents = true; + } + this->findRTSCamera(); +} + +void URTSCameraMinimapWidget::findRTSCamera() +{ + /// 尝试从当前的观察目标或 Pawn 中定位 RTSCamera 组件 + if (!this->cachedRTSCamera) + { + APlayerController* playerController = this->GetOwningPlayer(); + if (playerController) + { + if (AActor* viewTarget = playerController->GetViewTarget()) + { + this->cachedRTSCamera = viewTarget->FindComponentByClass(); + } + if (!this->cachedRTSCamera) + { + if (APawn* pawn = playerController->GetPawn()) + { + this->cachedRTSCamera = pawn->FindComponentByClass(); + } + } + } + } + + /// 若成功捕获组件,则建立响应式订阅并缓存辅助引用 + if (this->cachedRTSCamera) + { + /// 绑定视野更新委托:由相机的“主动推送”驱动 UI 的“局部失效” + this->cachedRTSCamera->onMinimapFrustumUpdated.RemoveAll(this); + this->cachedRTSCamera->onMinimapFrustumUpdated.AddUObject(this, &URTSCameraMinimapWidget::handleMinimapFrustumUpdated); + + AActor* cameraOwner = this->cachedRTSCamera->GetOwner(); + if (cameraOwner) + { + if (!this->cachedCameraComponent) + { + this->cachedCameraComponent = cameraOwner->FindComponentByClass(); + } + if (!this->cachedSpringArm) + { + 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 +{ + /// 将世界坐标系下的点线性映射至小地图控件的局部 0-1 空间,并适配轴向偏移 + if (this->cachedBoundsExtent.X < KINDA_SMALL_NUMBER || this->cachedBoundsExtent.Y < KINDA_SMALL_NUMBER) + { + return FVector2D::ZeroVector; + } + + 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); + + return FVector2D(normalizedY * WidgetSize.X, (1.0f - normalizedX) * WidgetSize.Y); +} + +FVector2D URTSCameraMinimapWidget::ConvertWidgetLocalToWorld(const FVector2D& LocalPos, const FVector2D& WidgetSize) const +{ + /// 将小地图局部像素坐标反投影回世界地图水平面的 X/Y 坐标 + if (WidgetSize.X <= 0.0f || WidgetSize.Y <= 0.0f) + { + return FVector2D::ZeroVector; + } + + float uParam = LocalPos.X / WidgetSize.X; + float vParam = LocalPos.Y / WidgetSize.Y; + + float normalizedX = 1.0f - vParam; + float normalizedY = uParam; + + 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); +} + +int32 URTSCameraMinimapWidget::NativePaint( + const FPaintArgs& Args, + const FGeometry& AllottedGeometry, + const FSlateRect& MyCullingRect, + FSlateWindowElementList& OutDrawElements, + int32 LayerId, + const FWidgetStyle& InWidgetStyle, + bool bParentEnabled +) const +{ + /// 执行基础绘制流程。注:如果当前组件没有被 Invalidate,Slate 可能会完全跳过此函数执行。 + int32 maxLayerId = Super::NativePaint(Args, AllottedGeometry, MyCullingRect, OutDrawElements, LayerId, InWidgetStyle, bParentEnabled); + + if (!this->cachedRTSCamera || !this->bHasValidBounds) + { + const_cast(this)->findRTSCamera(); + } + + if (!this->cachedRTSCamera || !this->bHasValidBounds || !this->cachedSpringArm || !this->cachedCameraComponent) + { + return maxLayerId; + } + + FVector2D geometrySize = AllottedGeometry.GetLocalSize(); + if (geometrySize.X < 1.0f || geometrySize.Y < 1.0f) + { + return maxLayerId; + } + + /// 战术兼容性调整:虽然静态数组更直接,但由于 Slate 的 MakeLines API 严格要求 TArray 类型容器, + /// 此处采用预分配空间的 TArray 以平衡性能与接口规范。 + TArray drawPoints; + drawPoints.Reserve(5); + + for (int32 i = 0; i < 4; ++i) + { + const FVector& worldPt = this->cachedRTSCamera->minimapFrustumPoints[i]; + drawPoints.Add(this->ConvertWorldToWidgetLocal(FVector2D(worldPt.X, worldPt.Y), geometrySize)); + } + + if (drawPoints.Num() > 0) + { + /// 彻底消除断言崩溃:显式拷贝首个元素至栈变量。 + /// UE 5.6 严禁直接 Add 容器内部的元素地址,以防扩容时发生非法访问。 + const FVector2D closedPoint = drawPoints[0]; + drawPoints.Add(closedPoint); + } + + FSlateDrawElement::MakeLines( + OutDrawElements, + LayerId + 1, + AllottedGeometry.ToPaintGeometry(), + drawPoints, + ESlateDrawEffect::None, + FLinearColor::White, + true, + this->lineWidth + ); + + return maxLayerId + 1; +} + +FReply URTSCameraMinimapWidget::NativeOnMouseButtonDown(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + /// 响应点击:将屏幕点击直接转化为相机的战略突变 + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton) + { + this->bIsDragging = true; + if (this->cachedRTSCamera) + { + 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(this->TakeWidget()); + } + return FReply::Unhandled(); +} + +FReply URTSCameraMinimapWidget::NativeOnMouseButtonUp(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + /// 释放拖拽锁 + if (InMouseEvent.GetEffectingButton() == EKeys::LeftMouseButton && this->bIsDragging) + { + this->bIsDragging = false; + return FReply::Handled().ReleaseMouseCapture(); + } + return FReply::Unhandled(); +} + +FReply URTSCameraMinimapWidget::NativeOnMouseMove(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + /// 拖拽追踪:连续更新相机位置 + if (this->bIsDragging && this->HasMouseCapture()) + { + if (this->cachedRTSCamera) + { + 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(); + } + return FReply::Unhandled(); +} + +void URTSCameraMinimapWidget::NativeOnMouseEnter(const FGeometry& InGeometry, const FPointerEvent& InMouseEvent) +{ + Super::NativeOnMouseEnter(InGeometry, InMouseEvent); +} + +void URTSCameraMinimapWidget::NativeOnMouseLeave(const FPointerEvent& InMouseEvent) +{ + Super::NativeOnMouseLeave(InMouseEvent); +} 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/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/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/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/OpenRTSCamera.h b/Source/OpenRTSCamera/Public/OpenRTSCamera.h index 9e39ca2..3741d4d 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 @@ -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 bb579d2..73d2d92 100644 --- a/Source/OpenRTSCamera/Public/RTSCamera.h +++ b/Source/OpenRTSCamera/Public/RTSCamera.h @@ -2,201 +2,414 @@ #pragma once -#include "CoreMinimal.h" +#include #include "InputMappingContext.h" #include "Camera/CameraComponent.h" #include "Components/ActorComponent.h" #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 xAxisValue = 0; + + /// 目标位置在 Y 轴上的分量增量 UPROPERTY() - float Y = 0; + float yAxisValue = 0; + + /// 本次移动指令的缩放权重比例 UPROPERTY() - float Scale = 0; + float movementScale = 0; }; +/** + * @brief RTS 相机组件,处理视口平移、边缘滚动、意图缩放及视野投影逻辑。 + * + * 组件遵循“战略由人,战术由AI”的设计原则,旨在平衡操作的顺滑感与 UI 的即时反馈。 + **/ UCLASS(Blueprintable, ClassGroup=(Custom), meta=(BlueprintSpawnableComponent)) 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() const; + void setActiveCamera(); + /** + * @brief 将相机组件瞬間移动至指定的 X/Y 地面坐标 + * + * @param 参数名称: position 数据类型: FVector + **/ UFUNCTION(BlueprintCallable, Category = "RTSCamera") - void JumpTo(FVector Position) const; - - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") - float MinimumZoomLength; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") - float MaximumZoomLength; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") - float ZoomCatchupSpeed; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings") - float ZoomSpeed; - - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float StartingYAngle; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float StartingZAngle; - - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float MoveSpeed; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - float RotateSpeed; - + void jumpTo(FVector position); + /** - * 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. - */ + * @brief 获取当前用于相机移动约束的边界体积引用 + * + * @return 返回值类型: AActor* + **/ + UFUNCTION(BlueprintPure, Category = "RTSCamera") + AActor* getMovementBoundaryVolume() const { return movementBoundaryVolume; } + + /// 相机缩放的最小目标距离(最接近地面) + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings", meta = (DisplayName = "最小缩放高度", ToolTip = "相机距离地面的最近距离。")) + float minimumZoomLength; + + /// 相机缩放的最大目标距离(最高视野) + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings", meta = (DisplayName = "最大缩放高度", ToolTip = "相机距离地面的最远距离。")) + float maximumZoomLength; + + /// 缩放插值的补全速率(值越大,物理位置追赶意图的速度越快) + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings", meta = (DisplayName = "缩放平滑速度", ToolTip = "物理相机追赶缩放意图的速度。")) + float zoomCatchupSpeed; + + /// 单次滚轮操作触发的缩放距离步长 + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera - Zoom Settings", meta = (DisplayName = "单次缩进步幅", ToolTip = "鼠标滚轮单次滚动引起的距离变化量。")) + float zoomSpeed; + + /// 初始化时的相机俯仰角 (Pitch) + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "初始俯仰角", ToolTip = "相机的初始垂直倾斜角度(度)。")) + float startingPitchAngle; + + /// 初始化时的相机偏航角 (Yaw) + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "初始偏航角", ToolTip = "相机的初始水平旋转角度(度)。")) + float startingYawAngle; + + /// 相机在最大缩放高度时的移动速度上限 + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "最大平移速度", ToolTip = "在高空缩放时的移动速度。")) + float maxMovementSpeed; + + /// 相机在最小缩放高度时的基础移动速度 + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "最小平移速度", ToolTip = "在低空缩放时的移动速度。")) + float minMovementSpeed; + + /// 输入控制下的水平旋转感官速度 + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "旋转响应速度", ToolTip = "相机水平旋转的速度权重。")) + float rotationSpeed; + + /// 相机拖拽操作在视口中的拉伸增量比例 UPROPERTY( BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", - meta = (ClampMin = "0.0", ClampMax = "1.0") + meta = (ClampMin = "0.0", ClampMax = "1.0", DisplayName = "拖拽惯性系数", ToolTip = "鼠标拖拽移动时的平滑权重。") ) - float DragExtent; + 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; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - bool EnableCameraLag; - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera") - bool EnableCameraRotationLag; + /** 是否启用 Y 轴 (东西/左右) 方向的边界视野约束 */ + UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera | Boundary", meta = (DisplayName = "启用 Y 轴边界约束")) + bool bEnableYBoundaryConstraint; + /// 启用相机位置移动的插值延迟 + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "启用位置平滑", ToolTip = "开启后相机移动将具有物理惯性。")) + bool enableCameraLag; + /// 启用相机视野旋转的插值延迟 + UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "RTSCamera", meta = (DisplayName = "启用旋转平滑", ToolTip = "开启后相机旋转将具有物理惯性。")) + 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; - UPROPERTY( - BlueprintReadWrite, - EditAnywhere, - Category = "RTSCamera - Edge Scroll Settings", - meta=(EditCondition="EnableEdgeScrolling") - ) - float EdgeScrollSpeed; + 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 响应增强输入事件。执行向右转向操作。 + * + * @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 xAxisValue 数据类型: float + * @param yAxisValue 数据类型: float + * @param movementScale 数据类型: float + **/ + void requestCameraMovement(float xAxisValue, float yAxisValue, float movementScale); + + /** + * @brief 在一个逻辑帧内,分步执行指令队列中积压的所有平移指令 + **/ + void applyAccumulatedMovementCommands(); + + /// 组件所属的 Actor 引用,定义了相机的生命周期主体 UPROPERTY() - AActor* Owner; + AActor* cameraOwner; + + /// 根场景组件,控制整个相机组在该坐标下的水平面移动 UPROPERTY() - USceneComponent* Root; + USceneComponent* rootComponent; + + /// 相机组件,定义视野视野参数与最终渲染输出 UPROPERTY() - UCameraComponent* Camera; + UCameraComponent* cameraComponent; + + /// 弹簧臂组件,控制相机与中心点之间的意图长度与俯仰关系 UPROPERTY() - USpringArmComponent* SpringArm; + USpringArmComponent* springArmComponent; + + /// 实时战略专用的玩家控制器,分发输入事件 UPROPERTY() - APlayerController* PlayerController; + APlayerController* realTimeStrategyPlayerController; + + /// 用于限制相机在地图中活动范围的体积 UPROPERTY() - AActor* BoundaryVolume; + AActor* movementBoundaryVolume; + + /// 玩家输入的理想缩放目标高度,视野计算将优先同步此意图而非物理插值过程 UPROPERTY() - float DesiredZoomLength; + float desiredZoomLength; private: - void CollectComponentDependencyReferences(); - void ConfigureSpringArm(); - void TryToFindBoundaryVolumeReference(); - void ConditionallyEnableEdgeScrolling() const; - void CheckForEnhancedInputComponent() const; - void BindInputMappingContext() const; - void BindInputActions(); + void resolveComponentDependencyPointers(); + void setupInitialSpringArmState(); - void ConditionallyPerformEdgeScrolling() const; - void EdgeScrollLeft() const; - void EdgeScrollRight() const; - void EdgeScrollUp() const; - void EdgeScrollDown() const; + void locateMapBoundaryVolumeByTag(); + void configureInputModeForEdgeScrolling(); + void validateEnhancedInputAvailability(); + void registerInputMappingContext(); + void bindActionCallbacks(); - void FollowTargetIfSet() const; - void SmoothTargetArmLengthToDesiredZoom() const; - void ConditionallyKeepCameraAtDesiredZoomAboveGround(); - void ConditionallyApplyCameraBounds() const; + void executeEdgeScrollingEvaluation(); + void performEdgeScrollLeft(); + void performEdgeScrollRight(); + void performEdgeScrollUp(); + void performEdgeScrollDown(); + void updateFollowPositionIfTargetActive(); + void handleTargetArmLengthInterpolation(); + void rectifyRootHeightFromTerrain(); + + /** @brief 计算当前坐标下的边界补偿并应用 */ + void applyBoundaryConstraints(); + + /** @brief 基于 Y 坐标计算 Lateral Socket Offset */ + float calculateYOffset(float worldY) const; + + /** @brief 基于 X 坐标计算 Vertical Socket Offset */ + float calculateXOffset(float worldX) const; + + /// 相机当前正在锁定跟随的 Actor 实测对象 UPROPERTY() - FName CameraBlockingVolumeTag; - UPROPERTY() - AActor* CameraFollowTarget; + AActor* activeCameraFollowTarget; + + /// 缓存边界侧移量 (SocketOffset.Y) + float currentLateralSocketOffset; + /// 缓存边界纵移量 (SocketOffset.X) + float currentVerticalSocketOffset; + /// 自上一帧以来的时间增量(秒) UPROPERTY() - float DeltaSeconds; + float deltaSeconds; + + /** @brief 预计算的横向延伸系数 (Lateral Reach / TargetArmLength) */ + float lateralReachFactor; + + /** @brief 预计算的纵向延伸系数 (Forward Reach / TargetArmLength) */ + float forwardReachFactor; + + /** @brief 预计算的后向延伸系数 (Backward Reach / TargetArmLength) */ + float backwardReachFactor; + + /// 状态位:指示是否正在进行鼠标拖拽操作 UPROPERTY() - bool IsCameraOutOfBoundsErrorAlreadyDisplayed; + bool isDragging; + + /// 拖拽操作开始时的视口坐标缓存 UPROPERTY() - bool IsDragging; + FVector2D dragInteractionInitialLocation; + + /// 移动指令队列,用于适配变动帧率下的平滑渲染 UPROPERTY() - FVector2D DragStartLocation; + TArray pendingMovementCommands; + + /// 当前瞬时计算的相机移动速度值 UPROPERTY() - TArray MoveCameraCommands; + float currentMovementSpeed; + +public: + /// 静态数组,存储由视野投影计算出的地平面四个接地区顶点。 + /// 顺序遵循:[0]左上, [1]右上, [2]右下, [3]左下。 + FVector minimapFrustumPoints[4]; + + /** + * @brief 强制触发視野投影计算,基于当前位置及缩放意图刷新 minimapFrustumPoint 数组数据。 + * 注意:仅在相机产生明确的战略意图变更(移动、缩放、跳转)时产生计算开销。 + **/ + UFUNCTION(BlueprintCallable, Category = "RTSCamera|Minimap") + void updateMinimapFrustum(); }; diff --git a/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h b/Source/OpenRTSCamera/Public/RTSCameraBoundsVolume.h index 55a3a0d..bce438a 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" @@ -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/RTSHUD.h b/Source/OpenRTSCamera/Public/RTSHUD.h index 694685c..130c528 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,13 +13,18 @@ class OPENRTSCAMERA_API ARTSHUD : public AHUD public: ARTSHUD(); - UPROPERTY(BlueprintReadWrite, EditAnywhere, Category = "Selection Box") FLinearColor SelectionBoxColor; 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); @@ -36,9 +41,14 @@ class OPENRTSCAMERA_API ARTSHUD : public AHUD void PerformSelection(); protected: + /** Draw the selection box */ + void DrawSelectionMarquee(); + virtual void DrawHUD() override; private: + void PerformMassSelection(TArray& OutEntities); + bool bIsDrawingSelectionBox; bool bIsPerformingSelection; FVector2D SelectionStart; diff --git a/Source/OpenRTSCamera/Public/RTSSelectable.h b/Source/OpenRTSCamera/Public/RTSSelectable.h index 412c59e..94a4f25 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)) @@ -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..5a258fb --- /dev/null +++ b/Source/OpenRTSCamera/Public/RTSSelectionStructs.h @@ -0,0 +1,101 @@ +// 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..632344d --- /dev/null +++ b/Source/OpenRTSCamera/Public/RTSSelectionSubsystem.h @@ -0,0 +1,137 @@ +// 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_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. + */ +UCLASS() +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; + + 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(); + + /** + * 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) + */ + 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); + + /** + * 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; } + + 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); } + + /** 返回当前选中的 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. + */ + UPROPERTY(BlueprintAssignable, Category = "RTS Selection") + FOnSelectionChanged OnSelectionChanged; + +private: + // Raw State + UPROPERTY() + TArray SelectedActors; + + TArray SelectedEntities; + + // Cycle State + UPROPERTY() + TArray AvailableGroupKeys; + + UPROPERTY() + TObjectPtr DefaultGridNative; + + int32 CurrentGroupIndex = 0; + + // Helpers + FRTSUnitData CreateUnitDataFromActor(AActor* Actor); + FRTSUnitData CreateUnitDataFromEntity(const FEntityHandle& Handle); + + // Thresholds + const int32 ListModeMaxCount = 12; +}; 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" diff --git a/Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h b/Source/OpenRTSCamera/Public/UI/RTSActiveGroupWidget.h new file mode 100644 index 0000000..9f008f6 --- /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() + 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. + // 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/RTSCameraMinimapWidget.h b/Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h new file mode 100644 index 0000000..002b161 --- /dev/null +++ b/Source/OpenRTSCamera/Public/UI/RTSCameraMinimapWidget.h @@ -0,0 +1,88 @@ +// 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: + /** + * @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; + + /** Convert Widget Local Coordinates to World Location (XY) */ + FVector2D ConvertWidgetLocalToWorld(const FVector2D& LocalPos, const FVector2D& WidgetSize) const; + +protected: + /** @brief 缓存的 RTS 相机组件引用 */ + UPROPERTY(Transient, BlueprintReadOnly, Category = "RTSCamera|Cache") + TObjectPtr cachedRTSCamera; + + /** @brief 缓存的相机渲染组件,作为视场角(FOV)的原始依据 */ + UPROPERTY(Transient, BlueprintReadOnly, Category = "RTSCamera|Cache") + TObjectPtr cachedCameraComponent; + + /** @brief 缓存的弹簧臂组件,作为相机缩放与倾斜的物理依据 */ + UPROPERTY(Transient, BlueprintReadOnly, Category = "RTSCamera|Cache") + TObjectPtr cachedSpringArm; + + /// 地图边界的中心点缓存 + FVector cachedBoundsOrigin = FVector::ZeroVector; + + /// 地图边界的延伸范围缓存 + FVector cachedBoundsExtent = FVector(100.f, 100.f, 100.f); + + /// 标识当前缓存的地图边界数据是否有效 + bool bHasValidBounds = false; + + /// 状态位:标识玩家当前是否正在通过鼠标在控件上执行位置拖拽 + bool bIsDragging = false; +}; 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/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/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; +}; 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); +};