✅ 일일 목표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을 쓰는 이유가 없어짐.