Single FPS Project(2)

정혜창·2025년 2월 19일
0

내일배움캠프

목록 보기
31/41

✅ 일일 목표1 : Player, AI SpawnProcess & Stage(Level) System Implement
✅ 일일 목표2 : GameMode Prototype 대략적으로 완성

🎮 AI SpawnProcess

기획단계에서 Enemy 스폰의 경우 스테이지가 높아질수록 적이 생성되는 수가 많아지도록 설계를 하였다. 최적화를 위해 ObjectPooling을 이용해 적의 개체를 관리.

📌 AIObjectPool.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AIObjectPool.generated.h"

class ABaseEnemy;
class ASpawnVolume;

UCLASS()
class GUNFIREPARAGON_API AAIObjectPool : public AActor
{
	GENERATED_BODY()
	
public:	
	
	AAIObjectPool();

	void InitializePool(int32 PoolSize, TSubclassOf<ABaseEnemy> EnemyClass);
	ABaseEnemy* GetPooledAI(ASpawnVolume* SpawnVolume);
	void ReturnAIToPool(ABaseEnemy* Enemy);

	UPROPERTY()
	TArray<ABaseEnemy*> PooledEnemies;
	UPROPERTY()
	TSubclassOf<ABaseEnemy> EnemyBP;
};
📌 AIObjectPool.cpp
#include "AIObjectPool.h"
#include "SpawnVolume.h"
#include "../BaseEnemy.h" 


AAIObjectPool::AAIObjectPool()
{
	PrimaryActorTick.bCanEverTick = false;

}

void AAIObjectPool::InitializePool(int32 PoolSize, TSubclassOf<ABaseEnemy> EnemyClass)
{
	EnemyBP = EnemyClass;

	for (int32 i = 0; i < PoolSize; i++)
	{
		ABaseEnemy* NewEnemy = GetWorld()->SpawnActor<ABaseEnemy>(
			EnemyBP,
			FVector::ZeroVector,
			FRotator::ZeroRotator);

		// 미리 Spawn, 스크럼때 얘기했던 Object Pooling 방식
		if (NewEnemy)
		{
			NewEnemy->SetActorHiddenInGame(true);
			NewEnemy->SetActorEnableCollision(false);
			NewEnemy->SetActorTickEnabled(false);
			PooledEnemies.Add(NewEnemy);
		}
	}
}

ABaseEnemy* AAIObjectPool::GetPooledAI(ASpawnVolume* SpawnVolume)
{
	if (!SpawnVolume) return nullptr;

	for (ABaseEnemy* Enemy : PooledEnemies)
	{
		if (!Enemy->IsActorTickEnabled())
		{
			FVector SpawnLocation = SpawnVolume->GetSafeSpawnPoint();
			Enemy->SetActorLocation(SpawnLocation);
			Enemy->SetActorHiddenInGame(false);
			Enemy->SetActorEnableCollision(true);
			Enemy->SetActorTickEnabled(true);
			return Enemy;
		}
	}
	return nullptr;
}

void AAIObjectPool::ReturnAIToPool(ABaseEnemy* Enemy)
{
	if (Enemy)
	{
		Enemy->SetActorHiddenInGame(true);
		Enemy->SetActorEnableCollision(false);
		Enemy->SetActorTickEnabled(false);
	}
}
  • #include "../BaseEnemy.h" 에서 ../ 는 이전경로를 말하는 것이다. GitHub를 이용해서 협업을 하면서 서로의 폴더구조가 다를 때가 가끔 있기 때문에 기억을 해두는 것이 좋을듯

  • SetActorHiddenInGame 은 게임에서 액터를 보이지않게 숨기겠냐는 것이다. 스폰은 되었지만 실질적으로 없는 상태. 미리 스폰을 해두는 것이다. 최적화를 위한 기법.

    • 물리적인 영향이 있으면 안되므로 SetActorEnableCollision을 false로 설정하고 Enemy의 Tick함수 또한 false설정하는 것.
  • 반대로 준비해둔 액터를 보이고, 실체화하는 것은 위의 과정과 반대로 하는 것을 알 수 있다.

  • 레벨이 바뀌더라도 재사용되어야 하기 때문에 차후 GameInstance에서 AI를 관리하는 로직을 생각해봐야 한다.


🎮 StageSystem

총 10개의 Stage가 있고 모든 적을 처치하면 보상을 얻은 뒤 다음 스테이지로 넘어가는 것으로 기획하였다. 아직 보상시스템은 구현이 안되어 있으므로 모든 적을 처치하면 다음 레벨(스테이지)로 넘어가는 형식으로 로직을 구현해보았다.

