[ Unreal Engine 5 / #24 Tile Game 교환등 기능 추가 ]

SeungWoo·2024년 10월 21일
0

[ Ureal Engine 5 / 수업 ]

목록 보기
25/31
post-thumbnail

  • TileGrid를 블루프린트로 만들고 그냥 Add 버튼으로 'Arrow'와 'Plane'를 하나 만든뒤, 월드에 배치 한다

  • Tile 도 블루 프린트로 만들어 놓고

  • Tile.h 로직 수정

  • Tile.cpp 로직 수정

  • TileGrid.h 를 약간 수정

  • TileGrid.cpp 를 약간 수정


  • 월드에 있는 Grid위치 조정

  • PlayerController를 상속받은 C++ 클래스 하나 추가한다

  • 만든 클래스를 블루프린트로 만들고, Worldsetting에 설정한다

  • TWeakObjectPtr<ATile> :
    • 가비지 컬렉션이 발생해도 안전하게 타일을 참조할 수 있습니다. 객체가 삭제되었는지 여부를 확인할 수 있는 기능을 제공합니다.
    • TWeakObjectPtr는 대상 객체가 사라질 수 있음을 가정하고, 객체가 유효한지 체크할 수 있는 기능을 가지고 있습니다.
    • 예를 들어 스타크래프트에 부대지정이라는 기능이 있다, 해당 부대를 지정하고 싸운후, 부대에 지정한 객체들이 없어질수도 다 사라졌을때, 그 공간을 언리얼 엔진에서 없애거나, 계속 찾을 수 있다 그래서 그 공간을 또 새로 만들고 그렇지 않고 그 공간을 Nullptr로 내용을 담아 그 공간을 계속해서 가지고 있는 것이다 이렇게 하면 새로 만들고 지우면서 발생하는 연산을 하지않고, 그 공간을 재활용할 수 있다
  • TObjectPtr<ATile> :
    • Unreal Engine 5에서 추가된 스마트 포인터로, UObject에 대한 강력한 참조를 유지하면서 GC와 연동됩니다.
  • UPROPERTY() 를 사용한 UObject 참조 :
    • UPROPERTY() 매크로를 사용하여 Unreal의 가비지 컬렉션 시스템에 의해 관리되는 포인터로 설정할 수 있습니다. 이를 통해 GC 보호를 받을 수 있습니다.

  • ATileGamePlayerController
    • GetHitResultUnderCursor 함수는 마우스 커서가 가리키는 화면 내 위치에 있는 액터를 찾아 Hit 객체에 정보를 채웁니다.
    • 이 함수의 첫 번째 인자는 탐색할 충돌 채널(ECC_Visibility), 두 번째 인자는 복잡한 충돌을 사용할지 여부, 세 번째 인자는 결과가 저장되는 FHitResult 참조

  • InputAction : bool
  • IMC_Main IA_Select 등록후, 마우스 왼쪽 버튼

  • 타일 선택 시 시각적 피드백 추가 (선택된 타일 강조)

코드 정리

Tile.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Tile.generated.h"

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

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, BlueprintReadWrite, Category = "Tile Properties")
	FName TileType;

	// 타일의 외형을 위한 Static Mesh Component
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tile Properties")
	UStaticMeshComponent* TileMesh;

	// 타일의 StaticMesh 설정을 위한 TSubclassOf
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Tile Properties")
	TMap<FName, UStaticMesh*> TileMeshes;

	// 타일 매칭 확인 함수
	bool IsMatching(ATile* OtherTile);

	// 타일이 선택되었는지 여부
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Tile")
	bool bIsSelected;

	// 타일을 선택 또는 해제하는 함수
	void SetSelected(bool bSelected);

	// 타일의 외형을 TileType에 따라 설정하는 함수
	void UpdateTileAppearance();

	// 병렬 처리를 테스트하는 함수
	void ProcessDataInParallerl();

protected:
	void UpdateAppearance();
};

Tile.cpp

#include "Tile.h"
#include "Components\StaticMeshComponent.h"
#include "Engine\Engine.h"
#include "Async\ParallelFor.h"

// Sets default values
ATile::ATile()
{
 	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	// StaticMeshComponent 생성
	TileMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("TileMesh"));
	RootComponent = TileMesh; // 타일의 루트 컴포넌트 설정

	// 메시 업데이트를 위한 타일 타입 초기화 
	TileType = FName("Default");

	bIsSelected = false; // 선택여부 초기화 
}

