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으로 변경하고
Line Of Sight -> OutlineDuration
에 적당한 값을 입력한다.
게임을 시작하면 시야에 들어온적들만 외곽선이 표시된다.
/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);
}
}