✅ 일일 목표1 : Player, AI SpawnProcess & Stage(Level) System Implement
✅ 일일 목표2 : GameMode Prototype 대략적으로 완성
기획단계에서 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를 관리하는 로직을 생각해봐야 한다.
총 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)
};
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을 쓰는 이유가 없어짐.