// Called when the game starts or when spawned
void ATile::BeginPlay()
{
	Super::BeginPlay();
	
	// 타일 외형 업데이트 ( TileType에 따라 메시 변경 )
	UpdateTileAppearance();
}

// Called every frame
void ATile::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

}

bool ATile::IsMatching(ATile* OtherTile)
{
	return TileType == OtherTile->TileType;
}

void ATile::SetSelected(bool bSelected)
{
	bIsSelected = bSelected;
	UpdateAppearance();
}

void ATile::UpdateTileAppearance()
{
	// TileMeshs Map에 TileType에 해당하는 키값이 존재하는가?
	if (TileMeshes.Contains(TileType))
	{
		TileMesh->SetStaticMesh(TileMeshes[TileType]);
	}
	else
	{
		UE_LOG(LogTemp, Error, TEXT("Attempting to update tile appearance"));
	}
}

void ATile::ProcessDataInParallerl()
{

	TArray<int32> DataArray;
	DataArray.Init(0, 100);

	ParallelFor(DataArray.Num(), [&](int32 Index)
		{
			DataArray[Index] = Index * 2;

			if (GEngine)
			{
				GEngine->AddOnScreenDebugMessage(-1, 5.0f, FColor::Red, FString::Printf(TEXT("Tile %d - %d "), Index, DataArray[Index]));
			}
		}
	);
	UE_LOG(LogTemp, Warning, TEXT("ParallerlForFinish"));
}

void ATile::UpdateAppearance()
{
	// 선택되었을 때의 시각적 피드백 ( 예: 색상 변경 )
	if (bIsSelected)
	{
		// 타일이 선택되었을 때의 효과
		// 예: StaticMeshComponent의 색상 변경
		if (UStaticMeshComponent* MeshComponent = TileMesh)
		{
			// 선택된 타일을 강조
			TileMesh->SetRenderCustomDepth(true);
			// 강조된 색상 변경
			TileMesh->SetScalarParameterValueOnMaterials(TEXT("EmissiveStrength"), 10.0f);
		}
	}
	else
	{
		if (UStaticMeshComponent* MeshComponent = TileMesh)
		{
			// 기본 상태로 복귀
			TileMesh->SetRenderCustomDepth(false);
			// 기본 색상 변경
			TileMesh->SetScalarParameterValueOnMaterials(TEXT("EmissiveStrength"), 0.0f);
		}
	}
}

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();
};

TileGrid.cpp

#include "TileGrid.h"
#include "Tile.h"
#include "Async\ParallelFor.h"
#include "Async/Async.h"


void ATileGrid::BeginPlay()
{
	Super::BeginPlay();
	InitializeGrid();
}

// Sets default values
ATileGrid::ATileGrid()
{
	PrimaryActorTick.bCanEverTick = true;
	
	GridWidth = 8;
	GridHeigh = 8;
	TileSpacing = 100;
	TileArray.SetNum(GridWidth * GridHeigh); 
	// 1차원 배열로 메모리 할당 8*8 형태
}

void ATileGrid::InitializeGrid()
{


	// 가능한 TileType 리스트
	TArray<FName> TileTypes =
	{
		FName("Cone"),
		FName("Cube"),
		FName("Cylinder"),
		FName("Sphere"),
		FName("Capsule"),
		FName("Pyramid")
	};

	for (int32 x = 0; x < GridWidth; ++x)
	{
		for (int32 y = 0; y < GridHeigh; ++y)
		{

			// 비동기적으로 타일을 생성
			AsyncTask(ENamedThreads::GameThread, [this, x, y, TileTypes]()
				{
					// 타일 타입을 핸덤하게 결정 ( 비동기 작업 )
					FName RandomTileType = TileTypes[FMath::RandRange(0, TileTypes.Num() - 1)];
					
					// 게임 스레드에서 타일 생성과 외형 설정을 처리
					AsyncTask(ENamedThreads::GameThread, [this, x, y, RandomTileType]()
						{
							if (!TileClass)
							{
								UE_LOG(LogTemp, Warning, TEXT("TileClass is not set in TileGrid "));
								return;
							}
							
							FActorSpawnParameters SpawnParams;
							ATile* NewTile = GetWorld()->SpawnActor<ATile>(TileClass, SpawnParams);

							if (NewTile)
							{
								NewTile->TileType = RandomTileType;

								NewTile->UpdateTileAppearance();

								// 타일을 TIleGrid의 자식으로 부착
								NewTile->AttachToActor(this, FAttachmentTransformRules::KeepRelativeTransform);

								FVector RelativeLocation = FVector(x * TileSpacing, y * TileSpacing, 0.0f);
								NewTile->SetActorRelativeLocation(RelativeLocation);

								// 그리드에 타일 저장
								SetTile(x, y, NewTile);

								UE_LOG(LogTemp, Warning, TEXT("Tile Create at [ %d, %d ] with type %s "), x, y, *RandomTileType.ToString());
							}
							else
							{
								UE_LOG(LogTemp, Warning, TEXT("Failed to Spawn Tile at [ %d, %d ]"), x, y);
							}

						});
				});
		}
	}
	// 가능한 TileType 리스트
}

