오늘은 아케인 보드(룬) 시스템의 룬 배치 및 검증 로직을 구현할 생각이었음
근데 검증 로직을 구현하다가 몇 가지 설계 변경이 필요하다는 걸 깨달았음
이전에 미처 생각 못한 부분들 수정하고 최적화했음
처음에는 ArcaneBoardManager 생성자에서 직접 데이터 테이블 로드하려고 했음:
// 이전 접근법 (문제점 발견)
static ConstructorHelpers::FObjectFinder<UDataTable> RuneTableAsset(TEXT("/Game/Data/DT_RuneTable"));
if (RuneTableAsset.Succeeded())
{
RuneTable = RuneTableAsset.Object;
}
근데 프로젝트가 여러 시스템이 연결되는 구조인데, 매니저가 직접 경로 알고 로드하면 유지보수 어려울거 같았음
그래서 ArcaneBoardManager를 가지고 있는 ArcaneBoardLPS에서 할당해주기로 함
이렇게 바꾸면 경로 관리도 한 곳에서 할 수 있고, 테스트할 때도 더 유연하게 할 수 있을 듯
처음에는 단순히 배열로 셀 데이터 관리했음
그래서 특정 위치의 셀 찾으려면 매번 반복문 돌려야 했음
// 이전 코드 (비효율적)
bool IsValidCell(const FIntPoint& Pos)
{
for (const FGridCellData& Cell : GridCells)
{
if (Cell.Pos == Pos && Cell.State == EGridCellState::Empty;)
{
return true;
}
}
return false;
}
이 함수가 자주 호출되는데 O(n) 복잡도면 비효율적이라 생각해서 TMap 구조로 변경했음
// 최적화된 코드: O(1) 접근
TMap<FIntPoint, FGridCellData> CurrGridState;
bool UGS_ArcaneBoardManager::IsValidCell(const FIntPoint& Pos)
{
if (CurrGridState.Contains(Pos))
{
return CurrGridState[Pos].State == EGridCellState::Empty;
}
return false;
}
메모리를 전보다 조금 더 쓰긴 하지만 그리드 자체가 작기 때문에 무시할 수 있을 듯
저장 시스템 구조잡다가 언리얼 엔진 직렬화 시스템 제한에 걸려서 빌드 오류 발생했음
error: The type 'TArray<FPlacedRuneInfo>' can not be used as a value in a TMap
TMap 값으로 FPlacedRuneInfo의 Array를 사용하면 직렬화 안 된다는 문제였음
고민하다가 중간 구조체 만들어서 해결함
// 문제 있던 코드
TMap<ECharacterClass, TArray<FPlacedRuneInfo>> SavedRunesByClass;
// 해결책: 중간 구조체 도입
USTRUCT(Atomic, BlueprintType)
struct FRunePlacementData
{
GENERATED_BODY()
UPROPERTY()
TArray<FPlacedRuneInfo> PlacedRunes;
};
// 사용 방식
UPROPERTY()
TMap<ECharacterClass, FRunePlacementData> SavedRunesByClass;
이렇게 바꾸니 빌드도 잘 되고, 언리얼 직렬화 시스템 제대로 작동하게 됨
처음에는 그리드 레이아웃 데이터 에셋 셀 상태 직접 수정하려고 했는데, 이러면 문제 생길 것 같았음
데이터 에셋은 원본 데이터고, 런타임에서는 복사본 수정해야 함
// 잘못된 접근법 (데이터 에셋 직접 수정)
CurrGridLayout->GridCells[index].State = EGridCellState::Occupied;
// 개선된 접근법: 런타임 상태 별도 관리
void InitGridState()
{
CurrGridState.Empty();
for (const FGridCellData& Cell : CurrGridLayout->GridCells)
{
// 복사본 생성
CurrGridState.Add(Cell.Pos, Cell);
}
}
void UpdateCellState(const FIntPoint& Pos, EGridCellState NewState)
{
// 복사본 수정
if (CurrGridState.Contains(Pos))
{
CurrGridState[Pos].State = NewState;
}
}
이 방식으로 바꾸니 직업 변경할 때 그리드 상태 깔끔하게 초기화하고 관리할 수 있게 됨
처음에는 스탯을 단순 TMap<FName, float>으로 관리했는데, 팀원이랑 상의 후에 FCharacterStats 구조체로 변경했음
// 이전 방식
TMap<FName, float> AppliedStatEffects;
// 변경된 방식
USTRUCT(Atomic, BlueprintType)
struct FCharacterStats
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float MaxHP = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float ATK = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float DEF = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float AGL = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stats")
float ATS = 0.0f;
};
// 사용
FCharacterStats AppliedStatEffects;
이렇게 바꾸면 UI에서도 스탯 명확하게 표시할 수 있을 듯
SaveGame 클래스를 이용해 플레이어의 아케인 보드 시스템을 저장하기 위해서 클래스를 생성하려는데, SaveGame을 상속받은 LocalPlayerSaveGame을 발견함
두 개의 차이는
언리얼 공식 문서 - LocalPlayerSaveGame
룬 시스템은 플레이어별로 관리해야 하니까 LocalPlayerSaveGame이 더 적합하다고 판단함.
if(객체) 사용이 적합한 경우:void AMyActor::ProcessItem(UItem* Item)
{
if (Item) // 여기서는 괜찮음 (함수 매개변수)
{
Item->Use();
}
}
IsValid(객체) 사용이 더 적합한 경우:void AMyActor::OnTimerEnd()
{
// 타이머 콜백에서는 IsValid 사용 권장
if (IsValid(TargetActor))
{
TargetActor->Activate();
}
}