Add building ghost preview

This commit is contained in:
2026-05-18 11:02:02 -07:00
parent 27be644ae6
commit 6baeca2783
5 changed files with 113 additions and 2 deletions
+3 -1
View File
@@ -610,7 +610,9 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
shelter through the server-authoritative placement component. Fully modular
wall-by-wall building stays deferred to `0.2.E Permanent Structures`.
- [x] Create build placement mode.
- [~] Add ghost preview.
- [x] Add ghost preview. The building placement component now exposes preview
validity/transform state and draws an assetless green/red wireframe ghost
footprint at the snapped placement location for MVP development builds.
- [x] Add placement validation.
- [x] Add basic shelter piece.
- [ ] Add wall piece if needed.
+5
View File
@@ -86,6 +86,11 @@ Early runtime systems should remain small and explicit:
as inventory parts and then combined into a single placeable primitive
shelter actor. Fully modular wall-by-wall construction is deferred to
permanent structures so the first survival loop remains reliable.
- `UAgrarianBuildingPlacementComponent` owns the MVP placement preview. It
traces from the player view, snaps to the configured grid, validates distance
and collision, broadcasts Blueprint-readable preview state, and draws a
green/red wireframe ghost footprint in development builds so placement can be
tested before final mesh/material ghost assets exist.
- Persistence layer: save/load contracts for player and world state.
Blueprints can compose and expose these systems, but core replicated behavior
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
HEADER = ROOT / "Source" / "AgrarianGame" / "AgrarianBuildingPlacementComponent.h"
SOURCE = ROOT / "Source" / "AgrarianGame" / "AgrarianBuildingPlacementComponent.cpp"
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
TECHNICAL_DESIGN = ROOT / "Docs" / "TechnicalDesignDocument.md"
def compact(path: Path) -> str:
return " ".join(path.read_text(encoding="utf-8").split())
def require(path: Path, text: str) -> None:
data = compact(path)
if text not in data:
raise SystemExit(f"FAIL: {path.relative_to(ROOT)} missing required text: {text}")
def main() -> None:
require(HEADER, "FAgrarianBuildPreviewUpdatedSignature")
require(HEADER, "OnBuildPreviewUpdated")
require(HEADER, "bShowGhostPreview")
require(HEADER, "GetPlacementPreviewState")
require(HEADER, "TickComponent")
require(SOURCE, "DrawDebugBox")
require(SOURCE, "ValidGhostPreviewColor")
require(SOURCE, "InvalidGhostPreviewColor")
require(SOURCE, "OnBuildPreviewUpdated.Broadcast")
require(ROADMAP, "[x] Add ghost preview.")
require(TECHNICAL_DESIGN, "green/red wireframe ghost footprint")
print("PASS: building ghost preview support is present.")
if __name__ == "__main__":
main()
@@ -2,13 +2,14 @@
#include "AgrarianBuildingPlacementComponent.h"
#include "AgrarianInventoryComponent.h"
#include "DrawDebugHelpers.h"
#include "GameFramework/Pawn.h"
#include "GameFramework/Controller.h"
#include "Engine/World.h"
UAgrarianBuildingPlacementComponent::UAgrarianBuildingPlacementComponent()
{
PrimaryComponentTick.bCanEverTick = false;
PrimaryComponentTick.bCanEverTick = true;
SetIsReplicatedByDefault(true);
}
@@ -55,6 +56,16 @@ bool UAgrarianBuildingPlacementComponent::GetPlacementPreview(FTransform& OutTra
return true;
}
bool UAgrarianBuildingPlacementComponent::GetPlacementPreviewState(FTransform& OutTransform, FText& FailureReason) const
{
if (!GetPlacementPreview(OutTransform, FailureReason))
{
return false;
}
return CanPlaceAtTransform(ActiveBuildClass, OutTransform, FailureReason);
}
bool UAgrarianBuildingPlacementComponent::CanPlaceAtTransform(TSubclassOf<AActor> BuildClass, const FTransform& PlacementTransform, FText& FailureReason) const
{
if (!BuildClass)
@@ -148,6 +159,38 @@ void UAgrarianBuildingPlacementComponent::ServerPlaceBuildable_Implementation(TS
OnBuildPlaced.Broadcast(PlacedActor);
}
void UAgrarianBuildingPlacementComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
if (!bShowGhostPreview || !ActiveBuildClass || !GetWorld())
{
return;
}
FTransform PreviewTransform;
FText FailureReason;
const bool bCanPlace = GetPlacementPreviewState(PreviewTransform, FailureReason);
const FColor PreviewColor = bCanPlace ? ValidGhostPreviewColor : InvalidGhostPreviewColor;
const FVector PreviewExtent(
FMath::Max(PlacementProbeRadius, 25.0f),
FMath::Max(PlacementProbeRadius, 25.0f),
FMath::Max(PlacementProbeRadius * 0.5f, 25.0f));
DrawDebugBox(
GetWorld(),
PreviewTransform.GetLocation() + FVector(0.0f, 0.0f, PreviewExtent.Z),
PreviewExtent,
PreviewTransform.GetRotation(),
PreviewColor,
false,
GhostPreviewLifetimeSeconds,
0,
GhostPreviewLineThickness);
OnBuildPreviewUpdated.Broadcast(bCanPlace, PreviewTransform, FailureReason);
}
bool UAgrarianBuildingPlacementComponent::HasPlacementCost(FText& FailureReason) const
{
const UAgrarianInventoryComponent* Inventory = GetOwner() ? GetOwner()->FindComponentByClass<UAgrarianInventoryComponent>() : nullptr;
@@ -9,6 +9,7 @@
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianBuildPlacedSignature, AActor*, PlacedActor);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FAgrarianBuildPlacementFailedSignature, FText, Reason);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_ThreeParams(FAgrarianBuildPreviewUpdatedSignature, bool, bCanPlace, FTransform, PreviewTransform, FText, FailureReason);
UCLASS(ClassGroup = (Agrarian), BlueprintType, Blueprintable, meta = (BlueprintSpawnableComponent))
class UAgrarianBuildingPlacementComponent : public UActorComponent
@@ -24,6 +25,9 @@ public:
UPROPERTY(BlueprintAssignable, Category = "Agrarian|Building")
FAgrarianBuildPlacementFailedSignature OnBuildPlacementFailed;
UPROPERTY(BlueprintAssignable, Category = "Agrarian|Building")
FAgrarianBuildPreviewUpdatedSignature OnBuildPreviewUpdated;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building")
TSubclassOf<AActor> ActiveBuildClass;
@@ -45,12 +49,30 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building", meta = (EditCondition = "bSnapToGrid", ClampMin = "1"))
float GridSize = 50.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview")
bool bShowGhostPreview = true;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview", meta = (ClampMin = "0"))
float GhostPreviewLifetimeSeconds = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview", meta = (ClampMin = "0"))
float GhostPreviewLineThickness = 2.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview")
FColor ValidGhostPreviewColor = FColor(70, 220, 120);
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Building|Preview")
FColor InvalidGhostPreviewColor = FColor(230, 80, 65);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Building")
void SetActiveBuildable(TSubclassOf<AActor> BuildClass, const TArray<FAgrarianItemStack>& Cost);
UFUNCTION(BlueprintCallable, Category = "Agrarian|Building")
bool GetPlacementPreview(FTransform& OutTransform, FText& FailureReason) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Building")
bool GetPlacementPreviewState(FTransform& OutTransform, FText& FailureReason) const;
UFUNCTION(BlueprintCallable, Category = "Agrarian|Building")
bool CanPlaceAtTransform(TSubclassOf<AActor> BuildClass, const FTransform& PlacementTransform, FText& FailureReason) const;
@@ -60,6 +82,8 @@ public:
UFUNCTION(Server, Reliable, BlueprintCallable, Category = "Agrarian|Building")
void ServerPlaceBuildable(TSubclassOf<AActor> BuildClass, FTransform PlacementTransform);
virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
protected:
bool HasPlacementCost(FText& FailureReason) const;
void ConsumePlacementCost();