From b18c19a4f3da1bc1eb49f07214e33a9c214ab8c1 Mon Sep 17 00:00:00 2001 From: yuichipanda Date: Sun, 1 Nov 2026 18:45:27 +0900 Subject: [PATCH] Add units with placement, reachable range display, and movement Co-Authored-By: Claude Sonnet 5.5 --- Source/Tactica/Private/GridCell.cpp | 10 +- Source/Tactica/Private/GridManager.cpp | 117 ++++++++++++++++++ Source/Tactica/Private/GridPathfinder.cpp | 91 ++++++++++++++ Source/Tactica/Private/GridState.cpp | 73 +++++++++++ .../Private/TacticaPlayerController.cpp | 114 +++++++++++++++-- Source/Tactica/Private/Unit.cpp | 71 +++++++++++ Source/Tactica/Public/GridCell.h | 6 + Source/Tactica/Public/GridManager.h | 32 +++++ Source/Tactica/Public/GridPathfinder.h | 32 +++++ Source/Tactica/Public/GridState.h | 50 ++++++++ .../Tactica/Public/TacticaPlayerController.h | 30 ++++- Source/Tactica/Public/Unit.h | 50 ++++++++ Source/Tactica/Public/UnitStats.h | 16 +++ 13 files changed, 682 insertions(+), 10 deletions(-) create mode 100644 Source/Tactica/Private/GridPathfinder.cpp create mode 100644 Source/Tactica/Private/GridState.cpp create mode 100644 Source/Tactica/Private/Unit.cpp create mode 100644 Source/Tactica/Public/GridPathfinder.h create mode 100644 Source/Tactica/Public/GridState.h create mode 100644 Source/Tactica/Public/Unit.h create mode 100644 Source/Tactica/Public/UnitStats.h diff --git a/Source/Tactica/Private/GridCell.cpp b/Source/Tactica/Private/GridCell.cpp index ed5cec4..5031e12 100644 --- a/Source/Tactica/Private/GridCell.cpp +++ b/Source/Tactica/Private/GridCell.cpp @@ -48,6 +48,12 @@ void AGridCell::BeginPlay() RefreshColor(); } +void AGridCell::SetInRange(bool bInInRange) +{ + bInRange = bInInRange; + RefreshColor(); +} + void AGridCell::SetHovered(bool bInHovered) { bHovered = bInHovered; @@ -64,7 +70,9 @@ void AGridCell::RefreshColor() { if (DynamicMaterial) { - const FLinearColor& Color = bSelected ? SelectedColor : (bHovered ? HoverColor : NormalColor); + // 優先順位: 選択 > ホバー > 移動可能 > 通常 + const FLinearColor& Color = bSelected ? SelectedColor + : (bHovered ? HoverColor : (bInRange ? RangeColor : 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 aea81f7..824e355 100644 --- a/Source/Tactica/Private/GridManager.cpp +++ b/Source/Tactica/Private/GridManager.cpp @@ -1,6 +1,8 @@ #include "GridManager.h" #include "GridCell.h" #include "GridMath.h" +#include "GridPathfinder.h" +#include "Unit.h" #include "Engine/World.h" // このファイル専用のログカテゴリ @@ -51,6 +53,7 @@ void AGridManager::GenerateGrid() } Cells.Reset(); + GridState.Init(GridSizeX, GridSizeY); const FVector Origin = GetActorLocation(); for (int32 Y = 0; Y < GridSizeY; ++Y) @@ -70,6 +73,8 @@ void AGridManager::GenerateGrid() { Cell->GridCoord = FIntPoint(X, Y); Cells.Add(Cell); + // 論理データへ歩行可否をコピー(以後は GridState が正) + GridState.SetWalkable(Cell->GridCoord, Cell->bIsWalkable); } else { @@ -79,4 +84,116 @@ void AGridManager::GenerateGrid() } UE_LOG(LogTacticaGrid, Log, TEXT("%d 個のマスを生成しました"), Cells.Num()); + + SpawnUnits(); +} + +void AGridManager::SpawnUnits() +{ + UWorld* World = GetWorld(); + if (!World) + { + return; + } + + if (!UnitClass) + { + UnitClass = AUnit::StaticClass(); + } + + Units.Reset(); + NextUnitId = 0; + const FVector Origin = GetActorLocation(); + + for (const FIntPoint& Coord : InitialUnitCoords) + { + const int32 UnitId = NextUnitId; + + // 先に論理側で配置できるか確認する(範囲外・歩行不可・占有済みなら失敗) + const FGridCellData* Cell = GridState.GetCell(Coord); + if (!Cell || !Cell->bWalkable || Cell->OccupantId != INDEX_NONE) + { + UE_LOG(LogTacticaGrid, Warning, TEXT("ユニットを (%d, %d) に配置できません(範囲外・歩行不可・占有済み)"), + Coord.X, Coord.Y); + continue; + } + + FActorSpawnParameters Params; + Params.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn; + + AUnit* Unit = World->SpawnActor( + UnitClass, FGridMath::CellToWorld(Coord, Origin, CellSize), FRotator::ZeroRotator, Params); + if (!Unit) + { + UE_LOG(LogTacticaGrid, Warning, TEXT("ユニットの生成に失敗 (%d, %d)"), Coord.X, Coord.Y); + continue; + } + + GridState.PlaceUnit(UnitId, Coord); + Unit->UnitId = UnitId; + Unit->GridCoord = Coord; + Units.Add(Unit); + ++NextUnitId; + } + + UE_LOG(LogTacticaGrid, Log, TEXT("%d 体のユニットを配置しました"), Units.Num()); +} + +AUnit* AGridManager::GetUnitAt(FIntPoint Coord) const +{ + const int32 UnitId = GridState.GetOccupant(Coord); + if (UnitId == INDEX_NONE) + { + return nullptr; + } + for (AUnit* Unit : Units) + { + if (Unit && Unit->UnitId == UnitId) + { + return Unit; + } + } + return nullptr; +} + +bool AGridManager::TryMoveUnit(AUnit* Unit, FIntPoint Dest, const FGridReachResult& Reach) +{ + if (!Unit) + { + return false; + } + + const TArray Path = FGridPathfinder::BuildPath(Reach, Unit->GridCoord, Dest); + if (Path.Num() == 0) + { + return false; + } + + // 占有情報は移動の開始時に確定させる(見た目は後から追いかける) + if (!GridState.MoveUnit(Unit->UnitId, Unit->GridCoord, Dest)) + { + return false; + } + + const FVector Origin = GetActorLocation(); + TArray Waypoints; + for (const FIntPoint& Coord : Path) + { + Waypoints.Add(FGridMath::CellToWorld(Coord, Origin, CellSize)); + } + + Unit->GridCoord = Dest; + Unit->MoveAlongPath(Waypoints); + return true; +} + +void AGridManager::SetRangeHighlight(const TArray& Coords, bool bInRange) +{ + for (const FIntPoint& Coord : Coords) + { + if (AGridCell* Cell = GetCellAt(Coord)) + { + Cell->SetInRange(bInRange); + } + } } \ No newline at end of file diff --git a/Source/Tactica/Private/GridPathfinder.cpp b/Source/Tactica/Private/GridPathfinder.cpp new file mode 100644 index 0000000..01a5ada --- /dev/null +++ b/Source/Tactica/Private/GridPathfinder.cpp @@ -0,0 +1,91 @@ +#include "GridPathfinder.h" +#include "GridState.h" +#include "Algo/Reverse.h" + +int32 FGridPathfinder::GetStepCost(const FGridState& State, FIntPoint From, FIntPoint To) +{ + return 1; +} + +FGridReachResult FGridPathfinder::FindReachable(const FGridState& State, FIntPoint Start, int32 MaxCost) +{ + FGridReachResult Result; + if (!State.IsInBounds(Start) || MaxCost < 0) + { + return Result; + } + + static const FIntPoint Directions[4] = { + FIntPoint(1, 0), FIntPoint(-1, 0), FIntPoint(0, 1), FIntPoint(0, -1) }; + + Result.Cost.Add(Start, 0); + + // 未確定のマス。グリッドは小さいので、最小コストの線形探索で十分 + TArray Open; + Open.Add(Start); + + while (Open.Num() > 0) + { + // コスト最小のマスを取り出して確定する + int32 BestIndex = 0; + for (int32 i = 1; i < Open.Num(); ++i) + { + if (Result.Cost[Open[i]] < Result.Cost[Open[BestIndex]]) + { + BestIndex = i; + } + } + const FIntPoint Current = Open[BestIndex]; + Open.RemoveAtSwap(BestIndex); + const int32 CurrentCost = Result.Cost[Current]; + + for (const FIntPoint& Dir : Directions) + { + const FIntPoint Next = Current + Dir; + const FGridCellData* Cell = State.GetCell(Next); + if (!Cell || !Cell->bWalkable || Cell->OccupantId != INDEX_NONE) + { + continue; + } + + const int32 NewCost = CurrentCost + GetStepCost(State, Current, Next); + if (NewCost > MaxCost) + { + continue; + } + + const int32* OldCost = Result.Cost.Find(Next); + if (!OldCost || NewCost < *OldCost) + { + Result.Cost.Add(Next, NewCost); + Result.Parent.Add(Next, Current); + Open.AddUnique(Next); + } + } + } + + return Result; +} + +TArray FGridPathfinder::BuildPath(const FGridReachResult& Reach, FIntPoint Start, FIntPoint Goal) +{ + TArray Path; + if (Start == Goal || !Reach.IsReachable(Goal)) + { + return Path; + } + + FIntPoint Current = Goal; + while (Current != Start) + { + Path.Add(Current); + const FIntPoint* Parent = Reach.Parent.Find(Current); + if (!Parent) + { + return TArray(); // 起点に戻れない(通常は起きない) + } + Current = *Parent; + } + Algo::Reverse(Path); + return Path; +} diff --git a/Source/Tactica/Private/GridState.cpp b/Source/Tactica/Private/GridState.cpp new file mode 100644 index 0000000..7b0820b --- /dev/null +++ b/Source/Tactica/Private/GridState.cpp @@ -0,0 +1,73 @@ +#include "GridState.h" +#include "GridMath.h" + +void FGridState::Init(int32 InSizeX, int32 InSizeY) +{ + SizeX = FMath::Max(InSizeX, 0); + SizeY = FMath::Max(InSizeY, 0); + Cells.Reset(); + Cells.SetNum(SizeX * SizeY); +} + +bool FGridState::IsInBounds(FIntPoint Coord) const +{ + return FGridMath::IsInBounds(Coord, SizeX, SizeY); +} + +const FGridCellData* FGridState::GetCell(FIntPoint Coord) const +{ + const int32 Index = FGridMath::CoordToIndex(Coord, SizeX, SizeY); + return Cells.IsValidIndex(Index) ? &Cells[Index] : nullptr; +} + +FGridCellData* FGridState::GetCellMutable(FIntPoint Coord) +{ + const int32 Index = FGridMath::CoordToIndex(Coord, SizeX, SizeY); + return Cells.IsValidIndex(Index) ? &Cells[Index] : nullptr; +} + +void FGridState::SetWalkable(FIntPoint Coord, bool bWalkable) +{ + if (FGridCellData* Cell = GetCellMutable(Coord)) + { + Cell->bWalkable = bWalkable; + } +} + +int32 FGridState::GetOccupant(FIntPoint Coord) const +{ + const FGridCellData* Cell = GetCell(Coord); + return Cell ? Cell->OccupantId : INDEX_NONE; +} + +bool FGridState::PlaceUnit(int32 UnitId, FIntPoint Coord) +{ + FGridCellData* Cell = GetCellMutable(Coord); + if (!Cell || !Cell->bWalkable || Cell->OccupantId != INDEX_NONE) + { + return false; + } + Cell->OccupantId = UnitId; + return true; +} + +bool FGridState::MoveUnit(int32 UnitId, FIntPoint From, FIntPoint To) +{ + FGridCellData* FromCell = GetCellMutable(From); + FGridCellData* ToCell = GetCellMutable(To); + if (!FromCell || !ToCell || FromCell->OccupantId != UnitId) + { + return false; + } + if (From == To) + { + return true; + } + if (!ToCell->bWalkable || ToCell->OccupantId != INDEX_NONE) + { + return false; + } + FromCell->OccupantId = INDEX_NONE; + ToCell->OccupantId = UnitId; + return true; +} diff --git a/Source/Tactica/Private/TacticaPlayerController.cpp b/Source/Tactica/Private/TacticaPlayerController.cpp index 4dd9367..84ff322 100644 --- a/Source/Tactica/Private/TacticaPlayerController.cpp +++ b/Source/Tactica/Private/TacticaPlayerController.cpp @@ -1,7 +1,10 @@ #include "TacticaPlayerController.h" #include "GridCell.h" +#include "GridManager.h" +#include "Unit.h" #include "Components/InputComponent.h" #include "InputCoreTypes.h" +#include "Kismet/GameplayStatics.h" DEFINE_LOG_CATEGORY_STATIC(LogTacticaInput, Log, All); @@ -57,21 +60,116 @@ AGridCell* ATacticaPlayerController::GetCellUnderCursor() const return nullptr; } +AGridManager* ATacticaPlayerController::GetGridManager() +{ + if (!GridManager) + { + GridManager = Cast( + UGameplayStatics::GetActorOfClass(this, AGridManager::StaticClass())); + } + return GridManager; +} + void ATacticaPlayerController::OnClick() { - AGridCell* Cell = GetCellUnderCursor(); - if (!Cell) + if (State == EState::Moving) { - return; // マス以外のクリックでは選択を変えない + UE_LOG(LogTacticaInput, Log, TEXT("移動中のため入力を無視しました")); + return; } + AGridCell* Cell = GetCellUnderCursor(); + AGridManager* Grid = GetGridManager(); + if (!Cell || !Grid) + { + return; // マス以外のクリックでは何も変えない + } + + const FIntPoint Coord = Cell->GridCoord; + UE_LOG(LogTacticaInput, Log, TEXT("Cell clicked: (%d, %d)"), Coord.X, Coord.Y); + + if (State == EState::Idle) + { + if (AUnit* Unit = Grid->GetUnitAt(Coord)) + { + SelectUnit(Unit, Cell); + } + else + { + UE_LOG(LogTacticaInput, Log, TEXT("(%d, %d) にユニットはいません"), Coord.X, Coord.Y); + } + return; + } + + // UnitSelected: 移動可能なマス(起点を除く)だけ受け付ける + if (SelectedUnit && Coord != SelectedUnit->GridCoord && ReachResult.IsReachable(Coord)) + { + if (Grid->TryMoveUnit(SelectedUnit, Coord, ReachResult)) + { + State = EState::Moving; + SelectedUnit->OnMoveFinished.AddUObject(this, &ATacticaPlayerController::OnUnitMoveFinished); + UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d を (%d, %d) へ移動開始"), + SelectedUnit->UnitId, Coord.X, Coord.Y); + return; + } + } + + UE_LOG(LogTacticaInput, Log, TEXT("(%d, %d) には移動できません"), Coord.X, Coord.Y); +} + +void ATacticaPlayerController::SelectUnit(AUnit* Unit, AGridCell* Cell) +{ + AGridManager* Grid = GetGridManager(); + if (!Unit || !Cell || !Grid) + { + return; + } + + SelectedUnit = Unit; + SelectedCell = Cell; + SelectedCell->SetSelected(true); + + ReachResult = FGridPathfinder::FindReachable(Grid->GetGridState(), Unit->GridCoord, Unit->Stats.MoveRange); + + RangeCoords.Reset(); + for (const TPair& Pair : ReachResult.Cost) + { + if (Pair.Key != Unit->GridCoord) + { + RangeCoords.Add(Pair.Key); + } + } + Grid->SetRangeHighlight(RangeCoords, true); + + State = EState::UnitSelected; + UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d を選択 (移動力 %d, 移動可能 %d マス)"), + Unit->UnitId, Unit->Stats.MoveRange, RangeCoords.Num()); +} + +void ATacticaPlayerController::ClearSelection() +{ + if (AGridManager* Grid = GetGridManager()) + { + Grid->SetRangeHighlight(RangeCoords, false); + } + RangeCoords.Reset(); + ReachResult = FGridReachResult(); + 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); + SelectedCell = nullptr; + SelectedUnit = nullptr; + State = EState::Idle; +} + +void ATacticaPlayerController::OnUnitMoveFinished() +{ + if (SelectedUnit) + { + SelectedUnit->OnMoveFinished.RemoveAll(this); + UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d の移動完了"), SelectedUnit->UnitId); + } + ClearSelection(); } diff --git a/Source/Tactica/Private/Unit.cpp b/Source/Tactica/Private/Unit.cpp new file mode 100644 index 0000000..c46f270 --- /dev/null +++ b/Source/Tactica/Private/Unit.cpp @@ -0,0 +1,71 @@ +#include "Unit.h" +#include "Components/StaticMeshComponent.h" +#include "Engine/StaticMesh.h" +#include "UObject/ConstructorHelpers.h" + +AUnit::AUnit() +{ + // Tickは移動中だけ有効にする + PrimaryActorTick.bCanEverTick = true; + PrimaryActorTick.bStartWithTickEnabled = false; + + USceneComponent* Root = CreateDefaultSubobject(TEXT("Root")); + SetRootComponent(Root); + + MeshComponent = CreateDefaultSubobject(TEXT("MeshComponent")); + MeshComponent->SetupAttachment(Root); + + // 仮の見た目: 標準のシリンダー(100x100x100cm)を細くする + static ConstructorHelpers::FObjectFinder CylinderAsset( + TEXT("/Engine/BasicShapes/Cylinder.Cylinder")); + if (CylinderAsset.Succeeded()) + { + MeshComponent->SetStaticMesh(CylinderAsset.Object); + } + MeshComponent->SetRelativeScale3D(FVector(0.4f, 0.4f, 1.0f)); + // 底面がActorの原点(マスの上面)に来るよう、高さの半分(50cm)上げる + MeshComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 50.0f)); + + // マスのクリック判定をユニットが邪魔しないよう、当たり判定は無効にする + MeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision); +} + +void AUnit::MoveAlongPath(const TArray& InWaypoints) +{ + Waypoints = InWaypoints; + WaypointIndex = 0; + + if (Waypoints.Num() == 0) + { + OnMoveFinished.Broadcast(); + return; + } + SetActorTickEnabled(true); +} + +void AUnit::Tick(float DeltaTime) +{ + Super::Tick(DeltaTime); + + if (!IsMoving()) + { + SetActorTickEnabled(false); + return; + } + + const FVector Target = Waypoints[WaypointIndex]; + const FVector NewLocation = FMath::VInterpConstantTo(GetActorLocation(), Target, DeltaTime, MoveSpeed); + SetActorLocation(NewLocation); + + if (NewLocation.Equals(Target, 0.1f)) + { + ++WaypointIndex; + if (!IsMoving()) + { + Waypoints.Reset(); + WaypointIndex = 0; + SetActorTickEnabled(false); + OnMoveFinished.Broadcast(); + } + } +} diff --git a/Source/Tactica/Public/GridCell.h b/Source/Tactica/Public/GridCell.h index 7c3ecd7..889ad6e 100644 --- a/Source/Tactica/Public/GridCell.h +++ b/Source/Tactica/Public/GridCell.h @@ -43,6 +43,11 @@ public: UPROPERTY(EditAnywhere, Category = "Grid|Highlight") FLinearColor SelectedColor = FLinearColor(0.1f, 0.6f, 1.0f); + // 移動可能範囲の色 + UPROPERTY(EditAnywhere, Category = "Grid|Highlight") + FLinearColor RangeColor = FLinearColor(0.2f, 0.8f, 0.3f); + + void SetInRange(bool bInInRange); void SetHovered(bool bInHovered); void SetSelected(bool bInSelected); @@ -53,6 +58,7 @@ private: // 状態から色を決めて反映する(優先順位: 選択 > ホバー > 通常) void RefreshColor(); + bool bInRange = false; bool bHovered = false; bool bSelected = false; diff --git a/Source/Tactica/Public/GridManager.h b/Source/Tactica/Public/GridManager.h index 2a709ea..625aacd 100644 --- a/Source/Tactica/Public/GridManager.h +++ b/Source/Tactica/Public/GridManager.h @@ -2,9 +2,12 @@ #include "CoreMinimal.h" #include "GameFramework/Actor.h" +#include "GridState.h" #include "GridManager.generated.h" class AGridCell; +class AUnit; +struct FGridReachResult; UCLASS() class TACTICA_API AGridManager : public AActor @@ -38,6 +41,31 @@ public: UPROPERTY(VisibleAnywhere, Category = "Grid") TArray> Cells; + // スポーンするユニットのクラス(未設定ならAUnit) + UPROPERTY(EditAnywhere, Category = "Units") + TSubclassOf UnitClass; + + // ユニットの初期配置座標(1要素につき1体) + UPROPERTY(EditAnywhere, Category = "Units") + TArray InitialUnitCoords = { FIntPoint(0, 0), FIntPoint(2, 2) }; + + // 生成したユニットの一覧 + UPROPERTY(VisibleAnywhere, Category = "Units") + TArray> Units; + + // 論理データ(歩行可否・占有)。経路探索などはこれを読む + const FGridState& GetGridState() const { return GridState; } + + // 座標にいるユニットを返す(いなければnullptr) + AUnit* GetUnitAt(FIntPoint Coord) const; + + // ユニットを Dest へ移動させる。占有情報を更新し、見た目の移動を開始する + // Reach は、ユニットの現在位置から求めた FindReachable の結果 + bool TryMoveUnit(AUnit* Unit, FIntPoint Dest, const FGridReachResult& Reach); + + // 指定したマスの「移動可能」表示をON/OFFする + void SetRangeHighlight(const TArray& Coords, bool bInRange); + // 座標のマスを返す(範囲外はnullptr) AGridCell* GetCellAt(FIntPoint Coord) const; @@ -46,4 +74,8 @@ public: private: void GenerateGrid(); + void SpawnUnits(); + + FGridState GridState; + int32 NextUnitId = 0; }; \ No newline at end of file diff --git a/Source/Tactica/Public/GridPathfinder.h b/Source/Tactica/Public/GridPathfinder.h new file mode 100644 index 0000000..60f5547 --- /dev/null +++ b/Source/Tactica/Public/GridPathfinder.h @@ -0,0 +1,32 @@ +#pragma once + +#include "CoreMinimal.h" + +class FGridState; + +// 到達可能マスの探索結果 +struct FGridReachResult +{ + // 到達可能なマス → 起点からの総コスト(起点自身も0で含む) + TMap Cost; + + // 各マスへ最短で来るときの1つ前のマス(起点は含まない) + TMap Parent; + + bool IsReachable(FIntPoint Coord) const { return Cost.Contains(Coord); } +}; + +// 経路探索。UObject/Actorに依存しない純粋なロジック +struct TACTICA_API FGridPathfinder +{ + // 起点から MaxCost 以内で到達できるマスを求める(ダイクストラ、上下左右4方向) + // 歩行不可のマス、他ユニットのいるマスは通れない。起点は占有されていてもよい + static FGridReachResult FindReachable(const FGridState& State, FIntPoint Start, int32 MaxCost); + + // 探索結果から Start → Goal の経路を復元する(起点は含まず、Goalを最後に含む) + // 到達不可、または Start == Goal の場合は空 + static TArray BuildPath(const FGridReachResult& Reach, FIntPoint Start, FIntPoint Goal); + + // 隣接マスへ1歩進むコスト。今は常に1。高低差や地形コストを入れるときはここを変える + static int32 GetStepCost(const FGridState& State, FIntPoint From, FIntPoint To); +}; diff --git a/Source/Tactica/Public/GridState.h b/Source/Tactica/Public/GridState.h new file mode 100644 index 0000000..69e9cdb --- /dev/null +++ b/Source/Tactica/Public/GridState.h @@ -0,0 +1,50 @@ +#pragma once + +#include "CoreMinimal.h" + +// 1マス分の論理データ(見た目に依存しない) +struct FGridCellData +{ + // 歩行可能か + bool bWalkable = true; + + // 高さ(階層)。今は未使用だが経路コストや視線判定で使う予定 + int32 Height = 0; + + // 占有しているユニットのID(いなければ INDEX_NONE) + int32 OccupantId = INDEX_NONE; +}; + +// グリッドの論理状態。UObject/Actorに依存しない純粋なC++クラス +// 経路探索・視線判定・AI評価などから使い回す +class TACTICA_API FGridState +{ +public: + void Init(int32 InSizeX, int32 InSizeY); + + int32 GetSizeX() const { return SizeX; } + int32 GetSizeY() const { return SizeY; } + + bool IsInBounds(FIntPoint Coord) const; + + // 範囲外は nullptr + const FGridCellData* GetCell(FIntPoint Coord) const; + + void SetWalkable(FIntPoint Coord, bool bWalkable); + + // 占有しているユニットのID。いない/範囲外は INDEX_NONE + int32 GetOccupant(FIntPoint Coord) const; + + // ユニットを配置する。範囲外・歩行不可・占有済みなら false + bool PlaceUnit(int32 UnitId, FIntPoint Coord); + + // ユニットを移動する。From に UnitId がいない、To が使えない場合は false + bool MoveUnit(int32 UnitId, FIntPoint From, FIntPoint To); + +private: + FGridCellData* GetCellMutable(FIntPoint Coord); + + int32 SizeX = 0; + int32 SizeY = 0; + TArray Cells; +}; diff --git a/Source/Tactica/Public/TacticaPlayerController.h b/Source/Tactica/Public/TacticaPlayerController.h index 34c3798..f75921c 100644 --- a/Source/Tactica/Public/TacticaPlayerController.h +++ b/Source/Tactica/Public/TacticaPlayerController.h @@ -2,9 +2,12 @@ #include "CoreMinimal.h" #include "GameFramework/PlayerController.h" +#include "GridPathfinder.h" #include "TacticaPlayerController.generated.h" class AGridCell; +class AGridManager; +class AUnit; UCLASS() class TACTICA_API ATacticaPlayerController : public APlayerController @@ -19,16 +22,41 @@ protected: virtual void PlayerTick(float DeltaTime) override; private: + // 操作の状態 + enum class EState : uint8 + { + Idle, // 何も選んでいない + UnitSelected, // ユニットを選択中(移動範囲を表示中) + Moving // ユニットが移動中(入力を受け付けない) + }; + // カーソル下のマスを返す(なければnullptr) AGridCell* GetCellUnderCursor() const; + AGridManager* GetGridManager(); + void OnClick(); + void SelectUnit(AUnit* Unit, AGridCell* Cell); + void ClearSelection(); + void OnUnitMoveFinished(); + + EState State = EState::Idle; // 現在ホバー中のマス UPROPERTY() TObjectPtr HoveredCell; - // 現在選択中のマス + // 選択中のユニットと、そのユニットが乗っていたマス + UPROPERTY() + TObjectPtr SelectedUnit; + UPROPERTY() TObjectPtr SelectedCell; + + UPROPERTY() + TObjectPtr GridManager; + + // 選択中ユニットの移動可能範囲(起点を除く)と探索結果 + TArray RangeCoords; + FGridReachResult ReachResult; }; diff --git a/Source/Tactica/Public/Unit.h b/Source/Tactica/Public/Unit.h new file mode 100644 index 0000000..bcb9b11 --- /dev/null +++ b/Source/Tactica/Public/Unit.h @@ -0,0 +1,50 @@ +#pragma once + +#include "CoreMinimal.h" +#include "GameFramework/Actor.h" +#include "UnitStats.h" +#include "Unit.generated.h" + +class UStaticMeshComponent; + +// ユニット(兵士)。見た目と移動の補間だけを担当し、占有などの論理はAGridManager/FGridStateが持つ +UCLASS() +class TACTICA_API AUnit : public AActor +{ + GENERATED_BODY() + +public: + AUnit(); + + virtual void Tick(float DeltaTime) override; + + // ユニットID(AGridManagerが割り振る) + UPROPERTY(VisibleAnywhere, Category = "Unit") + int32 UnitId = INDEX_NONE; + + // 現在いるマスの座標(論理上の位置) + UPROPERTY(VisibleAnywhere, Category = "Unit") + FIntPoint GridCoord = FIntPoint::ZeroValue; + + UPROPERTY(EditAnywhere, Category = "Unit") + FUnitStats Stats; + + // 移動速度(cm/秒) + UPROPERTY(EditAnywhere, Category = "Unit") + float MoveSpeed = 400.0f; + + // ウェイポイントを順に通って移動する + void MoveAlongPath(const TArray& InWaypoints); + + bool IsMoving() const { return WaypointIndex < Waypoints.Num(); } + + // 移動完了時に呼ばれる + FSimpleMulticastDelegate OnMoveFinished; + +private: + UPROPERTY(VisibleAnywhere, Category = "Unit") + TObjectPtr MeshComponent; + + TArray Waypoints; + int32 WaypointIndex = 0; +}; diff --git a/Source/Tactica/Public/UnitStats.h b/Source/Tactica/Public/UnitStats.h new file mode 100644 index 0000000..9fa240b --- /dev/null +++ b/Source/Tactica/Public/UnitStats.h @@ -0,0 +1,16 @@ +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DataTable.h" +#include "UnitStats.generated.h" + +// ユニットのステータス。将来はDataTableの行として読み込めるようFTableRowBaseを継承する +USTRUCT(BlueprintType) +struct FUnitStats : public FTableRowBase +{ + GENERATED_BODY() + + // 移動力(マス数) + UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Unit") + int32 MoveRange = 4; +};