12. 외곽선 연출 2 - Elysia-ff/UE4-Custom_Stencil_Tutorial GitHub Wiki
적캐릭터가 시야에 들어올때만 외곽선을 표시해야 한다.
ULosComponent
에 시야각, 거리 등의 정보가 있으므로 이를 이용해서 시야 범위 내에 존재하는지 확인하고
LineTrace로 ULosComponent
의 위치부터 적캐릭터의 얼굴까지 중간에 가로막고 있는 장애물이 없는지 한번 더 확인하면 된다.
ULosComponent
를 범용적으로 사용하기 위해 시야에 들어올 수 있는 Actor는 ISpottable
Interface를 상속받도록 하겠다.
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class USpottable : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
class STEALTHTUTORIAL_API ISpottable
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
virtual FVector GetSpotPointLocation() const = 0;
};
Enemy.h
를 다음과 같이 수정한다.
class STEALTHTUTORIAL_API AEnemy : public AStealthCharacter, public ISpottable
// 생략
public:
virtual FVector GetSpotPointLocation() const override;
public:
UPROPERTY(VisibleAnywhere, Category = "Line Of Sight")
USceneComponent* SpotPoint;
Enemy.cpp
를 아래와 같이 수정하고
AEnemy::AEnemy(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// 생략
SpotPoint = CreateDefaultSubobject<USceneComponent>(TEXT("SpotPoint"));
SpotPoint->SetupAttachment(RootComponent);
}
FVector AEnemy::GetSpotPointLocation() const
{
return SpotPoint->GetComponentLocation();
}
BP_Enemy
에서 SpotPoint
의 위치를 메시의 머리쪽으로 조정한다.
마찬가지로 BP_Hitman
의 ULosComponent
의 위치도 머리쪽으로 조정한다.
이제 LosComponent.h
와 LosComponent.cpp
를 다음과 같이 수정한다.
class ISpottable;
public:
void RegisterSpottableObject(const TScriptInterface<ISpottable>& NewInterface);
private:
void FindActorsInSight();
bool IsInSight(const TScriptInterface<ISpottable>& Target) const;
public:
UPROPERTY(EditAnywhere, Category = "Line Of Sight")
float SpotInterval;
private:
UPROPERTY()
TArray<TScriptInterface<ISpottable>> SpotCandidates;
UPROPERTY()
TArray<TScriptInterface<ISpottable>> SpottedList;
FTimerHandle SpotTimer;
#include "LineOfSight/SpottableInterface.h"
void ULosComponent::BeginPlay()
{
// 생략
if (SpotInterval > 0.0f)
{
GetWorld()->GetTimerManager().SetTimer(SpotTimer, this, &ULosComponent::FindActorsInSight, SpotInterval, true);
}
}
void ULosComponent::RegisterSpottableObject(const TScriptInterface<ISpottable>& NewInterface)
{
SpotCandidates.Add(NewInterface);
}
void ULosComponent::FindActorsInSight()
{
SpottedList.Reset();
for (int32 i = 0; i < SpotCandidates.Num(); i++)
{
if (SpotCandidates[i] && IsInSight(SpotCandidates[i]))
{
SpottedList.Add(SpotCandidates[i]);
DrawDebugLine(GetWorld(), GetComponentLocation(), SpotCandidates[i]->GetSpotPointLocation(), FColor::Red, false, SpotInterval, (uint8)'\000', 5.0f);
}
}
}
bool ULosComponent::IsInSight(const TScriptInterface<ISpottable>& Target) const
{
FVector ComponentLocation = GetComponentLocation();
FVector TargetLocation = Target->GetSpotPointLocation();
TargetLocation.Z = ComponentLocation.Z;
FVector Dir = (TargetLocation - ComponentLocation);
float SqrDistanceToTarget = Dir.SizeSquared();
if (SqrDistanceToTarget > FarDistance * FarDistance)
{
return false;
}
bool bInSight = false;
if (SqrDistanceToTarget <= NearDistance * NearDistance)
{
bInSight = true;
}
else
{
float AngleInDegrees = FMath::RadiansToDegrees(FMath::Acos(FVector::DotProduct(GetForwardVector(), Dir.GetSafeNormal())));
float HalfFarSightAngle = FarSightAngle * 0.5f;
bInSight = (-HalfFarSightAngle <= AngleInDegrees && AngleInDegrees <= HalfFarSightAngle);
}
if (bInSight)
{
check(GetOwner());
FHitResult HitResult;
FCollisionQueryParams Param;
Param.AddIgnoredActor(GetOwner());
if (GetWorld()->LineTraceSingleByChannel(HitResult, ComponentLocation, Target->GetSpotPointLocation(), ECC_Camera, Param))
{
if (HitResult.Actor.IsValid() && HitResult.Actor == Target.GetObject())
{
return true;
}
}
}
return false;
}
SpotInterval
마다 SpotCandidates
에 등록된 Actor들이 시야내에 있으면 SpottedList
에 추가하고 디버그용 선을 표시한다.
이제 BP_Enemy
가 생성될때 RegisterSpottableObject(const TScriptInterface<ISpottable>& NewInterface)
를 호출해주기만 하면 된다.
Actor가 삭제되는 경우 UnRegister
함수를 구현하거나 FindActorsInSight
에서 nullptr가 대입된 SpotCandidates[i]
를 검사하면 되지만 여기서는 Actor가 삭제되는 일은 없으니 신경쓰지 않는다.
먼저 AStealthPlayerController
에서 생성된 AHitman
을 반환하는 함수를 추가한다.
public:
AHitman* GetHitman() const;
AHitman* AStealthPlayerController::GetHitman() const
{
check(Hitman);
return Hitman;
}
EnemySpawnManager.cpp
의 SpawnEnemy
하단에 다음 코드를 추가한다.
#include <Engine/Classes/Kismet/GameplayStatics.h>
#include "Character/Hitman.h"
#include "LineOfSight/LosComponent.h"
#include "Player/StealthPlayerController.h"
void AEnemySpawnManager::SpawnEnemy(const FTransform& SpawnTransform)
{
// 생략
AStealthPlayerController* Controller = Cast<AStealthPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
if (Controller)
{
AHitman* Hitman = Controller->GetHitman();
if (Hitman)
{
Hitman->LosComponent->RegisterSpottableObject(TScriptInterface<ISpottable>(NewEnemy));
}
}
}
이 함수가 제대로 실행되기 위해선 함수 호출 이전에 Hitman
의 생성이 보장되어 있어야 한다.
그러므로 AStealthPlayerController
에서 Hitman
생성 한 다음 적을 생성하도록 변경하겠다.
EnemySpawnManager.h
와 EnemySpawnManager.cpp
를 다음과 같이 수정한다.
public:
void BeginSpawn();
//protected:
// // Called when the game starts or when spawned
// virtual void BeginPlay() override;
//void AEnemySpawnManager::BeginPlay()
void AEnemySpawnManager::BeginSpawn()
{
// Super::BeginPlay();
Enemies.Reserve(SpawnPoses.Num());
for (int32 i = 0; i < SpawnPoses.Num(); i++)
{
if (!SpawnPoses[i].IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("Invalid spawn position : %d"), i);
continue;
}
SpawnEnemy(SpawnPoses[i]->GetTransform());
}
}
이제 StealthPlayerConroller.cpp
의 BeginPlay
하단에 아래 코드를 추가한다.
#include <Engine/Public/EngineUtils.h>
#include "SpawnManager/EnemySpawnManager.h"
void AStealthPlayerController::BeginPlay()
{
// 생략
TActorIterator<AEnemySpawnManager> Iterator(GetWorld());
if (Iterator)
{
Iterator->BeginSpawn();
}
}
BP_Hitman
-> LosComponent
의 SpotInterval
에 적절한 값을 설정하고 플레이하면 Hitman
과 시야에 들어온 적캐릭터 사이에 빨간 선이 표시된다.
/LineOfSight/SpottableInterface.h
// Copyright 2021. Elysia-ff
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "SpottableInterface.generated.h"
// This class does not need to be modified.
UINTERFACE(MinimalAPI)
class USpottable : public UInterface
{
GENERATED_BODY()
};
/**
*
*/
class STEALTHTUTORIAL_API ISpottable
{
GENERATED_BODY()
// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
virtual FVector GetSpotPointLocation() const = 0;
};
/Character/Enemy.h
// Copyright 2021. Elysia-ff
#pragma once
#include "LineOfSight/SpottableInterface.h"
#include "CoreMinimal.h"
#include "Character/StealthCharacter.h"
#include "Enemy.generated.h"
/**
*
*/
UCLASS()
class STEALTHTUTORIAL_API AEnemy : public AStealthCharacter, public ISpottable
{
GENERATED_BODY()
public:
AEnemy(const FObjectInitializer& ObjectInitializer);
virtual FVector GetSpotPointLocation() const override;
public:
UPROPERTY(VisibleAnywhere, Category = "Line Of Sight")
USceneComponent* SpotPoint;
};
/Character/Enemy.cpp
// Copyright 2021. Elysia-ff
#include "Enemy.h"
#include "Character/Controller/EnemyAIController.h"
AEnemy::AEnemy(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryActorTick.bCanEverTick = false;
AIControllerClass = AEnemyAIController::StaticClass();
SpotPoint = CreateDefaultSubobject<USceneComponent>(TEXT("SpotPoint"));
SpotPoint->SetupAttachment(RootComponent);
}
FVector AEnemy::GetSpotPointLocation() const
{
return SpotPoint->GetComponentLocation();
}
/LineOfSight/LosComponent.h
// Copyright 2021. Elysia-ff
#pragma once
#include "LineOfSight/ViewCastInfo.hpp"
#include "CoreMinimal.h"
#include "ProceduralMeshComponent.h"
#include "LosComponent.generated.h"
class ISpottable;
/**
*
*/
UCLASS()
class STEALTHTUTORIAL_API ULosComponent : public UProceduralMeshComponent
{
GENERATED_BODY()
public:
ULosComponent(const FObjectInitializer& ObjectInitializer);
virtual void BeginPlay() override;
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
void RegisterSpottableObject(const TScriptInterface<ISpottable>& NewInterface);
private:
void DrawMesh();
void AddLineOfSightPoints();
void AddNearSightPoints();
FVector GetDirection(float AngleInDegrees) const;
FViewCastInfo ViewCast(float AngleInDegrees, float Distance) const;
void FindEdge(FViewCastInfo ViewCastA, FViewCastInfo ViewCastB, float Distance);
void FindActorsInSight();
bool IsInSight(const TScriptInterface<ISpottable>& Target) const;
public:
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Debug")
UMaterialInterface* DebugMaterial;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Debug")
bool bDrawDebugLine;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Far", meta = (UIMin = 0, UIMax = 360))
int32 FarSightAngle = 0;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Far", meta = (UIMin = 2))
int32 FarViewCastCount = 2;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Far", meta = (UIMin = 0))
float FarDistance = 0.0f;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Near", meta = (UIMin = 2))
int32 NearViewCastCount = 2;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Near", meta = (UIMin = 0))
float NearDistance = 0.0f;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Edge")
float EdgeFindingThreshold = 0.01f;
UPROPERTY(EditAnywhere, Category = "Line Of Sight")
float SpotInterval;
private:
TArray<FVector> ViewPoints;
TArray<FVector> Vertices;
TArray<int32> Triangles;
TArray<FVector> Normals;
TArray<FVector2D> UV0;
TArray<FLinearColor> VertexColors;
TArray<FProcMeshTangent> Tangents;
UPROPERTY()
TArray<TScriptInterface<ISpottable>> SpotCandidates;
UPROPERTY()
TArray<TScriptInterface<ISpottable>> SpottedList;
FTimerHandle SpotTimer;
};
/LineOfSight/LosComponent.cpp
// Copyright 2021. Elysia-ff
#include "LosComponent.h"
#include <Engine/Public/DrawDebugHelpers.h>
#include "LineOfSight/SpottableInterface.h"
#include "StealthTypes.hpp"
ULosComponent::ULosComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryComponentTick.bCanEverTick = true;
SetCastShadow(false);
SetCollisionProfileName(TEXT("NoCollision"));
}
void ULosComponent::BeginPlay()
{
Super::BeginPlay();
if (DebugMaterial != nullptr)
{
SetMaterial(0, DebugMaterial);
}
if (SpotInterval > 0.0f)
{
GetWorld()->GetTimerManager().SetTimer(SpotTimer, this, &ULosComponent::FindActorsInSight, SpotInterval, true);
}
}
void ULosComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
DrawMesh();
}
void ULosComponent::RegisterSpottableObject(const TScriptInterface<ISpottable>& NewInterface)
{
SpotCandidates.Add(NewInterface);
}
void ULosComponent::DrawMesh()
{
ViewPoints.Reset();
AddLineOfSightPoints();
AddNearSightPoints();
Vertices.Reset(ViewPoints.Num() + 1);
Vertices.Add(FVector::ZeroVector);
Triangles.Reset((ViewPoints.Num() - 1) * 3);
int32 ViewPointsNum = ViewPoints.Num();
for (int32 i = 0; i < ViewPointsNum; i++)
{
if (bDrawDebugLine)
{
DrawDebugLine(GetWorld(), GetComponentLocation(), ViewPoints[i], FColor::Black, false, (-1.0f), (uint8)'\000', 1.0f);
}
Vertices.Add(GetComponentTransform().InverseTransformPosition(ViewPoints[i]));
if (i < ViewPointsNum - 1)
{
Triangles.Add(0);
Triangles.Add(i + 2);
Triangles.Add(i + 1);
}
}
CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UV0, VertexColors, Tangents, false);
}
void ULosComponent::AddLineOfSightPoints()
{
check(FarViewCastCount >= 2);
float StartAngle = -FarSightAngle * 0.5f;
float DeltaAngle = (float)FarSightAngle / (FarViewCastCount - 1);
FViewCastInfo OldViewCast;
for (int32 i = 0; i < FarViewCastCount; i++)
{
float Angle = StartAngle + (DeltaAngle * i);
FViewCastInfo ViewCastInfo = ViewCast(Angle, FarDistance);
if (i > 0 && OldViewCast != ViewCastInfo)
{
FindEdge(OldViewCast, ViewCastInfo, FarDistance);
}
ViewPoints.Add(ViewCastInfo.Point);
OldViewCast = ViewCastInfo;
}
}
void ULosComponent::AddNearSightPoints()
{
check(NearViewCastCount >= 2);
float NearSightAngle = 360.0f - FarSightAngle;
float StartAngle = FarSightAngle * 0.5f;
float DeltaAngle = NearSightAngle / (NearViewCastCount - 1);
FViewCastInfo OldViewCast;
for (int32 i = 0; i < NearViewCastCount; i++)
{
float Angle = StartAngle + (DeltaAngle * i);
FViewCastInfo ViewCastInfo = ViewCast(Angle, NearDistance);
if (i > 0 && OldViewCast != ViewCastInfo)
{
FindEdge(OldViewCast, ViewCastInfo, NearDistance);
}
ViewPoints.Add(ViewCastInfo.Point);
OldViewCast = ViewCastInfo;
}
}
FVector ULosComponent::GetDirection(float AngleInDegrees) const
{
FVector ForwardVector = GetForwardVector();
return ForwardVector.RotateAngleAxis(AngleInDegrees, GetUpVector());
}
FViewCastInfo ULosComponent::ViewCast(float AngleInDegrees, float Distance) const
{
FVector Dir = GetDirection(AngleInDegrees);
FVector LineStart = GetComponentLocation();
FVector LineEnd = LineStart + (Dir * Distance);
FHitResult HitResult;
if (GetWorld()->LineTraceSingleByChannel(HitResult, LineStart, LineEnd, ECC_ObstacleTrace))
{
return FViewCastInfo(HitResult.Actor, HitResult.ImpactPoint, AngleInDegrees, HitResult.ImpactNormal);
}
return FViewCastInfo(nullptr, LineEnd, AngleInDegrees, FVector_NetQuantizeNormal::ZeroVector);
}
void ULosComponent::FindEdge(FViewCastInfo ViewCastA, FViewCastInfo ViewCastB, float Distance)
{
check(EdgeFindingThreshold > 0.0f);
if (FMath::Abs(ViewCastA.AngleInDegrees - ViewCastB.AngleInDegrees) <= EdgeFindingThreshold)
{
ViewPoints.Add(ViewCastA.Point);
ViewPoints.Add(ViewCastB.Point);
return;
}
float Angle = (ViewCastA.AngleInDegrees + ViewCastB.AngleInDegrees) * 0.5f;
FViewCastInfo NewViewCast = ViewCast(Angle, Distance);
if (ViewCastA == NewViewCast)
{
FindEdge(NewViewCast, ViewCastB, Distance);
}
else if (ViewCastB == NewViewCast)
{
FindEdge(ViewCastA, NewViewCast, Distance);
}
else
{
FindEdge(ViewCastA, NewViewCast, Distance);
FindEdge(NewViewCast, ViewCastB, Distance);
}
}
void ULosComponent::FindActorsInSight()
{
SpottedList.Reset();
for (int32 i = 0; i < SpotCandidates.Num(); i++)
{
if (SpotCandidates[i] && IsInSight(SpotCandidates[i]))
{
SpottedList.Add(SpotCandidates[i]);
DrawDebugLine(GetWorld(), GetComponentLocation(), SpotCandidates[i]->GetSpotPointLocation(), FColor::Red, false, SpotInterval, (uint8)'\000', 5.0f);
}
}
}
bool ULosComponent::IsInSight(const TScriptInterface<ISpottable>& Target) const
{
FVector ComponentLocation = GetComponentLocation();
FVector TargetLocation = Target->GetSpotPointLocation();
TargetLocation.Z = ComponentLocation.Z;
FVector Dir = (TargetLocation - ComponentLocation);
float SqrDistanceToTarget = Dir.SizeSquared();
if (SqrDistanceToTarget > FarDistance * FarDistance)
{
return false;
}
bool bInSight = false;
if (SqrDistanceToTarget <= NearDistance * NearDistance)
{
bInSight = true;
}
else
{
float AngleInDegrees = FMath::RadiansToDegrees(FMath::Acos(FVector::DotProduct(GetForwardVector(), Dir.GetSafeNormal())));
float HalfFarSightAngle = FarSightAngle * 0.5f;
bInSight = (-HalfFarSightAngle <= AngleInDegrees && AngleInDegrees <= HalfFarSightAngle);
}
if (bInSight)
{
check(GetOwner());
FHitResult HitResult;
FCollisionQueryParams Param;
Param.AddIgnoredActor(GetOwner());
if (GetWorld()->LineTraceSingleByChannel(HitResult, ComponentLocation, Target->GetSpotPointLocation(), ECC_Camera, Param))
{
if (HitResult.Actor.IsValid() && HitResult.Actor == Target.GetObject())
{
return true;
}
}
}
return false;
}
/Player/StealthPlayerController.h
// Copyright 2021. Elysia-ff
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/PlayerController.h"
#include "StealthPlayerController.generated.h"
class AHitman;
/**
*
*/
UCLASS()
class STEALTHTUTORIAL_API AStealthPlayerController : public APlayerController
{
GENERATED_BODY()
public:
AStealthPlayerController(const FObjectInitializer& ObjectInitializer);
virtual void PlayerTick(float DeltaTime) override;
AHitman* GetHitman() const;
protected:
virtual void BeginPlay() override;
virtual void SetupInputComponent() override;
private:
void OnRMBPressed();
void OnRMBReleased();
void MoveHitmanToMouseCursor();
private:
UPROPERTY()
UClass* HitmanBlueprint;
UPROPERTY()
AHitman* Hitman;
bool bOnRMB;
};
/Player/StealthPlayerController.cpp
// Copyright 2021. Elysia-ff
#include "StealthPlayerController.h"
#include <Engine/Public/EngineUtils.h>
#include "Character/Controller/HitmanAIController.h"
#include "Character/Hitman.h"
#include "Player/StealthPlayerCamera.h"
#include "SpawnManager/EnemySpawnManager.h"
AStealthPlayerController::AStealthPlayerController(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryActorTick.bCanEverTick = true;
bShowMouseCursor = true;
static ConstructorHelpers::FObjectFinder<UClass> HitmanObjectFinder(TEXT("Blueprint'/Game/Blueprint/Character/BP_Hitman.BP_Hitman_C'"));
check(HitmanObjectFinder.Succeeded());
HitmanBlueprint = HitmanObjectFinder.Object;
}
void AStealthPlayerController::PlayerTick(float DeltaTime)
{
Super::PlayerTick(DeltaTime);
AStealthPlayerCamera* PossessedPawn = GetPawn<AStealthPlayerCamera>();
if (PossessedPawn != nullptr && GEngine != nullptr)
{
FVector2D MouseInput;
if (GetMousePosition(MouseInput.X, MouseInput.Y))
{
FVector2D MoveInput;
const FIntPoint ViewportSize = GEngine->GameViewport->Viewport->GetSizeXY();
if (MouseInput.X <= PossessedPawn->EdgePixel)
{
MoveInput.Y = -1.0f;
}
else if (MouseInput.X >= ViewportSize.X - PossessedPawn->EdgePixel)
{
MoveInput.Y = 1.0f;
}
if (MouseInput.Y <= PossessedPawn->EdgePixel)
{
MoveInput.X = 1.0f;
}
else if (MouseInput.Y >= ViewportSize.Y - PossessedPawn->EdgePixel)
{
MoveInput.X = -1.0f;
}
if (!MoveInput.IsZero())
{
PossessedPawn->AddMoveInput(MoveInput);
}
}
}
if (bOnRMB)
{
MoveHitmanToMouseCursor();
}
}
AHitman* AStealthPlayerController::GetHitman() const
{
check(Hitman);
return Hitman;
}
void AStealthPlayerController::BeginPlay()
{
Super::BeginPlay();
check(HitmanBlueprint);
FActorSpawnParameters Param;
Param.Owner = this;
Param.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
Hitman = GetWorld()->SpawnActor<AHitman>(HitmanBlueprint, FVector(0.0f, 0.0f, 90.0f), FRotator::ZeroRotator, Param);
check(Hitman);
Hitman->SpawnDefaultController();
TActorIterator<AEnemySpawnManager> Iterator(GetWorld());
if (Iterator)
{
Iterator->BeginSpawn();
}
}
void AStealthPlayerController::SetupInputComponent()
{
Super::SetupInputComponent();
InputComponent->BindAction("RMB", IE_Pressed, this, &AStealthPlayerController::OnRMBPressed);
InputComponent->BindAction("RMB", IE_Released, this, &AStealthPlayerController::OnRMBReleased);
}
void AStealthPlayerController::OnRMBPressed()
{
bOnRMB = true;
}
void AStealthPlayerController::OnRMBReleased()
{
bOnRMB = false;
}
void AStealthPlayerController::MoveHitmanToMouseCursor()
{
FHitResult HitResult;
if (GetHitResultUnderCursor(ECollisionChannel::ECC_Visibility, false, HitResult))
{
FVector TargetLocation = HitResult.ImpactPoint;
check(Hitman && Hitman->GetController<AHitmanAIController>());
Hitman->GetController<AHitmanAIController>()->MoveToLocation(TargetLocation);
}
}
/SpawnManager/EnemySpawnManager.h
// Copyright 2021. Elysia-ff
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EnemySpawnManager.generated.h"
class AEnemy;
UCLASS()
class STEALTHTUTORIAL_API AEnemySpawnManager : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AEnemySpawnManager(const FObjectInitializer& ObjectInitializer);
void BeginSpawn();
const TArray<AEnemy*>* GetEnemies() const;
private:
void SpawnEnemy(const FTransform& SpawnTransform);
public:
UPROPERTY(EditAnywhere, Category = "Spawn Position")
TArray<TWeakObjectPtr<AActor>> SpawnPoses;
private:
UPROPERTY()
UClass* EnemyBlueprint;
UPROPERTY()
TArray<AEnemy*> Enemies;
};
/SpawnManager/EnemySpawnManager.cpp
// Copyright 2021. Elysia-ff
#include "EnemySpawnManager.h"
#include <Engine/Classes/Kismet/GameplayStatics.h>
#include "Character/Enemy.h"
#include "Character/Hitman.h"
#include "LineOfSight/LosComponent.h"
#include "Player/StealthPlayerController.h"
// Sets default values
AEnemySpawnManager::AEnemySpawnManager(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
// Set this actor to call Tick() every frame. You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = false;
static ConstructorHelpers::FObjectFinder<UClass> EnemyObjectFinder(TEXT("Blueprint'/Game/Blueprint/Character/BP_Enemy.BP_Enemy_C'"));
check(EnemyObjectFinder.Succeeded());
EnemyBlueprint = EnemyObjectFinder.Object;
}
void AEnemySpawnManager::BeginSpawn()
{
Enemies.Reserve(SpawnPoses.Num());
for (int32 i = 0; i < SpawnPoses.Num(); i++)
{
if (!SpawnPoses[i].IsValid())
{
UE_LOG(LogTemp, Warning, TEXT("Invalid spawn position : %d"), i);
continue;
}
SpawnEnemy(SpawnPoses[i]->GetTransform());
}
}
const TArray<AEnemy*>* AEnemySpawnManager::GetEnemies() const
{
return &Enemies;
}
void AEnemySpawnManager::SpawnEnemy(const FTransform& SpawnTransform)
{
check(EnemyBlueprint);
FActorSpawnParameters Param;
Param.Owner = this;
Param.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AEnemy* NewEnemy = GetWorld()->SpawnActor<AEnemy>(EnemyBlueprint, SpawnTransform, Param);
check(NewEnemy);
NewEnemy->SpawnDefaultController();
Enemies.Add(NewEnemy);
AStealthPlayerController* Controller = Cast<AStealthPlayerController>(UGameplayStatics::GetPlayerController(GetWorld(), 0));
if (Controller)
{
AHitman* Hitman = Controller->GetHitman();
if (Hitman)
{
Hitman->LosComponent->RegisterSpottableObject(TScriptInterface<ISpottable>(NewEnemy));
}
}
}