13. 외곽선 연출 3 - Elysia-ff/UE4-Custom_Stencil_Tutorial GitHub Wiki

이제 ISpottable을 상속받은 오브젝트가 시야에 보이는지 트래킹할 수 있다.

SpottableInterface.h에 다음 코드를 추가한다.

	friend class ULosComponent;

public:
	bool IsSpotting() const { return bIsSpotting; };

	virtual void OnBeginSpotted() {}

	virtual void OnEndSpotted() {}

private:
	bool bIsSpotting;

그리고 void ULosComponent::FindActorsInSight()를 다음과 같이 수정한다.

void ULosComponent::FindActorsInSight()
{
	SpottedList.Reset();

	for (int32 i = 0; i < SpotCandidates.Num(); i++)
	{
		if (!SpotCandidates[i])
		{
			continue;
		}

		if (IsInSight(SpotCandidates[i]))
		{
			SpottedList.Add(SpotCandidates[i]);
			
			if (!SpotCandidates[i]->bIsSpotting)
			{
				SpotCandidates[i]->bIsSpotting = true;
				SpotCandidates[i]->OnBeginSpotted();
			}

			DrawDebugLine(GetWorld(), GetComponentLocation(), SpotCandidates[i]->GetSpotPointLocation(), FColor::Red, false, SpotInterval, (uint8)'\000', 5.0f);
		}
		else
		{
			if (SpotCandidates[i]->bIsSpotting)
			{
				SpotCandidates[i]->bIsSpotting = false;
				SpotCandidates[i]->OnEndSpotted();
			}
		}
	}
}

시야에 들어왔는지 여부는 bIsSpotting으로 트래킹하고 bIsSpotting이 변경될 때 OnBeginSpotted혹은 OnEndSpotted가 호출된다.

AEnemy에 상속받은 함수를 추가한다.

public:
	virtual void OnBeginSpotted() override;

	virtual void OnEndSpotted() override;

private:
	void TurnOutlineOn();

	void TurnOutlineOff();

public:
	UPROPERTY(EditAnywhere, Category = "Line Of Sight")
	float OutlineDuration;

private:
	FTimerHandle OutlineTimer;
void AEnemy::OnBeginSpotted()
{
	GetWorldTimerManager().ClearTimer(OutlineTimer);
	TurnOutlineOn();
}

void AEnemy::OnEndSpotted()
{
	if (OutlineDuration <= 0.0f)
	{
		TurnOutlineOff();
	}
	else
	{
		GetWorldTimerManager().SetTimer(OutlineTimer, this, &AEnemy::TurnOutlineOff, OutlineDuration);
	}
}

void AEnemy::TurnOutlineOn()
{
	if (GetMesh())
	{
		GetMesh()->SetCustomDepthStencilValue(2);
	}
}

void AEnemy::TurnOutlineOff()
{
	if (GetMesh())
	{
		GetMesh()->SetCustomDepthStencilValue(0);
	}
}

시야에 들어오면 외곽선을 표시하고 시야에서 벗어나면 OutlineDuration후에 외곽선을 끈다.

이제 BP_Enemy -> Mesh의 스텐실 값을 0으로 변경하고
image

Line Of Sight -> OutlineDuration에 적당한 값을 입력한다.
image

게임을 시작하면 시야에 들어온적들만 외곽선이 표시된다.
outline


완성된 코드

/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()

	friend class ULosComponent;

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:
	virtual FVector GetSpotPointLocation() const = 0;

	bool IsSpotting() const { return bIsSpotting; };

	virtual void OnBeginSpotted() {}

	virtual void OnEndSpotted() {}

private:
	bool bIsSpotting;
};
/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])
		{
			continue;
		}

		if (IsInSight(SpotCandidates[i]))
		{
			SpottedList.Add(SpotCandidates[i]);
			
			if (!SpotCandidates[i]->bIsSpotting)
			{
				SpotCandidates[i]->bIsSpotting = true;
				SpotCandidates[i]->OnBeginSpotted();
			}

			DrawDebugLine(GetWorld(), GetComponentLocation(), SpotCandidates[i]->GetSpotPointLocation(), FColor::Red, false, SpotInterval, (uint8)'\000', 5.0f);
		}
		else
		{
			if (SpotCandidates[i]->bIsSpotting)
			{
				SpotCandidates[i]->bIsSpotting = false;
				SpotCandidates[i]->OnEndSpotted();
			}
		}
	}
}

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;
}
/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;

	virtual void OnBeginSpotted() override;

	virtual void OnEndSpotted() override;

private:
	void TurnOutlineOn();

	void TurnOutlineOff();

public:
	UPROPERTY(VisibleAnywhere, Category = "Line Of Sight")
	USceneComponent* SpotPoint;

	UPROPERTY(EditAnywhere, Category = "Line Of Sight")
	float OutlineDuration;

private:
	FTimerHandle OutlineTimer;
};
/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();
}

void AEnemy::OnBeginSpotted()
{
	GetWorldTimerManager().ClearTimer(OutlineTimer);
	TurnOutlineOn();
}

void AEnemy::OnEndSpotted()
{
	if (OutlineDuration <= 0.0f)
	{
		TurnOutlineOff();
	}
	else
	{
		GetWorldTimerManager().SetTimer(OutlineTimer, this, &AEnemy::TurnOutlineOff, OutlineDuration);
	}
}

void AEnemy::TurnOutlineOn()
{
	if (GetMesh())
	{
		GetMesh()->SetCustomDepthStencilValue(2);
	}
}

void AEnemy::TurnOutlineOff()
{
	if (GetMesh())
	{
		GetMesh()->SetCustomDepthStencilValue(0);
	}
}
⚠️ **GitHub.com Fallback** ⚠️