BFS ( Breadth-first Search : 너비 우선 탐색 )
BFS Pseudocode:
비교
DFS
: 재귀적이고 깊이 우선적으로 탐색을 수행하며,BFS
: 큐를 사용하여 너비 우선적으로 탐색하며,언제 DFS, BFS를 선택해야 하는가?
재귀 함수의 구조
종료 조건(Base Case)
: 재귀 호출을 중단하고 결과를 반환하는 조건입니다. 이를 통해 무한 반복을 방지합니다.재귀 호출(Recursive Call)
: 문제를 더 작은 문제로 나누어 자기 자신을 호출하는 부분입니다.스택 오버플로우 주의
Unreal Engine에서 재귀 함수의 활용
트리 구조 탐색
: Unreal Engine의 Actor나 Component 계층 구조를 순회할 때, 재귀를 통해 하위 노드를 탐색할 수 있습니다.프랙탈(Fractal) 생성
: 재귀적인 구조를 활용해 자연스러운 절차적 지형이나 패턴을 생성할 때도 사용할 수 있습니다.TQueue
먼저 타일을 순차적으로 탐색하면서 가로 및 세로로 같은 종류의 타일이 3개 이상 연속되었는지 확인합니다. 이를 위해 각각의 타일을 기준 타일로 설정한 후, 해당 타일의 좌우(가로)와 상하(세로)에 있는 타일들이 같은지 검사
TileGrid.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TileGrid.generated.h"
class ATile;
UCLASS()
class PUZZLESTUDY_API ATileGrid : public AActor
{
GENERATED_BODY()
protected:
virtual void BeginPlay() override;
public:
// Sets default values for this actor's properties
ATileGrid();
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tile Grid")
int32 GridWidth;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tile Grid")
int32 GridHeigh;
// 타일 간의 배치 간격 ( 기본값 : 100 )
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tile Grid")
float TileSpacing;
UPROPERTY()
TArray<ATile*> TileArray;
// 타일을 생성할 Blueprint 클래스를 선택할 수 있도록 TSubClassOf 사용
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tile Grid")
TSubclassOf<ATile> TileClass; // TileClass는 월드에 배치된 TileGrid에서 설정할 수 있음
// 그리드 초기화하는 함수
void InitializeGrid();
// 특정 타일의 위치를 얻는 함수
ATile* GetTile(int32 x, int32 y) const;
// 특정 타일의 위치를 설정하는 함수
void SetTile(int32 x, int32 y, ATile* Tile);
// 바둑판 크기 정의
const int32 Rows = 4;
const int32 Colums = 4;
// TArray 선언
TArray<int32> GridArray;
// 바둑판 그리기 함수
void DrawGrid();
// 매칭을 확인 Array
TArray<ATile*> CheckForMatches();
// swap 함수
void SwapTiles(ATile* FirstTile, ATile* SecondTile);
void RemoveMatchingTIles(const TArray<ATile*>& MachingTiles);
void DropDownTiles();
bool GetTileGridPosition(ATile* Tile, int32& OutX, int32& OutY) const;
void RefillGrid();
FName GenerateRandomTileType();
void ProcessMatchingLoop();
private:
TArray<ATile*> CheckHorizontalMatches(int32 StartX, int32 StartY);
TArray<ATile*> CheckVerticalMatches(int32 StartX, int32 StartY);
};
TileGrid.cpp ( CheckForMatches )
TArray<ATile*> ATileGrid::CheckForMatches()
{
TArray<ATile*> AllMatchedTile;
// 가로 매칭 확인
for (int32 x = 0; x < GridWidth; ++x)
{
for (int32 y = 0; y < GridHeigh; ++y)
{
ATile* CurrentTile = GetTile(x, y);
if (!CurrentTile) continue;
// 가로 매칭된 타일 찾기
TArray<ATile*> HorizontalMatches = CheckHorizontalMatches(x, y);
if (HorizontalMatches.Num() >= 3)
{
AllMatchedTile.Append(HorizontalMatches);
}
}
}
// 세로 매칭 확인
for (int32 x = 0; x < GridWidth; ++x)
{
for (int32 y = 0; y < GridHeigh; ++y)
{
ATile* CurrentTile = GetTile(x, y);
if (!CurrentTile) continue;
// 세로 매칭된 타일 찾기
TArray<ATile*> VerticalMatches = CheckVerticalMatches(x, y);
if (VerticalMatches.Num() >= 3)
{
AllMatchedTile.Append(VerticalMatches);
}
}
}
return AllMatchedTile;
}
TileGrid.cpp (CheckHorizontalMatches/CheckVerticalMatches)
TArray<ATile*> ATileGrid::CheckHorizontalMatches(int32 StartX, int32 StartY)
{
TArray<ATile*> HorizontalMatches;
ATile* StartTile = GetTile(StartX, StartY);
if (!StartTile) return HorizontalMatches;
HorizontalMatches.Add(StartTile);
// 오른쪽으로 2칸까지 같은 타일이 있는지 확인
for (int32 x = StartX + 1; x < GridWidth; ++x)
{
ATile* NextTile = GetTile(x, StartY);
if (NextTile && NextTile->TileType == StartTile->TileType)
{
HorizontalMatches.Add(NextTile);
}
else
{
break;
}
}
return HorizontalMatches;
}
TArray<ATile*> ATileGrid::CheckVerticalMatches(int32 StartX, int32 StartY)
{
TArray<ATile*> VerticalMatches;
ATile* StartTile = GetTile(StartX, StartY);
if (!StartTile) return VerticalMatches;
VerticalMatches.Add(StartTile);
// 위쪽으로 2칸까지 같은 타일이 있는지 확인
for (int32 y = StartY + 1; y < GridHeigh; ++y)
{
ATile* NextTile = GetTile(StartX, y);
if (NextTile && NextTile->TileType == StartTile->TileType)
{
VerticalMatches.Add(NextTile);
}
else
{
break;
}
}
return VerticalMatches;
}
TileGrid.cpp ( SwapTiles )
void ATileGrid::SwapTiles(ATile* FirstTile, ATile* SecondTile)
{
int32 x1, y1, x2, y2;
if (GetTileGridPosition(FirstTile, x1, y1) && GetTileGridPosition(SecondTile, x2, y2))
{
// 타일 배열 업데이트
SetTile(x1, y1, SecondTile);
SetTile(x2, y2, FirstTile);
// 타일의 그리드 좌표도 업데이트
FVector2D TempPosition = FirstTile->TilePosition;
FirstTile->TilePosition = SecondTile->TilePosition;
SecondTile->TilePosition = TempPosition;
// 타일 위치를 스왑
FVector TempLocation = FirstTile->GetActorLocation();
FirstTile->SetActorLocation(SecondTile->GetActorLocation());
SecondTile->SetActorLocation(TempLocation);
}
}
TileGrid.cpp ( GetTileGridPosition )
bool ATileGrid::GetTileGridPosition(ATile* Tile, int32& OutX, int32& OutY) const
{
for (int32 Index = 0; Index < TileArray.Num(); ++Index)
{
if (TileArray[Index] == Tile)
{
OutX = Index % GridWidth; // X 좌표 계산
OutY = Index / GridWidth; // Y 좌표 계산
//OutY = Index / GridHeigh; // 내가 변경한 내용
return true;
}
}
// 타일을 찾지 못했을 경우
return false;
}
TileGrid.cpp ( RemoveMatchingTIles / DropDownTiles )
void ATileGrid::RemoveMatchingTIles(const TArray<ATile*>& MachingTiles)
{
for (ATile* Tile : MachingTiles)
{
if (Tile)
{
int32 X, Y;
if (GetTileGridPosition(Tile, X, Y))
{
SetTile(X, Y, nullptr);
// 타일을 삭제하기 전에 애니메이션을 적용할 수 있습니다
// 예시 : Tile->PlayDeletAnimation(); // 0.5초동안 Scale을 0으로 줄이기
Tile->Destroy();
}
}
}
/*
* // 매칭된 타일 수에 따라 점수 추가
* addScore(NumMatchedTiles * 100 ); // 예시 : 타일 하나당 100점
*/
// 빈 공간을 채우기 위해 타일을 아래로 드랍
DropDownTiles();
// 이후 매칭 루프 처리
ProcessMatchingLoop(); // 매칭 루프 시작
}
void ATileGrid::DropDownTiles()
{
for (int32 x = 0; x < GridWidth; ++x)
{
for (int32 y = GridHeigh - 1; y >= 0; --y)
{
// 빈 칸일 경우
if (!GetTile(x, y))
{
for (int32 AboveY = y - 1; AboveY >= 0; --AboveY)
{
ATile* AboveTile = GetTile(x, AboveY);
if (AboveTile)
{
// 타일 배열 업데이트
SetTile(x, y, AboveTile);
SetTile(x, AboveY, nullptr);
// 타일의 그리드 좌표 갱신
AboveTile->TilePosition = FVector2D(x, y);
// 타일을 이동 ( 상대적 위치로 )
FVector RelativeLocation = FVector(x * TileSpacing, y * TileSpacing, 0.0f);
AboveTile->SetActorRelativeLocation(RelativeLocation);
break;
}
}
}
}
}
// 빈 칸을 새로운 타일로 채우기
RefillGrid();
}
SwapTileCommand.cpp
void USwapTileCommand::Excute()
{
// 유효성 확인
if (!FirstTile || !SecondTile)
{
UE_LOG(LogTemp, Error, TEXT("Invaild Tile for Swapping. "));
return;
}
// 타일 그리드 가져오기 ( FirstTIle의 소속 그리드 )
ATileGrid* TileGrid = FirstTile->TileGrid;
// 그리드가 유효한지 확인
if (!TileGrid)
{
UE_LOG(LogTemp, Warning, TEXT("Failed to retried. "));
}
// 타일 swap
TileGrid->SwapTiles(FirstTile, SecondTile);
// 타일 swap
TArray<ATile*> MachingTiles = TileGrid->CheckForMatches();
if (MachingTiles.Num() > 0) // 매칭이 있을 경우
{
TileGrid->RemoveMatchingTIles(MachingTiles);
}
else
{
// 매칭이 안될시 돌아가기
Undo();
}
// TODO : 매칭 확인 등의 로직
}
void USwapTileCommand::Undo()
{
// 그리드 내에서 타일을 교환
FirstTile->SetActorLocation(FirstTileOrigineType);
SecondTile->SetActorLocation(SecondTileOrigineType);
// 그리드 내의 타일 좌표도 원래 좌표로
FVector2D TempPosition = FirstTile->TilePosition;
FirstTile->TilePosition = SecondTile->TilePosition;
SecondTile->TilePosition = TempPosition;
ATileGrid* TileGrid = FirstTile->TileGrid;
if (TileGrid)
{
int32 x1, y1, x2, y2;
if (TileGrid->GetTileGridPosition(FirstTile, x1, y1) && TileGrid->GetTileGridPosition(SecondTile, x2, y2))
{
TileGrid->SetTile(x1, y1, FirstTile);
TileGrid->SetTile(x2, y2, SecondTile);
}
}
}
Tile.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "TileGrid.h"
#include "Tile.generated.h"
UCLASS()
class PUZZLESTUDY_API ATile : public AActor
{
GENERATED_BODY()
...생략
public:
ATileGrid* TileGrid;
protected:
void UpdateAppearance();
};
TileGrid.cpp ( InitializeGrid )
TileGamePlayerController.cpp ( ProcessSelectedTile )
void ATIleGmaePlayerController::ProcessSelectedTile()
{
// 두 개의 타일이 선택되었을때,
if (!FirstSelectedTile.IsValid() && !SecondSelectedTile.IsValid())
{
// 타일 처리 로직 ( 자리교한, 매칭 확인 )
UE_LOG(LogTemp, Error, TEXT("Invaild Tile Selected. "));
return;
}
// 타일이 인접한지 확인
if (!FirstSelectedTile->IsAdjacentTo(SecondSelectedTile.Get()))
{
UE_LOG(LogTemp, Error, TEXT("Tile are not Adjacent! "));
return;
}
// 교환 명령 생성
USwapTileCommand* SwapCommand = NewObject<USwapTileCommand>(); // 클래스 이름 수정
SwapCommand->Initalize(FirstSelectedTile.Get(), SecondSelectedTile.Get());
// 커맨드 실행
ATileCommandInvoker* CommandInvoker = GetWorld()->SpawnActor<ATileCommandInvoker>();
CommandInvoker->ExcuteCommand(SwapCommand);
// 매칭 확인 로직 ( 교환 후 매칭이 있는지 확인 )
// MatchCheck(FirstSelectedTile, SecondSelectedTile);
// 두 타일의 선택 해제
if (FirstSelectedTile.IsValid())FirstSelectedTile->SetSelected(false);
if (SecondSelectedTile.IsValid())SecondSelectedTile->SetSelected(false);
// 선택 초기화
if (FirstSelectedTile.IsValid())FirstSelectedTile = nullptr;
if (SecondSelectedTile.IsValid())SecondSelectedTile = nullptr;
}
Tile.cpp ( IsAdjacentTo )
bool ATile::IsAdjacentTo(ATile* OtherTile) const
{
if (!OtherTile)
return false;
// 두 타일의 그리드 좌표 차이를 계산하여 인접 여부 확인
int32 DeltaX = FMath::Abs(TilePosition.X - OtherTile->TilePosition.X);
int32 DelatY = FMath::Abs(TilePosition.Y - OtherTile->TilePosition.Y);
// 두 타일이 가로 또는 세로로 1칸 차이일 경우 인접한 것으로 판단
return (DeltaX + DelatY) == 1;
}
TileGrid.cpp ( RefillGrid / GenerateRandomTileType )
void ATileGrid::RefillGrid()
{
for (int32 x = 0; x < GridWidth; ++x)
{
for (int32 y = 0; y < GridHeigh; ++y)
{
if (!GetTile(x, y))
{
ATile* NewTile = GetWorld()->SpawnActor<ATile>(TileClass);
if (NewTile)
{
// 타일의 그리드 좌표 설정
NewTile->TilePosition = FVector2D(x, y);
NewTile->TileType = GenerateRandomTileType();
NewTile->UpdateTileAppearance();
NewTile->TileGrid = this;
// 타일을 TileGrid의 자식으로 설정
NewTile->AttachToActor(this, FAttachmentTransformRules::KeepRelativeTransform);
// 상대적 위치 설정
FVector RelativeLocation = FVector(x* TileSpacing, y * TileSpacing, 0.0f);
NewTile->SetActorRelativeLocation(RelativeLocation);
// 그리드에 새로운 타일 설정 ( TileArray 업데이트 )
SetTile(x, y, NewTile);
}
}
}
}
ProcessMatchingLoop();
}
FName ATileGrid::GenerateRandomTileType()
{
TArray<FName> TileTypes =
{
FName("Cone"),
FName("Cube"),
FName("Cylinder"),
FName("Sphere"),
FName("Capsule"),
FName("Pyramid")
};
return TileTypes[FMath::RandRange(0, TileTypes.Num() - 1)];
}
TileGrid.cpp ( ProcessMatchingLoop )
void ATileGrid::ProcessMatchingLoop()
{
// 매칭이 있는지 확인
TArray<ATile*> MatchingTiles = CheckForMatches();
if (MatchingTiles.Num() > 0)
{
// 매칭된 타일이 있을 경우 삭제
RemoveMatchingTIles(MatchingTiles);
// 타일을 빈 공간으로 이동
DropDownTiles();
// 빈 공간에 새로운 타일을 채우기
RefillGrid();
// 모든 작업이 끝난 후 다시 매칭을 확인하기 위해 재귀 호출 ( 매칭이 더 이상 없을 때까지 반복 )
ProcessMatchingLoop(); // 재귀 호출
}
else
{
// 더 이상 매칭이 없으면 루프 종료
UE_LOG(LogTemp, Display, TEXT("No more matches, puzzle stabilized. "));
}
}