본문 바로가기

[UE4 지뢰찾기] 타일을 관리하는 Board(판) 만들기

Kwonriver 2022. 11. 16.
728x90

타일을 관리하는 Board 클래스를 생성한다.

게임이 시작되면 GameMode에 의해 Board는 타일맵을 생성한다.

 

UCLASS()
class FINDMINE_API ABoard : public AActor
{
	GENERATED_BODY()
	
public:	
	ABoard();

protected:
	virtual void BeginPlay() override;

	float GamePlayTime = 0;	// 이번 게임 플레이 시간
	int32 Row = 0;		// 행, x에 해당
	int32 Column = 0;	// 열, y에 해당

	int32 UserSetFlagCount = 0;	// 유저가 세운 깃발 수
	int32 MineSetFlagCount = 0; // 실제 지뢰 위에 깃발 수

	TArray<TArray<ATile*>> TileMap;

	ATile* GetTileAt(int x, int y);

	void ChangeTileMesh(ATile* pTile);		// 타일 메시 변경 함수 
	void CheckNearTileState(int32 x, int32 y);	// x, y 주변 탐색 함수, 재귀적으로 반복
	void CheckTileState(int32 x, int32 y);	// X, Y 좌표에 해당하는 타일 검사

public:
	UPROPERTY(VisibleAnywhere, Category = "Board")
	int32 TotalMineCount = 0;	// 전체 지뢰 갯수

	/* 일반 노말 상태의 이미지*/
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Board")
	UMaterial* NormalTileMaterial;

	/* 지뢰 상태의 이미지*/
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Board")
	UMaterial* MineTileMaterial;
	
	/* 깃발 꽂은 상태의 이미지*/
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Board")
	UMaterial* FlagTileMaterial;

	/* 숫자 상태의 이미지 배열, 인덱스 0은 주변에 없는 칸, 1부터는 숫자 1*/
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Board")
	TArray<UMaterial*> NumberTileMaterials;

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

	UFUNCTION()
	void CreateBoard(int32 row, int32 column, int32 mineCount);

	UFUNCTION()
	void DestroyBoard();

	UFUNCTION()
	EMineGameState CheckMineInUserTouchedTile(int32 x, int32 y, bool isSetFlag);

	inline void SetGamePlayTime(float Time) { GamePlayTime = Time; };
	inline float GetGamePlayTime() { return GamePlayTime; };

};

 

생성자에서는 기본적인 데이터를 만들어준다.

게임을 시작하기 전까지는 특별하게 하는 동작이 없기 때문에 타일 관리에 사용될 리소스 정도만 생성한다.

 

ABoard::ABoard()
{
	PrimaryActorTick.bCanEverTick = true;
	SetRootComponent(CreateDefaultSubobject<USceneComponent>(TEXT("Root")));

	MineTileMaterial = CreateDefaultSubobject<UMaterial>(TEXT("MineTileMaterial"));
	NormalTileMaterial = CreateDefaultSubobject<UMaterial>(TEXT("NormalTileMaterial"));
	FlagTileMaterial = CreateDefaultSubobject<UMaterial>(TEXT("FlagTileMaterial"));
}

 

중요한 부분인 CreateBoard 함수이다.

void ABoard::CreateBoard(int32 row, int32 column, int32 mineCount)
{
	int32 nInitMineCount = 0;
	FVector OriginLocation = GetActorLocation();
	float x, y;

	this->Row = row;
	this->Column = column;
	this->TotalMineCount = mineCount;

	for (int i = 0; i < row; i++)
	{
		TArray<ATile*> RowArray;
		y = OriginLocation.Z - i * 100;

		for (int j = 0; j < column; j++)
		{
			ATile* tile = (ATile*)GetWorld()->SpawnActor(ATile::StaticClass());
			if (tile)
			{
				if (nInitMineCount < mineCount)
				{
					//?? KGR 지뢰 세팅 함수 변경
					tile->SetTileHasMine(true);
					nInitMineCount++;
				}

				x = OriginLocation.Y + j * 100;

				//tile->SetActorRelativeLocation(FVector(OriginLocation.X, x, y));
				tile->SetActorLocation(FVector(-500, x-395, y+395));
				ChangeTileMesh(tile);

				RowArray.Emplace(tile);

				UE_LOG(LogTemp, Warning, TEXT("Row: %d, Column : %d"), i, j);
				UE_LOG(LogTemp, Warning, TEXT("x: %f, y : %f"), x, y);
			}
		}

		TileMap.Add(RowArray);
	}
	
	// 타일의 주변 지뢰 숫자 세팅
}

 

아직 타일 주변 지뢰를 찾아 갯수를 세팅하는 부분은 생성하지 않았다.

현재 지뢰를 생성하는 로직을 그냥 순차적으로 부여하도록 해놓았다. 

지뢰배치는 나중으로 미룬다.

 

게임이 클리어되거나 새로운 게임을 생성할 때 사용될 DestroyBoard 함수도 제대로 만들어준다.

void ABoard::DestroyBoard()
{
	this->Row = 0;
	this->Column = 0;
	this->TotalMineCount = 0;

	for (int i = 0; i < Row; i++)
	{
		TArray<ATile*> row = TileMap[i];
		if (row.Num() != 0)
		{
			for (int j = 0; j < Column; j++)
			{
				row[j]->Destroy();
			}

			row.Empty();
		}
	}

	TileMap.Empty();
}

 

매번 TArray에서 찾아내기 불편하니 좌표를 받아 해당 타일을 가져오는 함수도 생성한다.

