Tile이 매칭 됐을때, Score점수가 올라갈 수 있게 로직을 짜볼 예정이다.
IObserver 인터페이스 확장
게임을 인스턴스를 하나 만들어서 해당 값을 가지고 있게 변수를 선언할 예정이다
UGameInstance를 상속 받는 C++ 클래스를 만든다
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;
}
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;
};
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 : 매칭 확인 등의 로직
}