Add turn system, two-action rule with dash, and HUD

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
yuichipandaandClaude Sonnet 5.5 committed 2026-11-01 18:53:40 +09:00
1 parent 41709fdd65
commit f3f0c080a9
17 files changed
+546 -31

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
#include "ActionRules.h"
int32 FActionRules::GetCost(EActionType Type, int32 ActionPointsLeft)
{
switch (Type)
{
case EActionType::Dash:
return ActionPointsLeft; // 残りを全て消費
case EActionType::Move:
default:
return 1;
}
}
bool FActionRules::CanPerform(EActionType Type, int32 ActionPointsLeft, int32 ActionsPerTurn)
{
switch (Type)
{
case EActionType::Dash:
return ActionPointsLeft > 0 && ActionPointsLeft >= ActionsPerTurn;
case EActionType::Move:
default:
return ActionPointsLeft >= 1;
}
}
int32 FActionRules::GetDashRange(int32 MoveRange, float DashMultiplier)
{
return FMath::Max(MoveRange, FMath::FloorToInt(MoveRange * DashMultiplier));
}
EActionType FActionRules::ChooseMoveAction(int32 PathCost, int32 MoveRange)
{
return PathCost <= MoveRange ? EActionType::Move : EActionType::Dash;
}
+13 -4
View File
@@ -54,6 +54,12 @@ void AGridCell::SetInRange(bool bInInRange)
RefreshColor();
}
void AGridCell::SetInDashRange(bool bInInDashRange)
{
bInDashRange = bInInDashRange;
RefreshColor();
}
void AGridCell::SetHovered(bool bInHovered)
{
bHovered = bInHovered;
@@ -70,9 +76,12 @@ void AGridCell::RefreshColor()
{
if (DynamicMaterial)
{
// 優先順位: 選択 > ホバー > 移動可能 > 通常
const FLinearColor& Color = bSelected ? SelectedColor
: (bHovered ? HoverColor : (bInRange ? RangeColor : NormalColor));
DynamicMaterial->SetVectorParameterValue(TEXT("Color"), Color);
// 優先順位: 選択 > ホバー > 移動可能 > ダッシュ可能 > 通常
const FLinearColor* Color = &NormalColor;
if (bSelected) { Color = &SelectedColor; }
else if (bHovered) { Color = &HoverColor; }
else if (bInRange) { Color = &RangeColor; }
else if (bInDashRange) { Color = &DashRangeColor; }
DynamicMaterial->SetVectorParameterValue(TEXT("Color"), *Color);
}
}
+61 -3
View File
@@ -21,6 +21,54 @@ void AGridManager::BeginPlay()
GridSizeX, GridSizeY, CellSize);
GenerateGrid();
// ターン開始の通知を購読してから、最初のターンを始める
TurnManager.OnTurnStarted.AddUObject(this, &AGridManager::HandleTurnStarted);
TurnManager.Start(EFaction::Player);
}
void AGridManager::HandleTurnStarted(int32 TurnNumber, EFaction Faction)
{
UE_LOG(LogTacticaGrid, Log, TEXT("ターン %d 開始: %s"), TurnNumber,
Faction == EFaction::Player ? TEXT("Player") : TEXT("Enemy"));
// その陣営の全ユニットの行動ポイントを回復する
for (AUnit* Unit : Units)
{
if (Unit && Unit->Faction == Faction)
{
Unit->RestoreActions();
}
}
if (Faction == EFaction::Enemy)
{
RunEnemyTurn();
}
}
void AGridManager::RunEnemyTurn()
{
UE_LOG(LogTacticaGrid, Log, TEXT("敵ターン(仮): %.1f 秒後にPlayerターンへ戻ります"), EnemyTurnDelay);
GetWorldTimerManager().SetTimer(EnemyTurnTimer, this, &AGridManager::FinishEnemyTurn,
FMath::Max(EnemyTurnDelay, 0.01f), false);
}
void AGridManager::FinishEnemyTurn()
{
TurnManager.EndTurn();
}
bool AGridManager::HasActionsLeft(EFaction Faction) const
{
for (const AUnit* Unit : Units)
{
if (Unit && Unit->Faction == Faction && Unit->ActionPoints > 0)
{
return true;
}
}
return false;
}
AGridCell* AGridManager::GetCellAt(FIntPoint Coord) const
@@ -105,8 +153,9 @@ void AGridManager::SpawnUnits()
NextUnitId = 0;
const FVector Origin = GetActorLocation();
for (const FIntPoint& Coord : InitialUnitCoords)
for (const FInitialUnit& Entry : InitialUnits)
{
const FIntPoint Coord = Entry.Coord;
const int32 UnitId = NextUnitId;
// 先に論理側で配置できるか確認する(範囲外・歩行不可・占有済みなら失敗)
@@ -132,6 +181,8 @@ void AGridManager::SpawnUnits()
GridState.PlaceUnit(UnitId, Coord);
Unit->UnitId = UnitId;
Unit->GridCoord = Coord;
Unit->SetFaction(Entry.Faction);
Unit->RestoreActions();
Units.Add(Unit);
++NextUnitId;
}
@@ -187,13 +238,20 @@ bool AGridManager::TryMoveUnit(AUnit* Unit, FIntPoint Dest, const FGridReachResu
return true;
}
void AGridManager::SetRangeHighlight(const TArray<FIntPoint>& Coords, bool bInRange)
void AGridManager::SetRangeHighlight(const TArray<FIntPoint>& Coords, bool bInRange, bool bDash)
{
for (const FIntPoint& Coord : Coords)
{
if (AGridCell* Cell = GetCellAt(Coord))
{
Cell->SetInRange(bInRange);
if (bDash)
{
Cell->SetInDashRange(bInRange);
}
else
{
Cell->SetInRange(bInRange);
}
}
}
}
@@ -1,9 +1,11 @@
#include "TacticaGameMode.h"
#include "TacticaPlayerController.h"
#include "TacticaCameraPawn.h"
#include "TacticaHUD.h"
ATacticaGameMode::ATacticaGameMode()
{
DefaultPawnClass = ATacticaCameraPawn::StaticClass();
PlayerControllerClass = ATacticaPlayerController::StaticClass();
HUDClass = ATacticaHUD::StaticClass();
}
+44
View File
@@ -0,0 +1,44 @@
#include "TacticaHUD.h"
#include "GridManager.h"
#include "TacticaPlayerController.h"
#include "Unit.h"
#include "Engine/Canvas.h"
#include "Engine/Engine.h"
#include "Kismet/GameplayStatics.h"
void ATacticaHUD::DrawHUD()
{
Super::DrawHUD();
if (!GridManager)
{
GridManager = Cast<AGridManager>(
UGameplayStatics::GetActorOfClass(this, AGridManager::StaticClass()));
}
if (!GridManager || !GEngine)
{
return;
}
UFont* Font = GEngine->GetMediumFont();
const FTurnState& Turn = GridManager->GetTurnManager().GetState();
const bool bPlayerTurn = Turn.CurrentFaction == EFaction::Player;
const FString TurnText = FString::Printf(TEXT("Turn %d - %s"),
Turn.TurnNumber, bPlayerTurn ? TEXT("Player") : TEXT("Enemy"));
DrawText(TurnText, bPlayerTurn ? FLinearColor(0.4f, 0.7f, 1.0f) : FLinearColor(1.0f, 0.4f, 0.4f),
20.0f, 20.0f, Font, 1.5f);
// 選択中ユニットの残りアクション
if (const ATacticaPlayerController* PC = Cast<ATacticaPlayerController>(GetOwningPlayerController()))
{
if (const AUnit* Unit = PC->GetSelectedUnit())
{
const FString UnitText = FString::Printf(TEXT("Unit %d Actions: %d / %d"),
Unit->UnitId, Unit->ActionPoints, Unit->Stats.ActionsPerTurn);
DrawText(UnitText, FLinearColor::White, 20.0f, 55.0f, Font, 1.2f);
}
}
DrawText(TEXT("[Enter] End Turn"), FLinearColor(0.8f, 0.8f, 0.8f), 20.0f, 85.0f, Font, 1.0f);
}
@@ -1,4 +1,5 @@
#include "TacticaPlayerController.h"
#include "ActionRules.h"
#include "GridCell.h"
#include "GridManager.h"
#include "Unit.h"
@@ -21,6 +22,8 @@ void ATacticaPlayerController::SetupInputComponent()
{
InputComponent->BindKey(EKeys::LeftMouseButton, IE_Pressed, this,
&ATacticaPlayerController::OnClick);
InputComponent->BindKey(EKeys::Enter, IE_Pressed, this,
&ATacticaPlayerController::OnEndTurnKey);
}
}
@@ -70,11 +73,27 @@ AGridManager* ATacticaPlayerController::GetGridManager()
return GridManager;
}
void ATacticaPlayerController::OnClick()
bool ATacticaPlayerController::CanAcceptInput()
{
if (State == EState::Moving)
{
UE_LOG(LogTacticaInput, Log, TEXT("移動中のため入力を無視しました"));
return false;
}
const AGridManager* Grid = GetGridManager();
if (!Grid || Grid->GetTurnManager().GetCurrentFaction() != EFaction::Player)
{
UE_LOG(LogTacticaInput, Log, TEXT("Playerのターンではないため入力を無視しました"));
return false;
}
return true;
}
void ATacticaPlayerController::OnClick()
{
if (!CanAcceptInput())
{
return;
}
@@ -90,13 +109,19 @@ void ATacticaPlayerController::OnClick()
if (State == EState::Idle)
{
if (AUnit* Unit = Grid->GetUnitAt(Coord))
AUnit* Unit = Grid->GetUnitAt(Coord);
if (!Unit)
{
SelectUnit(Unit, Cell);
UE_LOG(LogTacticaInput, Log, TEXT("(%d, %d) にユニットはいません"), Coord.X, Coord.Y);
}
else if (Unit->Faction != EFaction::Player)
{
// 敵ユニットは操作できない
UE_LOG(LogTacticaInput, Log, TEXT("敵ユニット %d を選択しました(操作はできません)"), Unit->UnitId);
}
else
{
UE_LOG(LogTacticaInput, Log, TEXT("(%d, %d) にユニットはいません"), Coord.X, Coord.Y);
SelectUnit(Unit, Cell);
}
return;
}
@@ -104,19 +129,45 @@ void ATacticaPlayerController::OnClick()
// UnitSelected: 移動可能なマス(起点を除く)だけ受け付ける
if (SelectedUnit && Coord != SelectedUnit->GridCoord && ReachResult.IsReachable(Coord))
{
if (Grid->TryMoveUnit(SelectedUnit, Coord, ReachResult))
const int32 PathCost = ReachResult.Cost[Coord];
const EActionType Action = FActionRules::ChooseMoveAction(PathCost, SelectedUnit->Stats.MoveRange);
if (FActionRules::CanPerform(Action, SelectedUnit->ActionPoints, SelectedUnit->Stats.ActionsPerTurn))
{
State = EState::Moving;
SelectedUnit->OnMoveFinished.AddUObject(this, &ATacticaPlayerController::OnUnitMoveFinished);
UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d を (%d, %d) へ移動開始"),
SelectedUnit->UnitId, Coord.X, Coord.Y);
return;
const int32 Cost = FActionRules::GetCost(Action, SelectedUnit->ActionPoints);
if (Grid->TryMoveUnit(SelectedUnit, Coord, ReachResult))
{
// 行動ポイントは移動の開始時に消費する
SelectedUnit->SpendActions(Cost);
State = EState::Moving;
SelectedUnit->OnMoveFinished.AddUObject(this, &ATacticaPlayerController::OnUnitMoveFinished);
UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d を (%d, %d) へ移動開始 (%s, 消費 %d, 残り %d)"),
SelectedUnit->UnitId, Coord.X, Coord.Y,
Action == EActionType::Dash ? TEXT("Dash") : TEXT("Move"),
Cost, SelectedUnit->ActionPoints);
return;
}
}
}
UE_LOG(LogTacticaInput, Log, TEXT("(%d, %d) には移動できません"), Coord.X, Coord.Y);
}
void ATacticaPlayerController::OnEndTurnKey()
{
if (!CanAcceptInput())
{
return;
}
ClearSelection();
if (AGridManager* Grid = GetGridManager())
{
UE_LOG(LogTacticaInput, Log, TEXT("Enterキーでターンを終了します"));
Grid->GetTurnManager().EndTurn();
}
}
void ATacticaPlayerController::SelectUnit(AUnit* Unit, AGridCell* Cell)
{
AGridManager* Grid = GetGridManager();
@@ -128,31 +179,60 @@ void ATacticaPlayerController::SelectUnit(AUnit* Unit, AGridCell* Cell)
SelectedUnit = Unit;
SelectedCell = Cell;
SelectedCell->SetSelected(true);
ReachResult = FGridPathfinder::FindReachable(Grid->GetGridState(), Unit->GridCoord, Unit->Stats.MoveRange);
State = EState::UnitSelected;
RangeCoords.Reset();
DashRangeCoords.Reset();
ReachResult = FGridReachResult();
// アクションが残っていなければ、選択はできるが範囲は表示しない
if (Unit->ActionPoints <= 0)
{
UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d を選択 (アクションが残っていないため移動できません)"), Unit->UnitId);
return;
}
// ダッシュできる(アクションが全部残っている)ときだけ、探索範囲を広げる
const int32 MoveRange = Unit->Stats.MoveRange;
const bool bCanDash = FActionRules::CanPerform(
EActionType::Dash, Unit->ActionPoints, Unit->Stats.ActionsPerTurn);
const int32 MaxCost = bCanDash
? FActionRules::GetDashRange(MoveRange, Unit->Stats.DashMultiplier)
: MoveRange;
ReachResult = FGridPathfinder::FindReachable(Grid->GetGridState(), Unit->GridCoord, MaxCost);
for (const TPair<FIntPoint, int32>& Pair : ReachResult.Cost)
{
if (Pair.Key != Unit->GridCoord)
if (Pair.Key == Unit->GridCoord)
{
continue;
}
if (FActionRules::ChooseMoveAction(Pair.Value, MoveRange) == EActionType::Move)
{
RangeCoords.Add(Pair.Key);
}
else
{
DashRangeCoords.Add(Pair.Key);
}
}
Grid->SetRangeHighlight(RangeCoords, true);
Grid->SetRangeHighlight(RangeCoords, true, false);
Grid->SetRangeHighlight(DashRangeCoords, true, true);
State = EState::UnitSelected;
UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d を選択 (移動力 %d, 移動可能 %d マス)"),
Unit->UnitId, Unit->Stats.MoveRange, RangeCoords.Num());
UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d を選択 (残りアクション %d, 通常 %d マス, ダッシュ %d マス)"),
Unit->UnitId, Unit->ActionPoints, RangeCoords.Num(), DashRangeCoords.Num());
}
void ATacticaPlayerController::ClearSelection()
{
if (AGridManager* Grid = GetGridManager())
{
Grid->SetRangeHighlight(RangeCoords, false);
Grid->SetRangeHighlight(RangeCoords, false, false);
Grid->SetRangeHighlight(DashRangeCoords, false, true);
}
RangeCoords.Reset();
DashRangeCoords.Reset();
ReachResult = FGridReachResult();
if (SelectedCell)
@@ -172,4 +252,13 @@ void ATacticaPlayerController::OnUnitMoveFinished()
UE_LOG(LogTacticaInput, Log, TEXT("ユニット %d の移動完了"), SelectedUnit->UnitId);
}
ClearSelection();
// 移動が完全に終わった後で、自動終了を判定する(移動中は判定しない)
AGridManager* Grid = GetGridManager();
if (Grid && Grid->GetTurnManager().GetCurrentFaction() == EFaction::Player
&& !Grid->HasActionsLeft(EFaction::Player))
{
UE_LOG(LogTacticaInput, Log, TEXT("Playerの全ユニットのアクションが0になったためターンを終了します"));
Grid->GetTurnManager().EndTurn();
}
}
+27
View File
@@ -0,0 +1,27 @@
#include "TurnManager.h"
void FTurnManager::Start(EFaction FirstFaction)
{
State.TurnNumber = 1;
State.CurrentFaction = FirstFaction;
OnTurnStarted.Broadcast(State.TurnNumber, State.CurrentFaction);
}
void FTurnManager::EndTurn()
{
const EFaction Ended = State.CurrentFaction;
OnTurnEnded.Broadcast(Ended);
// Player → Enemy → Player ...。Playerに戻ったらターン数を進める
if (Ended == EFaction::Player)
{
State.CurrentFaction = EFaction::Enemy;
}
else
{
State.CurrentFaction = EFaction::Player;
++State.TurnNumber;
}
OnTurnStarted.Broadcast(State.TurnNumber, State.CurrentFaction);
}
+44
View File
@@ -1,6 +1,7 @@
#include "Unit.h"
#include "Components/StaticMeshComponent.h"
#include "Engine/StaticMesh.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "UObject/ConstructorHelpers.h"
AUnit::AUnit()
@@ -26,10 +27,53 @@ AUnit::AUnit()
// 底面がActorの原点(マスの上面)に来るよう、高さの半分(50cm)上げる
MeshComponent->SetRelativeLocation(FVector(0.0f, 0.0f, 50.0f));
// 色を変えられる標準マテリアル(パラメータ"Color")
static ConstructorHelpers::FObjectFinder<UMaterialInterface> BaseMaterial(
TEXT("/Engine/BasicShapes/BasicShapeMaterial.BasicShapeMaterial"));
if (BaseMaterial.Succeeded())
{
MeshComponent->SetMaterial(0, BaseMaterial.Object);
}
// マスのクリック判定をユニットが邪魔しないよう、当たり判定は無効にする
MeshComponent->SetCollisionEnabled(ECollisionEnabled::NoCollision);
}
void AUnit::BeginPlay()
{
Super::BeginPlay();
DynamicMaterial = MeshComponent->CreateDynamicMaterialInstance(0);
RefreshColor();
}
void AUnit::SetFaction(EFaction InFaction)
{
Faction = InFaction;
RefreshColor();
}
void AUnit::RefreshColor()
{
if (DynamicMaterial)
{
DynamicMaterial->SetVectorParameterValue(TEXT("Color"),
Faction == EFaction::Player ? PlayerColor : EnemyColor);
}
}
void AUnit::SpendActions(int32 Amount)
{
ActionPoints = FMath::Max(0, ActionPoints - Amount);
OnActionPointsChanged.Broadcast();
}
void AUnit::RestoreActions()
{
ActionPoints = Stats.ActionsPerTurn;
OnActionPointsChanged.Broadcast();
}
void AUnit::MoveAlongPath(const TArray<FVector>& InWaypoints)
{
Waypoints = InWaypoints;
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "CoreMinimal.h"
#include "TurnTypes.h"
// アクションのコストや可否のルール。UObject/Actorに依存しない純粋なロジック
// 数値(行動ポイント数、ダッシュ倍率)は呼び出し側がFUnitStats等から渡す
struct TACTICA_API FActionRules
{
// 実行時に消費する行動ポイント。Dashは残り全部
static int32 GetCost(EActionType Type, int32 ActionPointsLeft);
// 実行できるか。DashはActionsPerTurn分(=全部)残っているときだけ
static bool CanPerform(EActionType Type, int32 ActionPointsLeft, int32 ActionsPerTurn);
// ダッシュで移動できる最大マス数
static int32 GetDashRange(int32 MoveRange, float DashMultiplier);
// 移動コスト(マス数)から、必要なアクション種別を決める
static EActionType ChooseMoveAction(int32 PathCost, int32 MoveRange);
};
+6
View File
@@ -47,7 +47,12 @@ public:
UPROPERTY(EditAnywhere, Category = "Grid|Highlight")
FLinearColor RangeColor = FLinearColor(0.2f, 0.8f, 0.3f);
// ダッシュでのみ移動できる範囲の色
UPROPERTY(EditAnywhere, Category = "Grid|Highlight")
FLinearColor DashRangeColor = FLinearColor(1.0f, 0.45f, 0.1f);
void SetInRange(bool bInInRange);
void SetInDashRange(bool bInInDashRange);
void SetHovered(bool bInHovered);
void SetSelected(bool bInSelected);
@@ -59,6 +64,7 @@ private:
void RefreshColor();
bool bInRange = false;
bool bInDashRange = false;
bool bHovered = false;
bool bSelected = false;
+46 -4
View File
@@ -3,12 +3,30 @@
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "GridState.h"
#include "TurnManager.h"
#include "TurnTypes.h"
#include "GridManager.generated.h"
class AGridCell;
class AUnit;
struct FGridReachResult;
// ユニットの初期配置(座標と陣営)
USTRUCT()
struct FInitialUnit
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category = "Units")
FIntPoint Coord = FIntPoint::ZeroValue;
UPROPERTY(EditAnywhere, Category = "Units")
EFaction Faction = EFaction::Player;
FInitialUnit() {}
FInitialUnit(FIntPoint InCoord, EFaction InFaction) : Coord(InCoord), Faction(InFaction) {}
};
UCLASS()
class TACTICA_API AGridManager : public AActor
{
@@ -45,9 +63,17 @@ public:
UPROPERTY(EditAnywhere, Category = "Units")
TSubclassOf<AUnit> UnitClass;
// ユニットの初期配置座標(1要素につき1体)
// ユニットの初期配置(1要素につき1体)
UPROPERTY(EditAnywhere, Category = "Units")
TArray<FIntPoint> InitialUnitCoords = { FIntPoint(0, 0), FIntPoint(2, 2) };
TArray<FInitialUnit> InitialUnits = {
FInitialUnit(FIntPoint(0, 0), EFaction::Player),
FInitialUnit(FIntPoint(1, 0), EFaction::Player),
FInitialUnit(FIntPoint(2, 2), EFaction::Enemy),
FInitialUnit(FIntPoint(1, 2), EFaction::Enemy) };
// 敵ターンの待ち時間(秒)。仮実装
UPROPERTY(EditAnywhere, Category = "Turn")
float EnemyTurnDelay = 1.0f;
// 生成したユニットの一覧
UPROPERTY(VisibleAnywhere, Category = "Units")
@@ -63,8 +89,15 @@ public:
// Reach は、ユニットの現在位置から求めた FindReachable の結果
bool TryMoveUnit(AUnit* Unit, FIntPoint Dest, const FGridReachResult& Reach);
// 指定したマスの「移動可能」表示をON/OFFする
void SetRangeHighlight(const TArray<FIntPoint>& Coords, bool bInRange);
// 指定したマスの「移動可能」表示をON/OFFする(bDash=trueならダッシュ範囲の色)
void SetRangeHighlight(const TArray<FIntPoint>& Coords, bool bInRange, bool bDash = false);
// ターン管理(通知を購読するときはここから取る)
FTurnManager& GetTurnManager() { return TurnManager; }
const FTurnManager& GetTurnManager() const { return TurnManager; }
// 指定した陣営に、行動ポイントが残っているユニットがいるか
bool HasActionsLeft(EFaction Faction) const;
// 座標のマスを返す(範囲外はnullptr)
AGridCell* GetCellAt(FIntPoint Coord) const;
@@ -72,10 +105,19 @@ public:
// グリッド全体の中心(ワールド)
FVector GetGridCenter() const;
protected:
// 敵ターンの拡張点。将来の敵AIはここでユニットごとの行動を実行し、
// 終わったら TurnManager.EndTurn() を呼ぶ。今は待ってから終了するだけ
virtual void RunEnemyTurn();
private:
void GenerateGrid();
void SpawnUnits();
void HandleTurnStarted(int32 TurnNumber, EFaction Faction);
void FinishEnemyTurn();
FGridState GridState;
FTurnManager TurnManager;
FTimerHandle EnemyTurnTimer;
int32 NextUnitId = 0;
};
+22
View File
@@ -0,0 +1,22 @@
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/HUD.h"
#include "TacticaHUD.generated.h"
class AGridManager;
// ターン数・陣営・選択中ユニットの残りアクションを表示するHUD。
// 状態は持たず、毎フレーム TurnManager / Unit から読んで描くだけ
UCLASS()
class TACTICA_API ATacticaHUD : public AHUD
{
GENERATED_BODY()
public:
virtual void DrawHUD() override;
private:
UPROPERTY()
TObjectPtr<AGridManager> GridManager;
};
@@ -17,6 +17,9 @@ class TACTICA_API ATacticaPlayerController : public APlayerController
public:
ATacticaPlayerController();
// 選択中のユニット(HUD表示用)。なければnullptr
AUnit* GetSelectedUnit() const { return SelectedUnit; }
protected:
virtual void SetupInputComponent() override;
virtual void PlayerTick(float DeltaTime) override;
@@ -35,7 +38,11 @@ private:
AGridManager* GetGridManager();
// Playerのターンで、入力を受け付けてよいか(移動中は不可)
bool CanAcceptInput();
void OnClick();
void OnEndTurnKey();
void SelectUnit(AUnit* Unit, AGridCell* Cell);
void ClearSelection();
void OnUnitMoveFinished();
@@ -56,7 +63,8 @@ private:
UPROPERTY()
TObjectPtr<AGridManager> GridManager;
// 選択中ユニットの移動可能範囲(起点を除く)と探索結果
// 選択中ユニットの移動可能範囲(起点を除く)。通常移動とダッシュで分ける
TArray<FIntPoint> RangeCoords;
TArray<FIntPoint> DashRangeCoords;
FGridReachResult ReachResult;
};
+42
View File
@@ -0,0 +1,42 @@
#pragma once
#include "CoreMinimal.h"
#include "TurnTypes.h"
// ターンの状態。値だけで構成されるので、そのままセーブできる
struct FTurnState
{
// ターン数(Playerのターンに戻るたびに+1。1から)
int32 TurnNumber = 1;
EFaction CurrentFaction = EFaction::Player;
};
// ターンの進行。UObject/Actorに依存しない純粋なクラス。
// 状態の変化はDelegateで通知し、見た目や行動ポイントの回復は購読側が行う
class TACTICA_API FTurnManager
{
public:
DECLARE_MULTICAST_DELEGATE_TwoParams(FOnTurnStarted, int32 /*TurnNumber*/, EFaction /*Faction*/);
DECLARE_MULTICAST_DELEGATE_OneParam(FOnTurnEnded, EFaction /*Faction*/);
// 通知(購読は Actor 層が行う)
FOnTurnStarted OnTurnStarted;
FOnTurnEnded OnTurnEnded;
// 最初のターンを開始する(ターン数は1)
void Start(EFaction FirstFaction);
// 現在のターンを終了して、次の陣営のターンを開始する
void EndTurn();
// セーブデータからの復元。通知は発火しない(二重回復を防ぐ)
void Restore(const FTurnState& InState) { State = InState; }
const FTurnState& GetState() const { return State; }
int32 GetTurnNumber() const { return State.TurnNumber; }
EFaction GetCurrentFaction() const { return State.CurrentFaction; }
private:
FTurnState State;
};
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "CoreMinimal.h"
#include "TurnTypes.generated.h"
// 陣営
UENUM(BlueprintType)
enum class EFaction : uint8
{
Player,
Enemy
};
// アクションの種類。攻撃などを足すときはここに追加する
UENUM(BlueprintType)
enum class EActionType : uint8
{
Move,
Dash
};
+39 -1
View File
@@ -2,12 +2,15 @@
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TurnTypes.h"
#include "UnitStats.h"
#include "Unit.generated.h"
class UStaticMeshComponent;
class UMaterialInstanceDynamic;
// ユニット(兵士)。見た目と移動の補間だけを担当し、占有などの論理はAGridManager/FGridStateが持つ
// ユニット(兵士)。見た目と移動の補間、行動ポイントを持つ。
// 占有などの論理はAGridManager/FGridStateが持つ
UCLASS()
class TACTICA_API AUnit : public AActor
{
@@ -29,10 +32,34 @@ public:
UPROPERTY(EditAnywhere, Category = "Unit")
FUnitStats Stats;
// 陣営
UPROPERTY(VisibleAnywhere, Category = "Unit")
EFaction Faction = EFaction::Player;
// 残りの行動ポイント
UPROPERTY(VisibleAnywhere, Category = "Unit")
int32 ActionPoints = 0;
// 陣営ごとの色
UPROPERTY(EditAnywhere, Category = "Unit")
FLinearColor PlayerColor = FLinearColor(0.2f, 0.5f, 1.0f);
UPROPERTY(EditAnywhere, Category = "Unit")
FLinearColor EnemyColor = FLinearColor(1.0f, 0.2f, 0.2f);
// 移動速度(cm/秒)
UPROPERTY(EditAnywhere, Category = "Unit")
float MoveSpeed = 400.0f;
// 陣営を設定して色に反映する
void SetFaction(EFaction InFaction);
// 行動ポイントを消費する(0未満にはならない)
void SpendActions(int32 Amount);
// 行動ポイントを1ターン分に回復する
void RestoreActions();
// ウェイポイントを順に通って移動する
void MoveAlongPath(const TArray<FVector>& InWaypoints);
@@ -41,10 +68,21 @@ public:
// 移動完了時に呼ばれる
FSimpleMulticastDelegate OnMoveFinished;
// 行動ポイントが変化したときに呼ばれる
FSimpleMulticastDelegate OnActionPointsChanged;
protected:
virtual void BeginPlay() override;
private:
void RefreshColor();
UPROPERTY(VisibleAnywhere, Category = "Unit")
TObjectPtr<UStaticMeshComponent> MeshComponent;
UPROPERTY()
TObjectPtr<UMaterialInstanceDynamic> DynamicMaterial;
TArray<FVector> Waypoints;
int32 WaypointIndex = 0;
};
+8
View File
@@ -13,4 +13,12 @@ struct FUnitStats : public FTableRowBase
// 移動力(マス数)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Unit")
int32 MoveRange = 4;
// 1ターンの行動ポイント
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Unit")
int32 ActionsPerTurn = 2;
// ダッシュ時の移動範囲の倍率(移動力に掛ける)
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Unit")
float DashMultiplier = 2.0f;
};