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”