Add units with placement, reachable range display, and movement
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3a8082775f
commit
b18c19a4f3
13 files changed
+682
-10
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<AUnit>(
|
||||
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<FIntPoint> 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<FVector> 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<FIntPoint>& Coords, bool bInRange)
|
||||
{
|
||||
for (const FIntPoint& Coord : Coords)
|
||||
{
|
||||
if (AGridCell* Cell = GetCellAt(Coord))
|
||||
{
|
||||
Cell->SetInRange(bInRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<FIntPoint> 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<FIntPoint> FGridPathfinder::BuildPath(const FGridReachResult& Reach, FIntPoint Start, FIntPoint Goal)
|
||||
{
|
||||
TArray<FIntPoint> 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<FIntPoint>(); // 起点に戻れない(通常は起きない)
|
||||
}
|
||||
Current = *Parent;
|
||||
}
|
||||
Algo::Reverse(Path);
|
||||
return Path;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<AGridManager>(
|
||||
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<FIntPoint, int32>& 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();
|
||||
}
|
||||
@@ -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<USceneComponent>(TEXT("Root"));
|
||||
SetRootComponent(Root);
|
||||
|
||||
MeshComponent = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("MeshComponent"));
|
||||
MeshComponent->SetupAttachment(Root);
|
||||
|
||||
// 仮の見た目: 標準のシリンダー(100x100x100cm)を細くする
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> 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<FVector>& 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<TObjectPtr<AGridCell>> Cells;
|
||||
|
||||
// スポーンするユニットのクラス(未設定ならAUnit)
|
||||
UPROPERTY(EditAnywhere, Category = "Units")
|
||||
TSubclassOf<AUnit> UnitClass;
|
||||
|
||||
// ユニットの初期配置座標(1要素につき1体)
|
||||
UPROPERTY(EditAnywhere, Category = "Units")
|
||||
TArray<FIntPoint> InitialUnitCoords = { FIntPoint(0, 0), FIntPoint(2, 2) };
|
||||
|
||||
// 生成したユニットの一覧
|
||||
UPROPERTY(VisibleAnywhere, Category = "Units")
|
||||
TArray<TObjectPtr<AUnit>> 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<FIntPoint>& Coords, bool bInRange);
|
||||
|
||||
// 座標のマスを返す(範囲外はnullptr)
|
||||
AGridCell* GetCellAt(FIntPoint Coord) const;
|
||||
|
||||
@@ -46,4 +74,8 @@ public:
|
||||
|
||||
private:
|
||||
void GenerateGrid();
|
||||
void SpawnUnits();
|
||||
|
||||
FGridState GridState;
|
||||
int32 NextUnitId = 0;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class FGridState;
|
||||
|
||||
// 到達可能マスの探索結果
|
||||
struct FGridReachResult
|
||||
{
|
||||
// 到達可能なマス → 起点からの総コスト(起点自身も0で含む)
|
||||
TMap<FIntPoint, int32> Cost;
|
||||
|
||||
// 各マスへ最短で来るときの1つ前のマス(起点は含まない)
|
||||
TMap<FIntPoint, FIntPoint> 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<FIntPoint> BuildPath(const FGridReachResult& Reach, FIntPoint Start, FIntPoint Goal);
|
||||
|
||||
// 隣接マスへ1歩進むコスト。今は常に1。高低差や地形コストを入れるときはここを変える
|
||||
static int32 GetStepCost(const FGridState& State, FIntPoint From, FIntPoint To);
|
||||
};
|
||||
@@ -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<FGridCellData> Cells;
|
||||
};
|
||||
@@ -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<AGridCell> HoveredCell;
|
||||
|
||||
// 現在選択中のマス
|
||||
// 選択中のユニットと、そのユニットが乗っていたマス
|
||||
UPROPERTY()
|
||||
TObjectPtr<AUnit> SelectedUnit;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<AGridCell> SelectedCell;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<AGridManager> GridManager;
|
||||
|
||||
// 選択中ユニットの移動可能範囲(起点を除く)と探索結果
|
||||
TArray<FIntPoint> RangeCoords;
|
||||
FGridReachResult ReachResult;
|
||||
};
|
||||
@@ -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<FVector>& InWaypoints);
|
||||
|
||||
bool IsMoving() const { return WaypointIndex < Waypoints.Num(); }
|
||||
|
||||
// 移動完了時に呼ばれる
|
||||
FSimpleMulticastDelegate OnMoveFinished;
|
||||
|
||||
private:
|
||||
UPROPERTY(VisibleAnywhere, Category = "Unit")
|
||||
TObjectPtr<UStaticMeshComponent> MeshComponent;
|
||||
|
||||
TArray<FVector> Waypoints;
|
||||
int32 WaypointIndex = 0;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user