Skip to content

Thad writes blogs

this is a lot more work than i expected

Menu
  • About
  • Pin Posts
Menu

Advent of Code in Unreal Day 14

Posted on December 15, 2022December 15, 2022 by Thad

On the 14th day of Advent we simulate a sand flow. My solution today will be an Actor written in C++, using Begin Play to initialize, and the Tick virtual overload to drop sand.

Starting with the class definition:

UCLASS()
class AOC_2022_API AD14Actor : public AActor
{
	GENERATED_BODY()
	
public:	
	// Sets default values for this actor's properties
	AD14Actor();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:	
	// Called every frame
	virtual void Tick(float DeltaTime) override;

    UPROPERTY(EditAnywhere)
    UPlainTextAsset* PuzzleInput;

	UPROPERTY(EditAnywhere)
	FVector2D Source;

	UPROPERTY(BlueprintReadOnly)
	FBox2D Bounds;
	
	UPROPERTY(BlueprintReadOnly)
	int FloorY = 0;

	UPROPERTY(BlueprintReadOnly)
	int Resting = 0;

	UPROPERTY(EditAnywhere)
	bool bVerbose = false;

	UPROPERTY(EditAnywhere)
	bool bPart2 = false;
	
private:
	TBitArray<> Buffer;
	bool Blocked( FVector2D P )const;
	void Set( FVector2D P, bool V=true );
	void SetSpan( FVector2D A, FVector2D B, bool V=true );
	void DebugLog() const;
	inline int Stride() const {return FMath::CeilToInt( Bounds.GetSize().X )+1;}
};

The puzzle input doesn’t include any massive spans that make allocating a single buffer unreasonable, especially as I only need to store each location as blocked, or not, so I am using a TBitArray sized to the bounding box of the space I need to care about.

// Called when the game starts or when spawned
void AD14Actor::BeginPlay()
{
	Super::BeginPlay();

	// Parse the puzzle input into an array of paths where each path is an array of points
	ensure(PuzzleInput);
	typedef TArray< FVector2D > Path;
	TArray< Path > Structures;
	Structures.Reserve(PuzzleInput->Lines.Num());
	for(auto Line : PuzzleInput->Lines)
	{
		Path TempPath;
		TArray<FString> PathStrings;
		Line.ParseIntoArray(PathStrings, TEXT(" -> "));
		TempPath.Reset();
		for(auto Point : PathStrings)
		{
			FString Left, Right;
			Point.Split(TEXT(","), &Left, &Right);
			TempPath.Add( FVector2d(FCString::Atoi(*Left), FCString::Atoi(*Right)) );
		}
		Structures.Add(TempPath);		
	}

	// determine the bounding box for the structures
	FBox2D LocalBounds(Structures[0]);
	for (int i=1;i<Structures.Num();++i)
	{
		LocalBounds += Structures[i];
	}	
	LocalBounds += Source;

	// In Part 2 we are told to "assume the floor is an infinite horizontal line
	// with a y coordinate equal to two plus the highest y coordinate of any point in your scan."
	// But the buffer (allocated below) doesnt need to be infinitely wide, just 2x the drop
	// 
	if (bPart2)
	{
		FloorY = LocalBounds.Max.Y + 2;
		auto FX1 = Source.X - FloorY;
		auto FX2 = Source.X + FloorY;
		LocalBounds += FVector2d(FX1,FloorY);
		LocalBounds += FVector2d(FX2,FloorY);
	}
	
	Bounds = LocalBounds;
	
	// Allocate the grid space to store blocking features (rocks and sand)
	Buffer.Add(false,Stride() * FMath::CeilToInt( Bounds.GetSize().Y+1 ));

	// Write the rock-spans into the buffer
	for (const Path& P : Structures)
	{
		FVector2D V1 = P[0];
		for (int i=1;i<P.Num();++i)
		{
			SetSpan(V1, P[i]);
			V1 = P[i];
		}
	}

	DebugLog(); 	
}

A couple of helper functions for writing into the buffer.

void AD14Actor::SetSpan( FVector2D A, FVector2D B, bool V)
{
	FVector2D AB(B-A);
	AB.Normalize();
	while(FVector2D::DistSquared(A,B)>=1)
	{
		Set(A,V);
		A+=AB;		
	}
	Set(A,V);
}

void AD14Actor::Set(FVector2D P, bool V) 
{	
	if (Bounds.IsInsideOrOn(P))
	{
		P -= Bounds.Min;
		int X = FMath::FloorToInt( P.X );
		int Y = FMath::FloorToInt( P.Y );
		int I = X + Y*Stride();
		Buffer[I] = V;
	}
}

Without my usual test automation – which is a bit more complex to set up for Actors than Functions – the DebugLog is an important step to helps visually verify my set up is correct and my “Blocked” (read) matched my “Set” (write):

bool AD14Actor::Blocked(FVector2D P) const
{
	if (bPart2)
	{
		if (P.Y>=FloorY) return true;
	}
	
	if (Bounds.IsInsideOrOn(P))
	{
		P -= Bounds.Min;
		int X = FMath::FloorToInt( P.X );
		int Y = FMath::FloorToInt( P.Y );
		int I = X + Y*Stride();
		return Buffer[I];
	}
	return 0;
}

void AD14Actor::DebugLog() const
{
	UE_LOG(LogTemp, Log, TEXT("AD14Actor::DebugLog ---" ) ); 	
	for (int Y=Bounds.Min.Y; Y<=Bounds.Max.Y; ++Y )
	{
		FString Line;
		for (int X=Bounds.Min.X; X<=Bounds.Max.X; ++X )
		{
			auto c = (FVector2D::DistSquared(Source,FVector2D(X,Y))<1)
				? '+'
				: Blocked(FVector2D(X,Y) ) ? '#' : '.';   
			Line.AppendChar( c );
		}
		UE_LOG(LogTemp, Log, TEXT("%s"), *Line); 
	}
}

After working through some boundary condition bugs, I have something that looks like the example in the Output Log:

All that’s left to do now is write the sand simulation. I drop 1 grain of sand per tick:

// Called every frame
void AD14Actor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	FVector2d Sand;
	FVector2d Next = Source;
	do
	{
		Sand = Next;
	
		Next = Sand + FVector2d(0,1);
		if (Blocked(Next)) Next = Sand + FVector2d(-1,1);
		if (Blocked(Next)) Next = Sand + FVector2d( 1,1);
		
	}
	while (!Blocked(Next) && (Bounds.IsInsideOrOn(Next)));
	if (Blocked(Next))
	{
		Set(Sand,true);
		Resting ++;
		if (bVerbose) DebugLog();
		if (FVector2D::DistSquared(Sand,Source)<1)
		{
			UE_LOG(LogTemp, Log, TEXT("Finshed Part 2 after %i came to rest."), Resting);		
			SetActorTickEnabled(false);			
		}
	}
	else
	{
		DebugLog();
		UE_LOG(LogTemp, Log, TEXT("Finshed Part 1 after %i came to rest."), Resting);
		
		SetActorTickEnabled(false);
	}
}

My tick rate is capping out at 120 fps, so the solution took just under 4 minutes, dropping 1 grain of sand per tick to solve part 2.

1 thought on “Advent of Code in Unreal Day 14”

  1. Pingback: Advent of Code in Unreal Engine Day 18

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Archives

  • October 2024
  • January 2023
  • December 2022
  • November 2022

Categories

  • Food
  • meta
  • Uncategorized
© 2025 Thad writes blogs | Powered by Minimalist Blog WordPress Theme