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:
1 parent
41709fdd65
commit
f3f0c080a9
17 files changed
+546
-31
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
Reference in new issue
Block a user