Add units with placement, reachable range display, and movement

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
yuichipandaandClaude Sonnet 5.5 committed 2026-11-01 18:45:27 +09:00
1 parent 3a8082775f
commit b18c19a4f3
13 files changed
+682 -10

No files matched your search

+9 -1
View File
@@ -48,6 +48,12 @@ void AGridCell::BeginPlay()
RefreshColor(); RefreshColor();
} }
void AGridCell::SetInRange(bool bInInRange)
{
bInRange = bInInRange;
RefreshColor();
}
void AGridCell::SetHovered(bool bInHovered) void AGridCell::SetHovered(bool bInHovered)
{ {
bHovered = bInHovered; bHovered = bInHovered;
@@ -64,7 +70,9 @@ void AGridCell::RefreshColor()
{ {
if (DynamicMaterial) 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); DynamicMaterial->SetVectorParameterValue(TEXT("Color"), Color);
} }
} }
+117
View File
@@ -1,6 +1,8 @@
#include "GridManager.h" #include "GridManager.h"
#include "GridCell.h" #include "GridCell.h"
#include "GridMath.h" #include "GridMath.h"
#include "GridPathfinder.h"
#include "Unit.h"
#include "Engine/World.h" #include "Engine/World.h"
// このファイル専用のログカテゴリ // このファイル専用のログカテゴリ
@@ -51,6 +53,7 @@ void AGridManager::GenerateGrid()
} }
Cells.Reset(); Cells.Reset();
GridState.Init(GridSizeX, GridSizeY);
const FVector Origin = GetActorLocation(); const FVector Origin = GetActorLocation();
for (int32 Y = 0; Y < GridSizeY; ++Y) for (int32 Y = 0; Y < GridSizeY; ++Y)
@@ -70,6 +73,8 @@ void AGridManager::GenerateGrid()
{ {
Cell->GridCoord = FIntPoint(X, Y); Cell->GridCoord = FIntPoint(X, Y);
Cells.Add(Cell); Cells.Add(Cell);
// 論理データへ歩行可否をコピー(以後は GridState が正)
GridState.SetWalkable(Cell->GridCoord, Cell->bIsWalkable);
} }
else else
{ {
@@ -79,4 +84,116 @@ void AGridManager::GenerateGrid()
} }
UE_LOG(LogTacticaGrid, Log, TEXT("%d 個のマスを生成しました"), Cells.Num()); 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);
}
}
} }
+91
View File
@@ -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;
}
+73
View File
@@ -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 "TacticaPlayerController.h"
#include "GridCell.h" #include "GridCell.h"
#include "GridManager.h"
#include "Unit.h"
#include "Components/InputComponent.h" #include "Components/InputComponent.h"
#include "InputCoreTypes.h" #include "InputCoreTypes.h"
#include "Kismet/GameplayStatics.h"
DEFINE_LOG_CATEGORY_STATIC(LogTacticaInput, Log, All); DEFINE_LOG_CATEGORY_STATIC(LogTacticaInput, Log, All);
@@ -57,21 +60,116 @@ AGridCell* ATacticaPlayerController::GetCellUnderCursor() const
return nullptr; return nullptr;
} }
AGridManager* ATacticaPlayerController::GetGridManager()
{
if (!GridManager)
{
GridManager = Cast<AGridManager>(
UGameplayStatics::GetActorOfClass(this, AGridManager::StaticClass()));
}
return GridManager;
}
void ATacticaPlayerController::OnClick() void ATacticaPlayerController::OnClick()
{ {
AGridCell* Cell = GetCellUnderCursor(); if (State == EState::Moving)
if (!Cell)
{ {
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) if (SelectedCell)
{ {
SelectedCell->SetSelected(false); SelectedCell->SetSelected(false);
} }
SelectedCell = Cell; SelectedCell = nullptr;
SelectedCell->SetSelected(true); SelectedUnit = nullptr;
State = EState::Idle;
UE_LOG(LogTacticaInput, Log, TEXT("Cell clicked: (%d, %d)"), }
Cell->GridCoord.X, Cell->GridCoord.Y);
void ATacticaPlayerController::OnUnitMoveFinished()
{
if (SelectedUnit)
{
SelectedUnit->OnMoveFinished.RemoveAll(this);
UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d の移動完了"), SelectedUnit->UnitId);
}
ClearSelection();
} }
+71
View File
@@ -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();
}
}
}
+6
View File
@@ -43,6 +43,11 @@ public:
UPROPERTY(EditAnywhere, Category = "Grid|Highlight") UPROPERTY(EditAnywhere, Category = "Grid|Highlight")
FLinearColor SelectedColor = FLinearColor(0.1f, 0.6f, 1.0f); 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 SetHovered(bool bInHovered);
void SetSelected(bool bInSelected); void SetSelected(bool bInSelected);
@@ -53,6 +58,7 @@ private:
// 状態から色を決めて反映する(優先順位: 選択 > ホバー > 通常) // 状態から色を決めて反映する(優先順位: 選択 > ホバー > 通常)
void RefreshColor(); void RefreshColor();
bool bInRange = false;
bool bHovered = false; bool bHovered = false;
bool bSelected = false; bool bSelected = false;
+32
View File
@@ -2,9 +2,12 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "GameFramework/Actor.h" #include "GameFramework/Actor.h"
#include "GridState.h"
#include "GridManager.generated.h" #include "GridManager.generated.h"
class AGridCell; class AGridCell;
class AUnit;
struct FGridReachResult;
UCLASS() UCLASS()
class TACTICA_API AGridManager : public AActor class TACTICA_API AGridManager : public AActor
@@ -38,6 +41,31 @@ public:
UPROPERTY(VisibleAnywhere, Category = "Grid") UPROPERTY(VisibleAnywhere, Category = "Grid")
TArray<TObjectPtr<AGridCell>> Cells; 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) // 座標のマスを返す(範囲外はnullptr)
AGridCell* GetCellAt(FIntPoint Coord) const; AGridCell* GetCellAt(FIntPoint Coord) const;
@@ -46,4 +74,8 @@ public:
private: private:
void GenerateGrid(); void GenerateGrid();
void SpawnUnits();
FGridState GridState;
int32 NextUnitId = 0;
}; };
+32
View File
@@ -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);
};
+50
View File
@@ -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 "CoreMinimal.h"
#include "GameFramework/PlayerController.h" #include "GameFramework/PlayerController.h"
#include "GridPathfinder.h"
#include "TacticaPlayerController.generated.h" #include "TacticaPlayerController.generated.h"
class AGridCell; class AGridCell;
class AGridManager;
class AUnit;
UCLASS() UCLASS()
class TACTICA_API ATacticaPlayerController : public APlayerController class TACTICA_API ATacticaPlayerController : public APlayerController
@@ -19,16 +22,41 @@ protected:
virtual void PlayerTick(float DeltaTime) override; virtual void PlayerTick(float DeltaTime) override;
private: private:
// 操作の状態
enum class EState : uint8
{
Idle, // 何も選んでいない
UnitSelected, // ユニットを選択中(移動範囲を表示中)
Moving // ユニットが移動中(入力を受け付けない)
};
// カーソル下のマスを返す(なければnullptr) // カーソル下のマスを返す(なければnullptr)
AGridCell* GetCellUnderCursor() const; AGridCell* GetCellUnderCursor() const;
AGridManager* GetGridManager();
void OnClick(); void OnClick();
void SelectUnit(AUnit* Unit, AGridCell* Cell);
void ClearSelection();
void OnUnitMoveFinished();
EState State = EState::Idle;
// 現在ホバー中のマス // 現在ホバー中のマス
UPROPERTY() UPROPERTY()
TObjectPtr<AGridCell> HoveredCell; TObjectPtr<AGridCell> HoveredCell;
// 現在選択中のマス // 選択中のユニットと、そのユニットが乗っていたマス
UPROPERTY()
TObjectPtr<AUnit> SelectedUnit;
UPROPERTY() UPROPERTY()
TObjectPtr<AGridCell> SelectedCell; TObjectPtr<AGridCell> SelectedCell;
UPROPERTY()
TObjectPtr<AGridManager> GridManager;
// 選択中ユニットの移動可能範囲(起点を除く)と探索結果
TArray<FIntPoint> RangeCoords;
FGridReachResult ReachResult;
}; };
+50
View File
@@ -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;
};
+16
View File
@@ -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;
};