[ Unreal Engine 5 / #27 Tile Game Score ]

SeungWoo·2024년 10월 24일
0

[ Ureal Engine 5 / 수업 ]

목록 보기
28/31
post-thumbnail
  • Tile이 매칭 됐을때, Score점수가 올라갈 수 있게 로직을 짜볼 예정이다.

  • IObserver 인터페이스 확장

    • OnNotifyRemainingMoves 함수를 추가

  • 게임을 인스턴스를 하나 만들어서 해당 값을 가지고 있게 변수를 선언할 예정이다

  • UGameInstance를 상속 받는 C++ 클래스를 만든다

    • 이름을 GI_Puzzle로 짓는다

GI_Puzzle.h

#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstance.h"
#include "GI_Puzzle.generated.h"

class UGameStateSub;
UCLASS()
class PUZZLESTUDY_API UGI_Puzzle : public UGameInstance
{
	GENERATED_BODY()
	UGI_Puzzle();

private:
	// 플레이 점수 
	int32 PlayerScore;

	// 게임 진행, 남은 매칭 횟수
	int32 RemainingMoves;

	// Observer 주제를 참조하는 변수 추가
	UGameStateSub* GameStateSubjectInstance;
public:

	// 점수 증가 매서드
	UFUNCTION(BlueprintCallable, Category = "Game Functions")
	void AddScore(int32 Points);

	// 남은 이동 횟수 감소 매서드
	UFUNCTION(BlueprintCallable, Category = "Game Funcitons")
	void DecreaseMoves();

	// 게임 상태 초기화 ( 레벨 시작시, 호출 - Retry )
	UFUNCTION(BlueprintCallable, Category = "Game Functions")
	void ResetGameState();

	// 현재 점수를 가져오기
	int32 GetCurrentScore() const;

	// 남은 이동 횟수 반환
	int32 GetRemainingMoves() const;

	// Observer 주제를 설정하는 실수
	void SetGameStateSubject(UGameStateSub* Subject);
};

GI_Puzzle.cpp

#include "GI_Puzzle.h"
#include "GameStateSub.h"

UGI_Puzzle::UGI_Puzzle()
{
	// 초기 점수 + 이동 점수
	PlayerScore = 0;
	RemainingMoves = 30; 
	GameStateSubjectInstance = nullptr;
}

void UGI_Puzzle::AddScore(int32 Points)
{
	PlayerScore += Points;

	UE_LOG(LogTemp, Log, TEXT("Score added. New score: %d"), PlayerScore);

	if (GameStateSubjectInstance)
	{
		UE_LOG(LogTemp, Log, TEXT("Notifying observers."));
		GameStateSubjectInstance->NotifyObservers(this);  // 'this'를 WorldContextObject로 전달
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("GameStateSubjectInstance is null. Cannot notify observers."));
	}

}

void UGI_Puzzle::DecreaseMoves()
{
	// 남은 움직이기 횟수가 있으면,
	if (RemainingMoves > 0)
	{
		RemainingMoves--;

		if (GameStateSubjectInstance)
		{
			// 이동 횟수 변경 알림
			UE_LOG(LogTemp, Log, TEXT("Notifying observers."));
			GameStateSubjectInstance->NotifyObservers(this);
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("GameStateSubjectInstance is null. Cannot notify observers."));
		}
	}
}

void UGI_Puzzle::ResetGameState()
{
	PlayerScore = 0;
	RemainingMoves = 30; 
}

int32 UGI_Puzzle::GetCurrentScore() const
{
	return PlayerScore;
}

int32 UGI_Puzzle::GetRemainingMoves() const
{
	return RemainingMoves;
}

void UGI_Puzzle::SetGameStateSubject(UGameStateSub* Subject)
{
	GameStateSubjectInstance = Subject;
}

  • ProjectSetting에 만든 클래스를 넣어주면서 해당 인스턴스 클래스를 게임내에 사용한다는 것이다

  • 타일이 부러졌을때, 해당 블록당 10점을 주기 위해 TileGrid::RemoveMatchingTiles 함수 로직 변경

GameWidgetObserver.h

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Observer.h"
#include "GameWidgetObserver.generated.h"

UCLASS()
class PUZZLESTUDY_API UGameWidgetObserver : public UUserWidget, public IObserver
{
	GENERATED_BODY()

private:
	// 현재 점수 변수
	// GameInstance GI_Puzzle 에서 대체
	// int32 CurrentScore;

protected:
	// UI에서 바인딩된 TextBlock을 가져오기 위한 변수 선언
	// BindWidget 이노테이션을 사용하여 Blueprint의 위젯을 바인딩
	UPROPERTY(meta = (BindWidget))
	class UTextBlock* ScoreText;