📌 GameMode.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameMode.h"
#include "FPSGameMode.generated.h"


UCLASS()
class GUNFIREPARAGON_API AFPSGameMode : public AGameMode
{
	GENERATED_BODY()

public:
	
	AFPSGameMode();
	virtual void BeginPlay() override;
	void ClearAllEnemies();

	UFUNCTION()
	void OnBossDefeated();
	UFUNCTION()
	void OnPlayerDead();
	UFUNCTION()
	void OnStageClear();
	UFUNCTION()
	void SpawnEnemies(int32 NumEnemies);
	UFUNCTION()
	void EndGame(bool bPlayWin);
	UFUNCTION()
	void ReturnToMainMenu();
};
📌 GameMode.cpp
#include "FPSGameMode.h"
#include "FPSGameState.h"
#include "FPSGameInstance.h"
#include "Kismet/GameplayStatics.h"
#include "../BaseEnemy.h"
#include "AIObjectPool.h"
#include "SpawnVolume.h"

AFPSGameMode::AFPSGameMode()
{
	// DefaultPawnClass = APlayerCharacter::StaticClass();
	// PlayerControllerClass = AMyPlayerController::StaticClass();
	GameStateClass = AFPSGameState::StaticClass();
}

void AFPSGameMode::BeginPlay()
{
	Super::BeginPlay();
	
	// HUD 추가 로직
	/*
	AMyPlayerController* PlayerController = Cast<AmyPlayerController>(UGameplayStatics::GetPlayerController(this, 0));
	if (PlayerController)
	{
		PlayerController->ShowHUD();
	}
	*/
}

void AFPSGameMode::OnBossDefeated()
{
	EndGame(true);
}

void AFPSGameMode::OnPlayerDead()
{
	EndGame(false);
}

void AFPSGameMode::OnStageClear()
{
	ClearAllEnemies();

	// 상자를 열면 다음스테이지 가던지, 어디에 도착하면 다음 스테이지 가던지 로직 구현해야함

	if (UGameInstance* GameInstance = GetGameInstance())
	{
		if (UFPSGameInstance* FPSGameInstance = Cast<UFPSGameInstance>(GameInstance))
		{
			if (FPSGameInstance)
			{
				FPSGameInstance->LoadNextStage();

				int32 NewStageIndex = FPSGameInstance->CurrentStageIndex;
				int32 NumEnemies = NewStageIndex * 5;

				// 바로 스폰되면 어색할 수 있으니 보고 SetTimer 추가할지 생각
				SpawnEnemies(NumEnemies);
			}
		}
	}
}

void AFPSGameMode::SpawnEnemies(int32 NumEnemies)
{
	UWorld* World = GetWorld();
	if (!World) return;

	AFPSGameState* FPSGameState = Cast<AFPSGameState>(World->GetGameState());
	if (!FPSGameState) return;

	AAIObjectPool* AIObjectPool = Cast<AAIObjectPool>(UGameplayStatics::GetActorOfClass(World, AAIObjectPool::StaticClass()));
	if (AIObjectPool) return;

	TArray<AActor*> FoundVolumes;

	UGameplayStatics::GetAllActorsOfClass(World, ASpawnVolume::StaticClass(), FoundVolumes);
	if (FoundVolumes.Num() > 0)
	{
		ASpawnVolume* SpawnVolume = Cast<ASpawnVolume>(FoundVolumes[0]);
		if (SpawnVolume)
		{
			for (int32 i = 0; i < NumEnemies; i++)
			{
				ABaseEnemy* SpawnedEnemy = AIObjectPool->GetPooledAI(SpawnVolume);
				if (SpawnedEnemy)
				{
					FPSGameState->RemainingEnemies++;
				}
			}
		}	
	}
}

void AFPSGameMode::ClearAllEnemies()
{
	UWorld* World = GetWorld();
	if (!World) return;

	TArray<AActor*> Enemies;
	UGameplayStatics::GetAllActorsOfClass(World, ABaseEnemy::StaticClass(), Enemies);

	for (AActor* Enemy : Enemies)
	{
		if (Enemy)
		{
			Enemy->Destroy();
		}
	}
}

void AFPSGameMode::EndGame(bool bPlayWin)
{
	if (bPlayWin)
	{
		UE_LOG(LogTemp, Warning, TEXT("Game Clear!"));
		// 게임 클리어 UI 호출하는 로직 추가해야됌.
		FTimerHandle EndTimerHandle;
		GetWorldTimerManager().SetTimer(
			EndTimerHandle,
			this,
			&AFPSGameMode::ReturnToMainMenu,
			5.0f,
			false
		);
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Game Over!"));
		// 게임 오버 UI 호출하는 로직 추가해야됌.
		FTimerHandle EndTimerHandle;
		GetWorldTimerManager().SetTimer(
			EndTimerHandle,
			this,
			&AFPSGameMode::ReturnToMainMenu,
			5.0f,
			false
		);
	}
}

