TIL: Unreal C++ 사이드프로젝트 32일차

박춘팔·6일 전

언리얼 TIL

목록 보기
31/34

누적 학습 시간 : 320시간 34분

📅 2026-05-18

무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.

프로젝트 진행기간 : 26.05.11 ~ 26.05.21

주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 레포지토리 참조
-> 일신상의 이유로 Perforce로 변경

MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.

Github -> Perforce 마이그레이션

계획 중 하나였던 9기의 모든 프로젝트를 하나의 Organization으로 관리하고 각 다른 팀의 PR과 코드를 볼 수 있게 하고싶었는데 LFS 대역폭이 너무 적어서 어떤 대체제가 있을지 찾던 중
Perforce 라는 걸 알게됐다.

Perforce는 Git과 비슷한 형상관리툴인데 다른점은 Github같이 플랫폼이 있어서 거기서 관리하는게 아니라 개인 서버 혹은 클라우드 저장소를 연결해서 사용한다.

Github

장점

  • Branch 전략과 Workflow를 통해 코드 diff와 수정, 배포, 버저닝이 매우 간편
  • 상대적으로 대중적이라(Perforce에 비해) 적응하기 쉬움

단점

  • 언리얼에셋(uasset, umap 등) 수정 시 diff 감지를 못하고 최후에 merge된 파일로 덮어씀
  • 자체 플랫폼의 클라우드 이용하기 때문에 결제 안하면 lfs 대역폭등 제한 사항이 많음

Perforce

장점

  • 바이너리 그대로 업로드 가능
  • 바이너리 에셋 수정 시 checkin/out 기능 제공
    checkin시 Perfoce에서 파일을 Readonly로 바꿔서 작업하는 동안 다른 사람의 수정을 막을 수 있음 (기존 Github는 바이너리 에셋의 수정사항을 인식하지 못하고 가장 나중에 merge된 파일을 기준으로 덮어씀)
  • Get Latest(pull) / Submit(push) 속도가 매우 빠름

단점

  • 클라우드 서버 설정에 대한 진입장벽
    남는 pc나 노트북이 있다면 하루종일 켜놔야하고 서버 초기 설정에 대한 진입장벽 있음
    난 웹개발자였어서 쉽게 했지만 다른 사람들은... 잘 모르겠음
  • 사용자 경험이 좋지않음
    UI가 상상 이상으로 좋지않음 아래 이미지를 첨부할테지만 정말 각오해야함
  • Git처럼 branch를 통해서 세세하게 관리하는 것이 아니라 이게 정말 관리가 될까? 싶음
    이 또한 UI/UX가 좋지않아서 그렇다고 생각함.

Perforce 사용법에 대해서는 나중에 다시한번 작성할 예정

Gamemode 승리조건 추가

#include "Gamemode/VampireSurvivalGamemode.h"

#include "EngineUtils.h"
#include "DataAsset/RunConfigDataAsset.h"
#include "DataAsset/WaveDataAsset.h"
#include "Kismet/GameplayStatics.h"
#include "Spawner/EnemySpawner.h"

AVampireSurvivalGamemode::AVampireSurvivalGamemode()
{
}

void AVampireSurvivalGamemode::BeginPlay()
{
	Super::BeginPlay();
	AWorldSettings* WorldSettings = GetWorldSettings();
	
	if (bUseDebugTimeAcceleration)
	{
		WorldSettings->SetTimeDilation(30.0f);
	}
	
	for (TActorIterator<AEnemySpawner> It(GetWorld()); It; ++It)
	{
		EnemySpawner = *It;
		break;
	}

	StartRun();
}

void AVampireSurvivalGamemode::StartRun()
{
	if (!RunConfig)
	{
		UE_LOG(LogTemp, Warning, TEXT("RunConfig is not set."));
		return;
	}

	//Debug, Production duration set 및 판단
	RunDurationSeconds = bUseDebugDuration
		                     ? RunConfig->DebugDurationSeconds
		                     : RunConfig->ProductionDurationSeconds;

	ElapsedTime = 0.f;
	CurrentWave = nullptr;
	CurrentWaveIndex = INDEX_NONE;

	SetCurrentWave(FindWaveForElapsedTime(ElapsedTime));

	GetWorldTimerManager().SetTimer(
		RunTimerHandler,
		this,
		&AVampireSurvivalGamemode::UpdateRunTime,
		1.f,
		true
	);
}

void AVampireSurvivalGamemode::UpdateRunTime()
{
	ElapsedTime += 1.f;
	OnRunElapsedTimeChanged.Broadcast(ElapsedTime);

	if (ElapsedTime >= RunDurationSeconds)
	{
		EndRun();
		return;
	}

	SetCurrentWave(FindWaveForElapsedTime(ElapsedTime));
}

void AVampireSurvivalGamemode::EndRun()
{
	GetWorldTimerManager().ClearTimer(RunTimerHandler);

	if (EnemySpawner)
	{
		EnemySpawner->StopSpawning();
	}
	
	// 게임 종료
	SetGameCompletedState();

	UE_LOG(LogTemp, Warning, TEXT("Run ended."));
}

UWaveDataAsset* AVampireSurvivalGamemode::FindWaveForElapsedTime(float InElapsedTime) const
{
	if (!RunConfig)
	{
		return nullptr;
	}

	for (UWaveDataAsset* Wave : RunConfig->Waves)
	{
		if (!Wave)
		{
			continue;
		}

		if (InElapsedTime >= Wave->StartTime && InElapsedTime < Wave->EndTime)
		{
			return Wave;
		}
	}

	return nullptr;
}

void AVampireSurvivalGamemode::SetCurrentWave(UWaveDataAsset* NewWave)
{
	if (CurrentWave == NewWave)
	{
		return;
	}

	CurrentWave = NewWave;

	if (!CurrentWave)
	{
		return;
	}

	CurrentWaveIndex = CurrentWave->WaveIndex;

	UE_LOG(LogTemp, Warning, TEXT("Wave changed: %d"), CurrentWaveIndex);

	if (EnemySpawner)
	{
		EnemySpawner->SetElapsedTime(ElapsedTime);
		EnemySpawner->SetWaveData(CurrentWave);
		EnemySpawner->StartSpawning();
	}
}

void AVampireSurvivalGamemode::SetGameCompletedState()
{
	AWorldSettings* WorldSettings = GetWorldSettings();
	
	if (!bIsGameCompleted && ElapsedTime >= 1800.f)
	{
		bIsGameCompleted = true;
		
		OnIsGameCompletedState.Broadcast(bIsGameCompleted);
		UGameplayStatics::SetGamePaused(GetWorld(), true);
		
	}
}
profile
이것 저것 다해보는 삶

0개의 댓글