	UPROPERTY(meta = (BindWidget))
	class UTextBlock* MovesText; // 남은 이동 횟수를 표시할 TextBlock

public:
	//주체로부터 상태변화를 수신할 때 호출하는 함수
	virtual void OnNotify_Implementation(int32 UpdateScore) override;
	//주체로부터 상태변화를 수신할 때 호출하는 함수
	virtual void OnNotifyRemainingMoves_Implementation(int32 RemainingMoves) override;

	//UFUNCTION(BlueprintImplementableEvent, Category = "UI")
	UFUNCTION()
	void UpdateScoreUI(int32 NewScore);
};

GameWidgetObserver.cpp

#include "GameWidgetObserver.h"
#include "GI_Puzzle.h"
#include "Components\TextBlock.h"
#include "Kismet\GameplayStatics.h"

void UGameWidgetObserver::OnNotify_Implementation(int32 UpdateScore)
{
	UE_LOG(LogTemp, Log, TEXT("OnNotify called with score: %d"), UpdateScore);

	UpdateScoreUI(UpdateScore);
}

void UGameWidgetObserver::OnNotifyRemainingMoves_Implementation(int32 RemainingMoves)
{
	if (MovesText)
	{
		MovesText->SetText(FText::AsNumber(RemainingMoves));
	}
}

void UGameWidgetObserver::UpdateScoreUI(int32 NewScore)
{
	// ScoreText가 바인딩되어 있는지 확인 
	if (ScoreText)
	{
		// TextBlock의 내용을 업데이트
		ScoreText->SetText(FText::AsNumber(NewScore));
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("ScoreText is not bound to the widger. "));
	}
}

Match3GameMmode.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "Match3GameMmode.generated.h"

class UGameWidgetObserver;
UCLASS()
class PUZZLESTUDY_API AMatch3GameMmode : public AGameModeBase
{
	GENERATED_BODY()
	
public:
	virtual void BeginPlay() override;


	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TSubclassOf<UGameWidgetObserver> MainWidgetClass;
};

  • 바인딩을 위해 블록을 만들고 블록이름을 변수 이름과 일치 시킨다

  • GameMode에 만든 WBP 위젯을 넣어준다

Match3GameMmode.cpp

#include "Match3GameMmode.h"

#include "Kismet\GameplayStatics.h"
#include "GameStateSub.h"
#include "GameWidgetObserver.h"
#include "GI_Puzzle.h"

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

	UGI_Puzzle* MyGameInstance = Cast<UGI_Puzzle>(UGameplayStatics::GetGameInstance(GetWorld()));
	if (MyGameInstance)
	{
		MyGameInstance->ResetGameState();
	}

	// Observer 주체 생성
	UGameStateSub* ObserverGameState = NewObject<UGameStateSub>();

	// GameInstance에 Observer 주체 설정
	if (MyGameInstance)
	{
		MyGameInstance->SetGameStateSubject(ObserverGameState);
	}

	if (MainWidgetClass)
	{
		UGameWidgetObserver* scoreWidget = CreateWidget<UGameWidgetObserver>(GetWorld(), MainWidgetClass);
		if (scoreWidget)
		{
			scoreWidget->AddToViewport();
			// 위젯을 옵저버로 등록 
			ObserverGameState->RegisterObserver(scoreWidget);

			// 디버그 로그 추가
			UE_LOG(LogTemp, Log, TEXT("ScoreWidget registered as observer."));
		}
	}
}

GameStateSub.h

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "Observer.h"
#include "GameStateSub.generated.h"

/**
 * 역할 : 주체 클래스 -> 상태를 관리하고 변경 시 옵저버들에게 알림을 보냄
 */
UCLASS()
class PUZZLESTUDY_API UGameStateSub : public UObject
{
	GENERATED_BODY()
	

private: 
	// 옵저버 리스트 : 등록된 옵저버들이 상태 변화를 수신함
	TArray<TScriptInterface<IObserver>> Observers;

public:
	// 생성자 : 초기 점수 설정
	UGameStateSub();

	// 옵저버 등록 : 주체가 상태 변화를 알릴 옵저버를 등록함
	void RegisterObserver(TScriptInterface<IObserver> observer);

	// 옵저버 등록 해제 : 주체가 상태 변화 알림을 중지할 옵저버를 제거함
	void UnregisterObserver(TScriptInterface<IObserver> observer);

	// 상태 변화 발생 시 모든 옵저버에게 알림
	void NotifyObservers(UObject* WorldContextObject);

