무한성이라고 불리던 스테이지를 일찍 탈출한 6명이서 사이드 프로젝트를 진행하기로 했다.
순전히 기능 공부를 위함이고 출시계획같은건 당연하게도 없다.
주제는 3D 뱀파이어 서바이벌이다.
개발 진행상황은 레포지토리 참조
MVP 스팩을 크게 6가지로 나눴다.
스킬 / 캐릭터 / 적 / 시스템(웨이브, 스포너) / 아이템 / UI
나는 그 중 시스템(웨이브, 스포너)를 담당하게 됐다.
마침 해당 사이드 프로젝트를 진행하기 전 EnemySpawner를 만들어봤었다.
해당 코드 첨부
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EnemySpawner.generated.h"
class ACharacter;
UCLASS()
class METAL3D_API AEnemySpawner : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AEnemySpawner();
protected:
// Called when the game starts or when spawned
virtual void BeginPlay() override;
public:
UFUNCTION(BlueprintCallable, Category="Spawner")
void StartSpawning();
UFUNCTION(BlueprintCallable, Category="Spawner")
void StopSpawning();
private:
UPROPERTY(EditAnywhere, Category="Spawner")
TSubclassOf<ACharacter> EnemyClass;
UPROPERTY(EditAnywhere, Category="Spawner")
float SpawnRadius = 1200.f;
UPROPERTY(EditAnywhere, Category="Spawner")
float SpawnInterval = 3.f;
UPROPERTY(EditAnywhere, Category="Spawner")
int32 MaxEnemyCount = 100;
UPROPERTY(EditAnywhere, Category="Spawner")
bool bAutoStart = true;
UPROPERTY()
TArray<TObjectPtr<AActor>> SpawnedEnemies;
FTimerHandle SpawnTimerHandler;
void SpawnEnemy();
bool FindSpawnLocation(FVector& OutLocation) const;
void CleanupInvalidEnemies();
};
// Fill out your copyright notice in the Description page of Project Settings.
#include "Spawner/EnemySpawner.h"
#include "NavigationSystem.h"
#include "GameFramework/Character.h"
#include "TimerManager.h"
#include "Components/CapsuleComponent.h"
#include "Engine/World.h"
// Sets default values
AEnemySpawner::AEnemySpawner()
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}
// Called when the game starts or when spawned
void AEnemySpawner::BeginPlay()
{
Super::BeginPlay();
if (bAutoStart)
{
StartSpawning();
}
}
void AEnemySpawner::StartSpawning()
{
if (!GetWorld())
{
return;
}
GetWorld()->GetTimerManager().SetTimer(
SpawnTimerHandler,
this,
&AEnemySpawner::SpawnEnemy,
SpawnInterval,
true,
0.f
);
}
void AEnemySpawner::StopSpawning()
{
if (!GetWorld())
{
return;
}
GetWorld()->GetTimerManager().ClearTimer(SpawnTimerHandler);
}
void AEnemySpawner::SpawnEnemy()
{
if (!EnemyClass)
{
UE_LOG(LogTemp, Warning, TEXT("Enemy Spawner: EnemyClass not set"));
return;
}
CleanupInvalidEnemies();
if (SpawnedEnemies.Num() >= MaxEnemyCount)
{
return;
}
FVector SpawnLocation;
if (!FindSpawnLocation(SpawnLocation))
{
UE_LOG(LogTemp, Warning, TEXT("Enemy Spawner: Failed to find spawn location"));
return;
}
ACharacter* DefaultEnemy = EnemyClass->GetDefaultObject<ACharacter>();
if (!DefaultEnemy || !DefaultEnemy->GetCapsuleComponent())
{
return;
}
const float CapsuleHalfHeight = DefaultEnemy->GetCapsuleComponent()->GetScaledCapsuleHalfHeight();
SpawnLocation.Z += CapsuleHalfHeight;
const FRotator SpawnRotation = FRotator::ZeroRotator;
FActorSpawnParameters SpawnParams;
SpawnParams.Owner = this;
SpawnParams.SpawnCollisionHandlingOverride =
ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButDontSpawnIfColliding;
ACharacter* SpawnedEnemy = GetWorld()->SpawnActor<ACharacter>(
EnemyClass,
SpawnLocation,
SpawnRotation,
SpawnParams
);
if (SpawnedEnemy)
{
SpawnedEnemies.Add(SpawnedEnemy);
}
}
bool AEnemySpawner::FindSpawnLocation(FVector& OutLocation) const
{
UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());
if (!NavSystem)
{
return false;
}
FNavLocation NavLocation;
const bool bFound = NavSystem->GetRandomReachablePointInRadius(
GetActorLocation(),
SpawnRadius,
NavLocation
);
if (!bFound)
{
return false;
}
OutLocation = NavLocation.Location;
return true;
}
void AEnemySpawner::CleanupInvalidEnemies()
{
SpawnedEnemies.RemoveAll([](const TObjectPtr<AActor>& Enemy)
{
return !IsValid(Enemy);
});
}
해당 코드를 복붙 후 수정.. 하고싶지만 우선은 Unreal c++에 익숙해지는게 먼저기 때문에 다시 손으로 따라쳐볼 예정이다. 거기에 달라지는 점들이 몇가지 있어서 추가 수정을 붙일 예정이다.