From 3a8082775f0a8414c89e610c81dddd31977dc090 Mon Sep 17 00:00:00 2001 From: yuichipanda Date: Sun, 1 Nov 2026 18:37:31 +0900 Subject: [PATCH] Add cell selection, auto camera placement, and FGridMath Co-Authored-By: Claude Sonnet 5.5 --- Source/Tactica/Private/GridCell.cpp | 21 ++++++++++--- Source/Tactica/Private/GridManager.cpp | 18 ++++++++++- Source/Tactica/Private/GridMath.cpp | 27 ++++++++++++++++ Source/Tactica/Private/TacticaCameraPawn.cpp | 31 ++++++++++++++++--- .../Private/TacticaPlayerController.cpp | 27 ++++++++++++---- Source/Tactica/Public/GridCell.h | 18 ++++++++--- Source/Tactica/Public/GridManager.h | 6 ++++ Source/Tactica/Public/GridMath.h | 23 ++++++++++++++ Source/Tactica/Public/TacticaCameraPawn.h | 13 +++++++- .../Tactica/Public/TacticaPlayerController.h | 6 +++- 10 files changed, 168 insertions(+), 22 deletions(-) create mode 100644 Source/Tactica/Private/GridMath.cpp create mode 100644 Source/Tactica/Public/GridMath.h diff --git a/Source/Tactica/Private/GridCell.cpp b/Source/Tactica/Private/GridCell.cpp index 2b21df9..ed5cec4 100644 --- a/Source/Tactica/Private/GridCell.cpp +++ b/Source/Tactica/Private/GridCell.cpp @@ -27,6 +27,7 @@ AGridCell::AGridCell() // カーソルのレイキャスト(Visibilityチャンネル)に当たるようにする MeshComponent->SetCollisionEnabled(ECollisionEnabled::QueryOnly); + MeshComponent->SetCollisionObjectType(ECC_WorldDynamic); MeshComponent->SetCollisionResponseToAllChannels(ECR_Ignore); MeshComponent->SetCollisionResponseToChannel(ECC_Visibility, ECR_Block); @@ -44,14 +45,26 @@ void AGridCell::BeginPlay() Super::BeginPlay(); DynamicMaterial = MeshComponent->CreateDynamicMaterialInstance(0); - SetHighlighted(false); + RefreshColor(); } -void AGridCell::SetHighlighted(bool bHighlighted) +void AGridCell::SetHovered(bool bInHovered) +{ + bHovered = bInHovered; + RefreshColor(); +} + +void AGridCell::SetSelected(bool bInSelected) +{ + bSelected = bInSelected; + RefreshColor(); +} + +void AGridCell::RefreshColor() { if (DynamicMaterial) { - DynamicMaterial->SetVectorParameterValue(TEXT("Color"), - bHighlighted ? HighlightColor : NormalColor); + const FLinearColor& Color = bSelected ? SelectedColor : (bHovered ? HoverColor : NormalColor); + DynamicMaterial->SetVectorParameterValue(TEXT("Color"), Color); } } \ No newline at end of file diff --git a/Source/Tactica/Private/GridManager.cpp b/Source/Tactica/Private/GridManager.cpp index 69a34b6..aea81f7 100644 --- a/Source/Tactica/Private/GridManager.cpp +++ b/Source/Tactica/Private/GridManager.cpp @@ -1,5 +1,6 @@ #include "GridManager.h" #include "GridCell.h" +#include "GridMath.h" #include "Engine/World.h" // このファイル専用のログカテゴリ @@ -20,6 +21,21 @@ void AGridManager::BeginPlay() GenerateGrid(); } +AGridCell* AGridManager::GetCellAt(FIntPoint Coord) const +{ + const int32 Index = FGridMath::CoordToIndex(Coord, GridSizeX, GridSizeY); + if (Cells.IsValidIndex(Index) && Cells[Index] && Cells[Index]->GridCoord == Coord) + { + return Cells[Index]; + } + return nullptr; +} + +FVector AGridManager::GetGridCenter() const +{ + return FGridMath::GetGridCenter(GetActorLocation(), GridSizeX, GridSizeY, CellSize); +} + void AGridManager::GenerateGrid() { UWorld* World = GetWorld(); @@ -41,7 +57,7 @@ void AGridManager::GenerateGrid() { for (int32 X = 0; X < GridSizeX; ++X) { - const FVector Location = Origin + FVector(X * CellSize, Y * CellSize, 0.0f); + const FVector Location = FGridMath::CellToWorld(FIntPoint(X, Y), Origin, CellSize); FActorSpawnParameters Params; Params.SpawnCollisionHandlingOverride = diff --git a/Source/Tactica/Private/GridMath.cpp b/Source/Tactica/Private/GridMath.cpp new file mode 100644 index 0000000..8f568aa --- /dev/null +++ b/Source/Tactica/Private/GridMath.cpp @@ -0,0 +1,27 @@ +#include "GridMath.h" + +FVector FGridMath::CellToWorld(FIntPoint Coord, const FVector& Origin, float CellSize) +{ + return Origin + FVector(Coord.X * CellSize, Coord.Y * CellSize, 0.0f); +} + +FIntPoint FGridMath::WorldToCell(const FVector& World, const FVector& Origin, float CellSize) +{ + const FVector Local = World - Origin; + return FIntPoint(FMath::RoundToInt(Local.X / CellSize), FMath::RoundToInt(Local.Y / CellSize)); +} + +bool FGridMath::IsInBounds(FIntPoint Coord, int32 SizeX, int32 SizeY) +{ + return Coord.X >= 0 && Coord.X < SizeX && Coord.Y >= 0 && Coord.Y < SizeY; +} + +int32 FGridMath::CoordToIndex(FIntPoint Coord, int32 SizeX, int32 SizeY) +{ + return IsInBounds(Coord, SizeX, SizeY) ? Coord.Y * SizeX + Coord.X : INDEX_NONE; +} + +FVector FGridMath::GetGridCenter(const FVector& Origin, int32 SizeX, int32 SizeY, float CellSize) +{ + return Origin + FVector((SizeX - 1) * CellSize * 0.5f, (SizeY - 1) * CellSize * 0.5f, 0.0f); +} diff --git a/Source/Tactica/Private/TacticaCameraPawn.cpp b/Source/Tactica/Private/TacticaCameraPawn.cpp index 1f943c2..31f810c 100644 --- a/Source/Tactica/Private/TacticaCameraPawn.cpp +++ b/Source/Tactica/Private/TacticaCameraPawn.cpp @@ -1,5 +1,9 @@ #include "TacticaCameraPawn.h" #include "Camera/CameraComponent.h" +#include "GridManager.h" +#include "Kismet/GameplayStatics.h" + +DEFINE_LOG_CATEGORY_STATIC(LogTacticaCamera, Log, All); ATacticaCameraPawn::ATacticaCameraPawn() { @@ -7,9 +11,26 @@ ATacticaCameraPawn::ATacticaCameraPawn() Camera = CreateDefaultSubobject(TEXT("Camera")); SetRootComponent(Camera); - - // 3x3グリッド(中心 (100,100))を斜め上から見下ろす位置。 - // 配置したPawnのActor位置を変えれば調整できる - Camera->SetRelativeLocation(FVector(-250.0f, 100.0f, 450.0f)); - Camera->SetRelativeRotation(FRotator(-60.0f, 0.0f, 0.0f)); +} + +void ATacticaCameraPawn::BeginPlay() +{ + Super::BeginPlay(); + + const AGridManager* Grid = Cast( + UGameplayStatics::GetActorOfClass(this, AGridManager::StaticClass())); + if (!Grid) + { + UE_LOG(LogTacticaCamera, Warning, TEXT("AGridManager が見つかりません。カメラは配置されません")); + return; + } + + // グリッドの長辺の半分(余白つき)が画角に収まる距離を求める + const float HalfExtent = FMath::Max(Grid->GridSizeX, Grid->GridSizeY) * Grid->CellSize * 0.5f * MarginScale; + const float HalfFovRad = FMath::DegreesToRadians(Camera->FieldOfView * 0.5f); + const float Distance = HalfExtent / FMath::Tan(HalfFovRad); + + const FRotator Rotation(Pitch, 0.0f, 0.0f); + const FVector Location = Grid->GetGridCenter() - Rotation.Vector() * Distance; + SetActorLocationAndRotation(Location, Rotation); } diff --git a/Source/Tactica/Private/TacticaPlayerController.cpp b/Source/Tactica/Private/TacticaPlayerController.cpp index 748a731..4dd9367 100644 --- a/Source/Tactica/Private/TacticaPlayerController.cpp +++ b/Source/Tactica/Private/TacticaPlayerController.cpp @@ -33,19 +33,24 @@ void ATacticaPlayerController::PlayerTick(float DeltaTime) if (HoveredCell) { - HoveredCell->SetHighlighted(false); + HoveredCell->SetHovered(false); } HoveredCell = Cell; if (HoveredCell) { - HoveredCell->SetHighlighted(true); + HoveredCell->SetHovered(true); } } AGridCell* ATacticaPlayerController::GetCellUnderCursor() const { + // マスはWorldDynamicのみを対象にする。 + // 地形などWorldStaticと上面が同じ高さでも、そちらに邪魔されない + const TArray> ObjectTypes = { + UEngineTypes::ConvertToObjectType(ECC_WorldDynamic) }; + FHitResult Hit; - if (GetHitResultUnderCursor(ECC_Visibility, false, Hit)) + if (GetHitResultUnderCursorForObjects(ObjectTypes, false, Hit)) { return Cast(Hit.GetActor()); } @@ -54,9 +59,19 @@ AGridCell* ATacticaPlayerController::GetCellUnderCursor() const void ATacticaPlayerController::OnClick() { - if (const AGridCell* Cell = GetCellUnderCursor()) + AGridCell* Cell = GetCellUnderCursor(); + if (!Cell) { - UE_LOG(LogTacticaInput, Log, TEXT("Cell clicked: (%d, %d)"), - Cell->GridCoord.X, Cell->GridCoord.Y); + return; // マス以外のクリックでは選択を変えない } + + if (SelectedCell) + { + SelectedCell->SetSelected(false); + } + SelectedCell = Cell; + SelectedCell->SetSelected(true); + + UE_LOG(LogTacticaInput, Log, TEXT("Cell clicked: (%d, %d)"), + Cell->GridCoord.X, Cell->GridCoord.Y); } diff --git a/Source/Tactica/Public/GridCell.h b/Source/Tactica/Public/GridCell.h index b3697ce..7c3ecd7 100644 --- a/Source/Tactica/Public/GridCell.h +++ b/Source/Tactica/Public/GridCell.h @@ -35,17 +35,27 @@ public: UPROPERTY(EditAnywhere, Category = "Grid|Highlight") FLinearColor NormalColor = FLinearColor(0.15f, 0.15f, 0.15f); - // ハイライト時の色 + // ホバー時の色 UPROPERTY(EditAnywhere, Category = "Grid|Highlight") - FLinearColor HighlightColor = FLinearColor(1.0f, 0.8f, 0.1f); + FLinearColor HoverColor = FLinearColor(1.0f, 0.8f, 0.1f); - // ハイライトのON/OFF - void SetHighlighted(bool bHighlighted); + // 選択時の色 + UPROPERTY(EditAnywhere, Category = "Grid|Highlight") + FLinearColor SelectedColor = FLinearColor(0.1f, 0.6f, 1.0f); + + void SetHovered(bool bInHovered); + void SetSelected(bool bInSelected); protected: virtual void BeginPlay() override; private: + // 状態から色を決めて反映する(優先順位: 選択 > ホバー > 通常) + void RefreshColor(); + + bool bHovered = false; + bool bSelected = false; + // 色変更用のダイナミックマテリアル UPROPERTY() TObjectPtr DynamicMaterial; diff --git a/Source/Tactica/Public/GridManager.h b/Source/Tactica/Public/GridManager.h index 0526dec..2a709ea 100644 --- a/Source/Tactica/Public/GridManager.h +++ b/Source/Tactica/Public/GridManager.h @@ -38,6 +38,12 @@ public: UPROPERTY(VisibleAnywhere, Category = "Grid") TArray> Cells; + // 座標のマスを返す(範囲外はnullptr) + AGridCell* GetCellAt(FIntPoint Coord) const; + + // グリッド全体の中心(ワールド) + FVector GetGridCenter() const; + private: void GenerateGrid(); }; \ No newline at end of file diff --git a/Source/Tactica/Public/GridMath.h b/Source/Tactica/Public/GridMath.h new file mode 100644 index 0000000..1be7882 --- /dev/null +++ b/Source/Tactica/Public/GridMath.h @@ -0,0 +1,23 @@ +#pragma once + +#include "CoreMinimal.h" + +// グリッドの座標計算。Actorに依存しない純粋なロジック +// (将来のユニット移動や射線計算でも使う) +struct TACTICA_API FGridMath +{ + // セル座標 → ワールド座標(セルの中心。Zは Origin.Z) + static FVector CellToWorld(FIntPoint Coord, const FVector& Origin, float CellSize); + + // ワールド座標 → セル座標(四捨五入) + static FIntPoint WorldToCell(const FVector& World, const FVector& Origin, float CellSize); + + // 範囲内か + static bool IsInBounds(FIntPoint Coord, int32 SizeX, int32 SizeY); + + // 配列インデックス(Y * SizeX + X)。範囲外は INDEX_NONE + static int32 CoordToIndex(FIntPoint Coord, int32 SizeX, int32 SizeY); + + // グリッド全体の中心(ワールド) + static FVector GetGridCenter(const FVector& Origin, int32 SizeX, int32 SizeY, float CellSize); +}; diff --git a/Source/Tactica/Public/TacticaCameraPawn.h b/Source/Tactica/Public/TacticaCameraPawn.h index 79fb300..1588c66 100644 --- a/Source/Tactica/Public/TacticaCameraPawn.h +++ b/Source/Tactica/Public/TacticaCameraPawn.h @@ -6,7 +6,7 @@ class UCameraComponent; -// 固定の俯瞰カメラ +// 固定の俯瞰カメラ。BeginPlayでグリッド全体が収まる位置に自動配置する UCLASS() class TACTICA_API ATacticaCameraPawn : public APawn { @@ -17,4 +17,15 @@ public: UPROPERTY(VisibleAnywhere, Category = "Camera") TObjectPtr Camera; + + // 俯角(度。マイナスで見下ろす) + UPROPERTY(EditAnywhere, Category = "Camera") + float Pitch = -60.0f; + + // グリッドに対する余白の倍率 + UPROPERTY(EditAnywhere, Category = "Camera") + float MarginScale = 1.2f; + +protected: + virtual void BeginPlay() override; }; diff --git a/Source/Tactica/Public/TacticaPlayerController.h b/Source/Tactica/Public/TacticaPlayerController.h index da68ee7..34c3798 100644 --- a/Source/Tactica/Public/TacticaPlayerController.h +++ b/Source/Tactica/Public/TacticaPlayerController.h @@ -24,7 +24,11 @@ private: void OnClick(); - // 現在ハイライト中のマス + // 現在ホバー中のマス UPROPERTY() TObjectPtr HoveredCell; + + // 現在選択中のマス + UPROPERTY() + TObjectPtr SelectedCell; };