void AFPSGameMode::ReturnToMainMenu()
{
	if (UGameInstance* GameInstance = GetGameInstance())
	{
		if (UFPSGameInstance* FPSGameInstance = Cast<UFPSGameInstance>(GameInstance))
		{
			if (FPSGameInstance)
			{
				FPSGameInstance->GotoMainMenu();
			}
		}
	}	
}
  • GameMode에서는 스폰처리, 게임이 클리어 되는 조건과 보스처치, 플레이어 사망시 처리 등을 구현하였다.

  • 모든 Stage마다 레벨이 바뀌다보니 GameState보다는 GameInstance과 더 긴밀하게 연결되었다.

📌 GameInstance.h
#pragma once

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


UCLASS()
class GUNFIREPARAGON_API UFPSGameInstance : public UGameInstance
{
	GENERATED_BODY()
	
public:
	UFPSGameInstance();
	virtual void Init() override;
	
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Player Status")
	int32 PlayerLevel;
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Player Status")
	float ExperiencePoints;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Player Status")
	float PlayerHealth;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Stage")
	int32 CurrentStageIndex;
	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Stage")
	TArray<FName> StageMapNames;

	UFUNCTION(BlueprintCallable)
	void StartGame();
	UFUNCTION(BlueprintCallable)
	void LoadNextStage();
	UFUNCTION(BlueprintCallable)
	void GotoMainMenu();




	// UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "PassiveInventory")
	// TArray<FPassiveInventory> PlayerPassiveInventory;

	// void SavePlayerStats(ACharacter* PlayerCharacter);
	// void LoadPlayerStats(ACharacter* PlayerCharacter);
	// void AddPassive(FPassiveInventory NewPassive);
	// void RemovePassive(FString PassiveName)
};
  • 주석처리는 추후 Passive 시스템을 구현하기 위해 적은 것
📌 GameInstance.cpp
#include "FPSGameInstance.h"
#include "Kismet/GameplayStatics.h"

UFPSGameInstance::UFPSGameInstance()
{
	PlayerLevel = 1;
	ExperiencePoints = 0.f;
	PlayerHealth = 1000.0f;
	CurrentStageIndex = 0; // 0 이 메인메뉴, 1 ~ 10 Stage 
}

void UFPSGameInstance::Init()
{
	Super::Init();

	if (CurrentStageIndex == 0)
	{
		if (StageMapNames.IsValidIndex(CurrentStageIndex))
		{
			UGameplayStatics::OpenLevel(this, StageMapNames[CurrentStageIndex]);
		}
	}
}

// LoadNextStage가 있어서 없어도 될 것 같긴한데 추후 유용할 수 있으니 남겨둠
void UFPSGameInstance::StartGame()
{
	CurrentStageIndex = 1;
	if (StageMapNames.IsValidIndex(CurrentStageIndex))
	{
		UGameplayStatics::OpenLevel(this, StageMapNames[CurrentStageIndex]);
	}
}

void UFPSGameInstance::LoadNextStage()
{
	CurrentStageIndex++;

	if (CurrentStageIndex > 10)
	{
		GotoMainMenu();
		return;
	}

	if (StageMapNames.IsValidIndex(CurrentStageIndex))
	{
		UGameplayStatics::OpenLevel(this, StageMapNames[CurrentStageIndex]);
	}
}

void UFPSGameInstance::GotoMainMenu()
{
	CurrentStageIndex = 0;
	if (StageMapNames.IsValidIndex(CurrentStageIndex))
	{
		UGameplayStatics::OpenLevel(this, StageMapNames[CurrentStageIndex]);
	}
}
  • CurrentStageIndex는 level이 바뀌더라도 변하지 않아야되는 변수이므로 Instance에서 관리를 해주었다. 그러다 보니 자연스럽게 Stage관리를 GameInstance에서 하게 되었다.

  • 앞서 말한 ObjectPool을 Instance에서 적 객체들을 관리하는 로직을 생각해야한다. 그렇지 않으면 레벨이 바뀔 때 마다 Enemy 객체가 사라졌다가 생성되므로 ObjectPool을 쓰는 이유가 없어짐.

profile
Unreal 1기

0개의 댓글

관련 채용 정보