	void OnNotifyRemainingMoves(UObject* WorldContextObject);

};

GameStateSub.cpp

#include "GameStateSub.h"
#include "GI_Puzzle.h"
#include "Kismet\GameplayStatics.h"

UGameStateSub::UGameStateSub()
{

}

void UGameStateSub::RegisterObserver(TScriptInterface<IObserver> observer)
{

	// 옵저버를 리스트에 추가
	Observers.Add(observer);
}

void UGameStateSub::UnregisterObserver(TScriptInterface<IObserver> observer)
{
	// 옵저버를 리스트에서 제거
	Observers.Remove(observer);
}

void UGameStateSub::NotifyObservers(UObject* WorldContextObject)
{
	//옵저버 리스트에서 유효하지 않은 옵저버 제거
	Observers.RemoveAll([](const TScriptInterface<IObserver>& Observer)
		{
			if (!IsValid(Observer.GetObject()))
			{
				UE_LOG(LogTemp, Warning, TEXT("Invalid observer detected, removing from list "));
				return true;
			}
			return false;
		});

	// WorldContextObject가 nullprt이 아닌지 확인
	if (!WorldContextObject)
	{
		UE_LOG(LogTemp, Error, TEXT("WorldContextObject is null"));
		return;
	}

	// GameInstance 가져오기
	UGI_Puzzle* GameInstance = Cast<UGI_Puzzle>(UGameplayStatics::GetGameInstance(WorldContextObject));

	if (!GameInstance)
	{
		UE_LOG(LogTemp, Warning, TEXT("Faild to get GameInstance from WorldContextObject"));
		return;
	}

	int32 CurrentScore = GameInstance->GetCurrentScore();

	// 옵저버들에게 점수 업데이트 알림
	for (const TScriptInterface<IObserver>& Observer : Observers)
	{

		if (IsValid(Observer.GetObject())
			&& Observer.GetObject()->GetClass()->ImplementsInterface(UObserver::StaticClass()))
		{
			/*UE_LOG(LogTemp, Warning, TEXT(" Observer : %s "), Observer.GetObject()->GetName());*/
			// 옵저버에게 알림 
			IObserver::Execute_OnNotify(Observer.GetObject(), CurrentScore);
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("Invalid observer or interface not implemented "));
		}
	}
}


void UGameStateSub::OnNotifyRemainingMoves(UObject* WorldContextObject)
{
	Observers.RemoveAll([](const TScriptInterface<IObserver>& Observer)
		{
			return !IsValid(Observer.GetObject());
		});

	// WorldContextObject가 nullptr이 아닌지 확인
	if (!WorldContextObject)
	{
		UE_LOG(LogTemp, Error, TEXT("WorldContextObject is null."));
		return;
	}

	// GameInstance 가져오기
	UGI_Puzzle* GameInstance = Cast<UGI_Puzzle>(UGameplayStatics::GetGameInstance(WorldContextObject));

	if (!GameInstance)
	{
		UE_LOG(LogTemp, Error, TEXT("Faild to get GameInstance"));
		return;
	}

	int32 RemainingMoves = GameInstance->GetRemainingMoves();
	
	for (const TScriptInterface<IObserver>& Observer : Observers)
	{
		if (IsValid(Observer.GetObject()) 
			&& Observer.GetObject()->GetClass()->ImplementsInterface(UObserver::StaticClass()))
		{
			// 옵저버에게 알림
			IObserver::Execute_OnNotifyRemainingMoves(Observer.GetObject(), RemainingMoves);
		}
		else
		{
			UE_LOG(LogTemp, Warning, TEXT("Invalid observer or Interface not implemented."));
		}
	}
}
  • 게임 내 스왑 시 남은 이동 횟수 차감을 한다

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. "));
		return;
	}

	UGI_Puzzle* GameInstance = Cast<UGI_Puzzle>(UGameplayStatics::GetGameInstance(TileGrid->GetWorld()));
	if (GameInstance)
	{
		GameInstance->DecreaseMoves(); // 스왑 시 이동 횟수 차감
	}

	// 그리드에서 타일을 교환
	TileGrid->SwapTiles(FirstTile, SecondTile);

	// 타일 swap
	TArray<ATile*> MachingTiles = TileGrid->CheckForMatches();
	if (MachingTiles.Num() > 0) // 매칭이 있을 경우
	{
		TileGrid->RemoveMatchingTIles(MachingTiles);

	}
	else
	{
		// 매칭이 안될시 돌아가기
		Undo();
	}


	// TODO : 매칭 확인 등의 로직
}
profile
This is my study archive

0개의 댓글