플레이어의 행동을 담은 행동 구조체 (Action Struct)의 정보를 런타임중에 수정해야하기 때문에 DataTable의 정보를 가진 게임 매니저를 GameInstance를 이용해 만들기로 했다.
GameInstance
게임이 시작되면 엔진을 초기화하고 가장 먼저 실행하는 오브젝트이며 프로그램이 종료될 떄 가장 마지막에 사라지는 오브젝트이다. 레벨 이동 간에 유지돼야 하는 정보를 저장하고 게임 전반에 걸쳐 사용하는 기능을 관리하기 적합하다.

일단 GameInstance를 만든다.

그 후 Project Settings 에서 GameInstance를 방금 만든 GameInstance로 설정해준다.
//MyGameInstance.h
UCLASS()
class SSOAJUKSHOUSE_API UMyGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
// Init 함수는 게임이 시작되고 가장 먼저 호출된다.
virtual void Init() override;
// 행동 구조체를 저장하기위한 ActuonStruct 배열
// 해당 배열을 레벨의 Interact 오브젝트들이 접근해야하기 때문에 public 으로 선언한다.
UPROPERTY()
TArray<FActionStruct> Actions;
// Level에 있는 Interact Object들이 IsShow를 On/Off 할 수 있는 함수
UFUNCTION(BlueprintCallable)
void IsShowOn(FName ActionName);
UFUNCTION(BlueprintCallable)
void IsShowOff(FName ActionName);
private:
// Action Struct가 저장돼있는 DataTable
UPROPERTY()
UDataTable* DT_ActionData = nullptr;
// Action을 Name으로 식별하기 위한 Map
UPROPERTY()
TMap<FName, int32> ActionMap;
};
Init()
Init 함수는 게임이 시작되고 가장 먼저 호출된다.
TArray Actions
해당 배열을 레벨의 Interact 오브젝트들이 접근해야하기 때문에 public 으로 선언한다.
DT_ActionData
Action Struct가 저장돼있는 DataTable.
ActionMap
Action을 Name으로 식별하여 행동 구조체 배열 (Actions)에서 접근을 한번에 하기 위한 맵.
//MyGameInstance.cpp
void UMyGameInstance::Init()
{
// ActionStruct 정보가 담긴 DT_Action을 불러온다.
DT_ActionData = LoadObject<UDataTable>(nullptr, TEXT("DataTable'/Game/BluePrint/DataTable/DT_Action.DT_Action'"));
// DT_Action DataTable의 모든 RowName을 불러와 FName 배열에 할당한다.
TArray<FName> ActionNames = DT_ActionData->GetRowNames();
// FName을 Key로 해당 index를 Value로 ActionMap에 저장한다.
// 그 후 RowName과 일치한 행의 Struct를 Actions 배열에 추가한다.
for (int i = 0; i < ActionNames.Num(); i++) {
ActionMap.Add(ActionNames[i], i);
Actions.Add(*(DT_ActionData->FindRow<FActionStruct>(ActionNames[i], ActionNames[i].ToString())));
}
}
void UMyGameInstance::IsShowOn(FName ActionName)
{
// Level에 배치된 InteractObject가 ActionName으로 해당 Action을 활성화한다.
Actions[ActionMap[ActionName]].IsShow = true;
}
void UMyGameInstance::IsShowOff(FName ActionName)
{
// Level에 배치된 InteractObject가 ActionName으로 해당 Action을 비활성화한다.
Actions[ActionMap[ActionName]].IsShow = false;
}
LoadObject와 FindObject
LoadObject는 아직 메모리에 로드되지 않은 리소스를 로드할 때 사용한다. 패키지 외부에 저장된 리소스를 가져올 때 사용한다.
FindObject는 이미 메모리에 로드된 객체를 찾는데 사용한다. 이미 로드된 리소스를 검색하는데 적합하다.
메모리에 로드된다는 것은 레벨에 객체가 배치된 경우나 코드나 시스템에 의해 로드된 경우를 말한다.
해당 DataTable은 GameInstance에서 가장 처음 불러오기 때문에 LoadObject를 하는 것이 적합하다.
TMap
Key - Value 쌍을 관리하는 자료구조로 Key를 사용하여 효율적으로 Value를 찾을 수 있다. 언리얼에서는 Value로 Key를 찾는 기능을 제공하지 않기 때문에 Key를 FName으로 설정했고, Value를 Actions의 Index값인 int로 설정했다. Action Name으로 접근하는 것이 더 직관적이기 때문이다.
//ActionWidget.cpp
void UActionWidget::NativeOnInitialized()
{
// GameInstance에 접근하여 ActionStruct 배열을 복사한다.
MyGameInstance = Cast<UMyGameInstance>(GetGameInstance());
ActionArray = MyGameInstance->Actions;
int ActionCount = 1;
for (int i = 0; i < ActionArray.Num(); i++) {
if (ActionArray[i].IsShow) {
FString ButtonName = FString::Printf(TEXT("ActionButton%d"), ActionCount++);
FName path = FName(*ButtonName);
button = Cast<UActionButton>(GetWidgetFromName(path));
button->ActionStruct = ActionArray[i];
button->ActionName->SetText(FText::FromString(ActionArray[i].ActionName));
button->RenderOpacity = 1;
Buttons.Add(button);
}
}
float Radius = 300.0f;
float Angle = 360.0f / (ActionCount - 1);
for (int i = 0; i < ActionCount - 1; i++) {
float Radians = FMath::DegreesToRadians(Angle * i);
float X = Radius * FMath::Cos(Radians);
float Y = Radius * FMath::Sin(Radians);
Buttons[i]->SetRenderTranslation(FVector2D(X, Y));
}
}
GameInstance에 접근하여 ActionStruct 배열을 복사하고 IsShow가 true인 Struct만 버튼에 할당하고 버튼을 표시한다.
저번에는 ActionWidget에서 DataTable정보를 가져와 버튼을 할당했지만 이제는 GameInstance에서 정보를 가져온다.

Interact Object에서는 Interact라는 BluePrintInterface를 생성하여 동일한 이름을 가진 함수들을 만들고 플레이어가 해당 함수를 실행시키고 오브젝트는 MyGameInstance에 접근하여 Action Name에 맞는 Struct의 IsShow를 On / Off 한다.

그냥 행동 버튼을 불러왔을 때

Interact Object를 trace하고 행동 버튼을 불러왔을 때
왼쪽 위에 Point라는 행동 버튼이 추가 되었고 이제 Interact할 오브젝트마다 행동을 DataTable에 추가하고 오브젝트의 함수에 해당 Action Name만 입력해주면 특정 행동이 추가된다.