ATile* ABoard::GetTileAt(int x, int y)
{
	if (x < 0 || y < 0)
		return nullptr;

	if (x >= TileMap.Num())
		return nullptr;

	TArray<ATile*> column = TileMap[x];
	if (y >= column.Num())
		return nullptr;

	return column[y];
}

 

유저가 특정 타일을 터치 또는 클릭했을 때 Board에서 체크한다.

이 때 타일의 상태를 가져와 현재 게임 상태를 계산하여 게임 상태를 반환한다.

여기서 return된 게임 상태를 GameMode에서 체크하여 게임 종료 선언 등을 진행한다.

 

EMineGameState ABoard::CheckMineInUserTouchedTile(int32 x, int32 y, bool isSetFlag)
{
	if (x < 0 || x >= this->Row || y < 0 || y >= this->Row)
		return EMineGameState::GS_NotStartGame;

	ATile* userTouchTile = GetTileAt(x, y);
	if (userTouchTile == nullptr)
		return EMineGameState::GS_NotStartGame;

	ETileState touchTileState = userTouchTile->CheckTileState(isSetFlag);

	// 타일 메시 변경
	ChangeTileMesh(userTouchTile);

	// 타일 애니메이션 동작
	userTouchTile->StartTileAnimation();

	// 게임 상태 체크
	switch (touchTileState)
	{
	case ETileState::TS_OpenClear:
		// 해당 타일의 주변에 지뢰가 전혀 없으므로 주변 탐색 진행
		CheckNearTileState(x, y);
		break;
	case ETileState::TS_Mine:
		// 지뢰를 클릭했으므로 게임 종료
		return EMineGameState::GS_StepOnMine;
		break;
	case ETileState::TS_FlagNotMine:
		// 유저가 지뢰라고 생각하여 깃발을 심었지만 지뢰가 아님, 종료체크 안함
		this->UserSetFlagCount++;
		break;
	case ETileState::TS_NotOpenFlagNotMine:
		// 잘못된 깃발을 되돌리는 중
		userTouchTile->SetTileState(ETileState::TS_NotOpen);
		this->UserSetFlagCount--;
		break;
	case ETileState::TS_Flag:
		// 유저가 해당 타일이 지뢰라고 생각하여 깃발을 심었기에 종료 체크 진행
		this->UserSetFlagCount++;
		this->MineSetFlagCount++;
		break;
	case ETileState::TS_NotOpen:
		// 깃발을 눌렀을 때만 이게 반환됨. 현재 깃발 개수 감소
		this->MineSetFlagCount--;
		this->UserSetFlagCount--;
		break;
	default:
		// 게임 종료 상황이 아님
		break;
	}

	// 모든 지뢰를 찾고 잘못 선택한 지뢰가 없으면 게임 클리어
	if (MineSetFlagCount >= TotalMineCount && MineSetFlagCount == UserSetFlagCount)
		return EMineGameState::GS_FoundAllMine;

	return EMineGameState::GS_Playing;
}

 

터치한 타일의 주변을 체크하는 것은 재귀적으로 동작한다.

 

void ABoard::CheckNearTileState(int32 x, int32 y)
{
	if (x < 0 || x >= this->Row || y < 0 || y >= this->Column)
		return;

	CheckTileState(x, y - 1);	// 상
	CheckTileState(x, y + 1);	// 하
	CheckTileState(x - 1, y);	// 좌
	CheckTileState(x + 1, y);	// 우
}

void ABoard::CheckTileState(int32 x, int32 y)
{
	if (x < 0 || x >= this->Row || y < 0 || y >= this->Column)
		return;

	ATile* tile = GetTileAt(x, y);
	if (tile == nullptr)
		return;

	switch (tile->GetTileState())
	{
	case ETileState::TS_NotOpen:
		if (tile->GetTilehasMine() == true)
		{
			// 해당 타일이 지뢰면 멈춤
			break;
		}
		else if (tile->GetNearMineCount() == 0)
		{
			// 주변 지뢰가 없으면 다시 반복
			tile->SetTileState(ETileState::TS_OpenClear);
			CheckNearTileState(x, y);
		}
		else
		{
			// 주변 지뢰가 있으면 타일 상태만 바꿈
			tile->SetTileState(ETileState::TS_OpenCount);
		}

		ChangeTileMesh(tile);
		break;
	default:
		break;
	}

	return;
}

 

위 코드는 지뢰를 만나거나 타일 맵 밖을 만나게 된다면 종료될 것이다.

 

타일에 변화가 생긴다면 타일의 외형도 변경해주어야 한다.

이때는 Material만 바꿔 지속적으로 StaticMeshComponent를 재생성하는 일을 줄인다.

 

void ABoard::ChangeTileMesh(ATile* pTile)
{
	if (pTile == nullptr)
		return;

	switch (pTile->GetTileState())
	{
	case ETileState::TS_Mine:
		pTile->SetTileMeshMaterial(this->MineTileMaterial);
		break;
	case ETileState::TS_Flag:
	case ETileState::TS_FlagNotMine:
		pTile->SetTileMeshMaterial(this->FlagTileMaterial);
		break;
	case ETileState::TS_NotOpen:
		pTile->SetTileMeshMaterial(this->NormalTileMaterial);
		break;
	case ETileState::TS_OpenCount:
	case ETileState::TS_OpenClear:
		pTile->SetTileMeshMaterial(this->NumberTileMaterials[pTile->GetNearMineCount()]);
		break;
	default:
		break;
	}
}

 

이 Board는 블루프린트로 만들어 월드맵에 배치한다.

해당 보드의 위치에서 타일이 생성되기 때문에 보드 위치를 잘 설정하도록 한다.

 

이 모든 과정을 마치고 게임을 시작하면 아래처럼 타일이 배치된다.

 

 

 

 

728x90