12. 외곽선 연출 2 - Elysia-ff/UE4-Custom_Stencil_Tutorial GitHub Wiki

시야 체크

적캐릭터가 시야에 들어올때만 외곽선을 표시해야 한다.
ULosComponent에 시야각, 거리 등의 정보가 있으므로 이를 이용해서 시야 범위 내에 존재하는지 확인하고
LineTrace로 ULosComponent의 위치부터 적캐릭터의 얼굴까지 중간에 가로막고 있는 장애물이 없는지 한번 더 확인하면 된다.

ULosComponent를 범용적으로 사용하기 위해 시야에 들어올 수 있는 Actor는 ISpottableInterface를 상속받도록 하겠다.

// 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_HitmanULosComponent의 위치도 머리쪽으로 조정한다.

이제 LosComponent.hLosComponent.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.cppSpawnEnemy하단에 다음 코드를 추가한다.

#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.hEnemySpawnManager.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.cppBeginPlay하단에 아래 코드를 추가한다.

#include <Engine/Public/EngineUtils.h>

#include "SpawnManager/EnemySpawnManager.h"

void AStealthPlayerController::BeginPlay()
{
	// 생략

	TActorIterator<AEnemySpawnManager> Iterator(GetWorld());
	if (Iterator)
	{
		Iterator->BeginSpawn();
	}
}

BP_Hitman -> LosComponentSpotInterval에 적절한 값을 설정하고 플레이하면 Hitman과 시야에 들어온 적캐릭터 사이에 빨간 선이 표시된다.
image
image


완성된 코드

/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));
		}
	}
}
⚠️ **GitHub.com Fallback** ⚠️