ATile* ATileGrid::GetTile(int32 x, int32 y) const
{
	if (x < 0 || x >= GridWidth || y < 0 || y >= GridHeigh)
	{
		// 유효하지 않은 좌표 거리
		return nullptr;
	}
	return TileArray[y * GridWidth + x];

}

void ATileGrid::SetTile(int32 x, int32 y, ATile* Tile)
{
	if (x >= 0 && x < GridWidth && y >= 0 && y < GridHeigh)
	{
		TileArray[y * GridWidth + x] = Tile;
	}
}

// 바둑판 출력 함수
void ATileGrid::DrawGrid()
{
	// 1차원 배열을 바둑판 형태로 출력
	for (int32 i = 0; i < Rows; i++)
	{
		FString RowText;

		for (int32 j = 0; j < Colums; j++)
		{
			int32 Index = i * Colums + j; 
			RowText += FString::Printf(TEXT("%2d "), GridArray[Index]);
		}

		// 현재 행 출력
		UE_LOG(LogTemp, Warning, TEXT("Row %d : %s "), i, *RowText);
	}
}

TileGamePlayerController.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "InputActionValue.h"
#include "TIleGmaePlayerController.generated.h"

class UInputMappingContext;
class UInputAction;
class ATile;
class ATileGrid;

UCLASS()
class PUZZLESTUDY_API ATIleGmaePlayerController : public APlayerController
{
	GENERATED_BODY()
	

public:
	ATIleGmaePlayerController();

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

	// 마우스 커서 보이기
	virtual void SetupInputComponent() override;

	// 첫 번째와 두 번째로 선택된 타일을 약한 참조로 저장 ( GC 대응 )
	TWeakObjectPtr<ATile> FirstSelectedTile; // +참고 _)UPROPETY()를 사용하면 Unreal Engine의 가비지 컬렉션에 의해 관리되므로, 안전하게 참조를 유지
	TWeakObjectPtr<ATile> SecondSelectedTile; // +참고 _)UPROPETY()를 사용하면 Unreal Engine의 가비지 컬렉션에 의해 관리되므로, 안전하게 참조를 유지
	// TWeakObjectPtr를 사용하면 가비지 켈렉션으로 삭제된 객체가 있는지 체크 할 수 있습니다
	// 예시 : 스타그래프트 부대 지정 메모리를 간직하기 위해서

	// TileGrid에 대한 참조
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Grid")
	ATileGrid* TileGrid;

	// Input 관련
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputMappingContext* InputMapping;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Input")
	UInputAction* SelectTileAction;

	// 타일을 선택하는 함수
	void SelectTile(const FInputActionValue& Value);

	// 두 개의 타일을 선택하고 처리하는 함수
	void ProcessSelectedTile();
};

TileGamePlayerController.cpp

#include "TIleGmaePlayerController.h"
#include "Tile.h"
#include "EnhancedInputComponent.h"
#include "EnhancedInputSubsystems.h"
#include "SwapTileCommand.h"
#include "TileCommandInvoker.h"

ATIleGmaePlayerController::ATIleGmaePlayerController()
{
	// 약한 참조로 타일을 관리 ( 처음에는 아무 것도 선택되 않는 상태 )
	FirstSelectedTile = nullptr;
	SecondSelectedTile = nullptr;
}

void ATIleGmaePlayerController::BeginPlay()
{
	Super::BeginPlay();

	// 마우스 커서 보이기 설정
	bShowMouseCursor = true;

	// Enhanced Input 설정
	if (APlayerController* PlayerController = Cast<APlayerController>(this))
	{
		// Enhanced Input 하위 시스템을 추가하여 InputMappingContext를 활성화
		UEnhancedInputLocalPlayerSubsystem* Subsystem = ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(PlayerController->GetLocalPlayer());
		if (Subsystem)
		{
			Subsystem->AddMappingContext(InputMapping, 0); // 우선순위 0으로 설정
		}
	}
}

void ATIleGmaePlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

	if (UEnhancedInputComponent* EnhancedInputComp = Cast< UEnhancedInputComponent>(InputComponent))
	{
		EnhancedInputComp->BindAction(SelectTileAction, ETriggerEvent::Triggered, this, &ATIleGmaePlayerController::SelectTile);
	}
}

void ATIleGmaePlayerController::SelectTile(const FInputActionValue& Value)
{
	// 마우스 클릭 위치를 가져옴
	FHitResult Hit; 
	GetHitResultUnderCursor(ECC_Visibility, false, Hit);

	if (Hit.bBlockingHit)
	{
		ATile* ClickedTile = Cast<ATile>(Hit.GetActor());
		if (ClickedTile)
		{
			if (!FirstSelectedTile.IsValid())
			{
				// 첫 번째 타일 선택
				FirstSelectedTile = ClickedTile;
				FirstSelectedTile->SetSelected(true);

				UE_LOG(LogTemp, Warning, TEXT("First Tile selected : %s "), *FirstSelectedTile->GetName());
			}
			else if (!SecondSelectedTile.IsValid() && ClickedTile != FirstSelectedTile)
			{
				// 두 번쨰 타일 선택
				SecondSelectedTile = ClickedTile;
				SecondSelectedTile->SetSelected(true);

				UE_LOG(LogTemp, Warning, TEXT("Second Tile selected : %s "), *SecondSelectedTile->GetName());

				// 두 타일 선택 완료 후 처리
				ProcessSelectedTile();
			}
		}
	}
}

void ATIleGmaePlayerController::ProcessSelectedTile()
{
	// 두 개의 타일이 선택되었을때,
	if (FirstSelectedTile.IsValid() && SecondSelectedTile.IsValid())
	{
		// 타일 처리 로직 ( 자리교한, 매칭 확인 ) 


		// 교환 명령 생성
		USwapTileCommand* SwapCommand = NewObject<USwapTileCommand>(); // 클래스 이름 수정
		SwapCommand->Initalize(FirstSelectedTile.Get(), SecondSelectedTile.Get());

		// 커맨드 실행
		ATileCommandInvoker* CommandInvoker = GetWorld()->SpawnActor<ATileCommandInvoker>();
		CommandInvoker->ExcuteCommand(SwapCommand);

		// 매칭 확인 로직 ( 교환 후 매칭이 있는지 확인 )
		// MatchCheck(FirstSelectedTile, SecondSelectedTile);
	
		// 두 타일의 선택 해제
		FirstSelectedTile->SetSelected(false);
		SecondSelectedTile->SetSelected(false);

		// 선택 초기화
		FirstSelectedTile = nullptr;
		SecondSelectedTile = nullptr;
	}
}

SwapTileCommand.h

#pragma once

#include "CoreMinimal.h"
#include "Command.h"
#include "SwapTileCommand.generated.h"

class ATile;

UCLASS()
class PUZZLESTUDY_API USwapTileCommand : public UObject, public ICommand
{
	GENERATED_BODY()

private:

	ATile* FirstTile;
	ATile* SecondTile;
	
	FVector FirstTileOrigineType;
	FVector SecondTileOrigineType;


public:
	void Initalize(ATile* InFirstTile, ATile* InSecondTile);

	// ICommand의 인터페이스 함수들 Imprement ~ 
	virtual void Excute() override;
	virtual void Undo() override;

};

SwapTileCommand.cpp

#include "SwapTileCommand.h"
#include "Tile.h"

void USwapTileCommand::Initalize(ATile* InFirstTile, ATile* InSecondTile)
{
	FirstTile = InFirstTile;
	SecondTile = InSecondTile;

	// 원래 위치를 저장
	FirstTileOrigineType = FirstTile->GetActorLocation();
	SecondTileOrigineType = SecondTile->GetActorLocation();
}

void USwapTileCommand::Excute()
{
	// 서로 두 타일 위치를 교환 
	FirstTile->SetActorLocation(SecondTileOrigineType);
	SecondTile->SetActorLocation(FirstTileOrigineType);

	// TODO : 매칭 확인 등의 로직
}

void USwapTileCommand::Undo()
{
	// 타일의 워치를 원래대로 되돌림 
	FirstTile->SetActorLocation(FirstTileOrigineType);
	SecondTile->SetActorLocation(SecondTileOrigineType);
}

profile
This is my study archive

0개의 댓글