06. 시야 연출 2 - Elysia-ff/UE4-Custom_Stencil_Tutorial GitHub Wiki
이제 C++ 에서 메시를 만들 차례다.
먼저 LineTrace의 결과값을 저장하는 struct를 하나 만들자.
struct FViewCastInfo
{
FVector Point;
FViewCastInfo(const FVector& _Point)
: Point(_Point)
{
}
};
StealthTutorial.Build.cs
를 다음과 같이 수정하고
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "AIModule", "ProceduralMeshComponent" });
UProceduralMeshComponent
를 확장해서 ULosComponent
를 생성한다.
LosComponent.h
와 LosComponent.cpp
에 일정 각도마다 장애물이 있는지 검사하는 코드를 추가한다.
private:
void AddLineOfSightPoints(TArray<FVector>* ViewPoints);
FVector GetDirection(float AngleInDegrees) const;
FViewCastInfo ViewCast(float AngleInDegrees, float Distance) const;
public:
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;
#include "StealthTypes.hpp"
void ULosComponent::AddLineOfSightPoints(TArray<FVector>* ViewPoints)
{
check(ViewPoints);
check(FarViewCastCount >= 2);
float StartAngle = -FarSightAngle * 0.5f;
float DeltaAngle = (float)FarSightAngle / (FarViewCastCount - 1);
for (int32 i = 0; i < FarViewCastCount; i++)
{
float Angle = StartAngle + (DeltaAngle * i);
FViewCastInfo ViewCastInfo = ViewCast(Angle, FarDistance);
ViewPoints->Add(ViewCastInfo.Point);
}
}
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.ImpactPoint);
}
return FViewCastInfo(LineEnd);
}
그리고 매프레임마다 메시를 생성한다.
주의할 점은 각 정점의 위치는 Component의 로컬좌표를 기준으로 한다.
public:
ULosComponent(const FObjectInitializer& ObjectInitializer);
virtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
private:
void DrawMesh();
ULosComponent::ULosComponent(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PrimaryComponentTick.bCanEverTick = true;
SetCastShadow(false);
SetCollisionProfileName(TEXT("NoCollision"));
}
void ULosComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
DrawMesh();
}
void ULosComponent::DrawMesh()
{
TArray<FVector> ViewPoints;
AddLineOfSightPoints(&ViewPoints);
TArray<FVector> Vertices;
Vertices.Reserve(ViewPoints.Num() + 1);
Vertices.Add(FVector::ZeroVector);
TArray<int32> Triangles;
Triangles.Reserve((ViewPoints.Num() - 1) * 3);
int32 ViewPointsNum = ViewPoints.Num();
for (int32 i = 0; i < ViewPointsNum; i++)
{
Vertices.Add(GetComponentTransform().InverseTransformPosition(ViewPoints[i]));
if (i < ViewPointsNum - 1)
{
Triangles.Add(0);
Triangles.Add(i + 2);
Triangles.Add(i + 1);
}
}
TArray<FVector> Normals;
TArray<FVector2D> UV0;
TArray<FLinearColor> VertexColors;
TArray<FProcMeshTangent> Tangents;
CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UV0, VertexColors, Tangents, false);
}
이제 Hitman.h
에 아래 코드를 추가하고
class ULosComponent;
public:
UPROPERTY(VisibleAnywhere, Category = "Line Of Sight")
ULosComponent* LosComponent;
Hitman.cpp
의 생성자하단에 다음 코드를 추가한 다음
LosComponent = CreateDefaultSubobject<ULosComponent>(TEXT("LosComponent"));
LosComponent->SetupAttachment(RootComponent);
BP_Hitman
에서 FarSightAngle
과 FarDistance
로 시야각, 거리를 조절하고 FarViewCastCount
의 수치를 조절하면 자연스러운 부채꼴모양의 메시가 표시된다.
앞만 볼 수 있는건 좀 이상하니까 주변 짧은 거리에도 시야를 줄 것이다.
시야각만 360 - FarSightAngle
로 지정하고 나머지는 이전과 같다.
private:
void AddNearSightPoints(TArray<FVector>* ViewPoints);
public:
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;
void ULosComponent::DrawMesh()
{
TArray<FVector> ViewPoints;
AddLineOfSightPoints(&ViewPoints);
AddNearSightPoints(&ViewPoints);
// 생략
}
void ULosComponent::AddNearSightPoints(TArray<FVector>* ViewPoints)
{
check(ViewPoints);
check(NearViewCastCount >= 2);
float NearSightAngle = 360.0f - FarSightAngle;
float StartAngle = FarSightAngle * 0.5f;
float DeltaAngle = NearSightAngle / (NearViewCastCount - 1);
for (int32 i = 0; i < NearViewCastCount; i++)
{
float Angle = StartAngle + (DeltaAngle * i);
FViewCastInfo ViewCastInfo = ViewCast(Angle, NearDistance);
ViewPoints->Add(ViewCastInfo.Point);
}
}
그런데 메시의 색도 어둡고 LineTrace가 어디서 일어나는지도 잘 모르겠으니 디버그 코드를 추가하자.
아래 코드를 LosComponent.h
에 추가한다.
public:
virtual void BeginPlay() override;
public:
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Debug")
UMaterialInterface* DebugMaterial;
UPROPERTY(EditAnywhere, Category = "Line Of Sight|Debug")
bool bDrawDebugLine;
LosComponent.cpp
에 아래 함수를 추가하고
void ULosComponent::BeginPlay()
{
Super::BeginPlay();
if (DebugMaterial != nullptr)
{
SetMaterial(0, DebugMaterial);
}
}
DrawMesh
함수를 다음과 같이 수정한다.
#include <Engine/Public/DrawDebugHelpers.h>
void ULosComponent::DrawMesh()
{
// 생략
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);
}
// 생략
그 다음 에디터에서 마테리얼을 생성하고 BP_Hitman
에 연결한 다음 게임을 시작하면 보기 편해진 메시를 볼 수 있다.
/LineOfSight/ViewCastInfo.hpp
// Copyright 2021. Elysia-ff
#pragma once
/**
*
*/
struct FViewCastInfo
{
FVector Point;
FViewCastInfo(const FVector& _Point)
: Point(_Point)
{
}
};
/LineOfSight/LosComponent.h
// Copyright 2021. Elysia-ff
#pragma once
#include "LineOfSight/ViewCastInfo.hpp"
#include "CoreMinimal.h"
#include "ProceduralMeshComponent.h"
#include "LosComponent.generated.h"
/**
*
*/
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;
private:
void DrawMesh();
void AddLineOfSightPoints(TArray<FVector>* ViewPoints);
void AddNearSightPoints(TArray<FVector>* ViewPoints);
FVector GetDirection(float AngleInDegrees) const;
FViewCastInfo ViewCast(float AngleInDegrees, float Distance) 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;
};
/LineOfSight/LosComponent.cpp
// Copyright 2021. Elysia-ff
#include "LosComponent.h"
#include <Engine/Public/DrawDebugHelpers.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);
}
}
void ULosComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
DrawMesh();
}
void ULosComponent::DrawMesh()
{
TArray<FVector> ViewPoints;
AddLineOfSightPoints(&ViewPoints);
AddNearSightPoints(&ViewPoints);
TArray<FVector> Vertices;
Vertices.Reserve(ViewPoints.Num() + 1);
Vertices.Add(FVector::ZeroVector);
TArray<int32> Triangles;
Triangles.Reserve((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);
}
}
TArray<FVector> Normals;
TArray<FVector2D> UV0;
TArray<FLinearColor> VertexColors;
TArray<FProcMeshTangent> Tangents;
CreateMeshSection_LinearColor(0, Vertices, Triangles, Normals, UV0, VertexColors, Tangents, false);
}
void ULosComponent::AddLineOfSightPoints(TArray<FVector>* ViewPoints)
{
check(ViewPoints);
check(FarViewCastCount >= 2);
float StartAngle = -FarSightAngle * 0.5f;
float DeltaAngle = (float)FarSightAngle / (FarViewCastCount - 1);
for (int32 i = 0; i < FarViewCastCount; i++)
{
float Angle = StartAngle + (DeltaAngle * i);
FViewCastInfo ViewCastInfo = ViewCast(Angle, FarDistance);
ViewPoints->Add(ViewCastInfo.Point);
}
}
void ULosComponent::AddNearSightPoints(TArray<FVector>* ViewPoints)
{
check(ViewPoints);
check(NearViewCastCount >= 2);
float NearSightAngle = 360.0f - FarSightAngle;
float StartAngle = FarSightAngle * 0.5f;
float DeltaAngle = NearSightAngle / (NearViewCastCount - 1);
for (int32 i = 0; i < NearViewCastCount; i++)
{
float Angle = StartAngle + (DeltaAngle * i);
FViewCastInfo ViewCastInfo = ViewCast(Angle, NearDistance);
ViewPoints->Add(ViewCastInfo.Point);
}
}
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.ImpactPoint);
}
return FViewCastInfo(LineEnd);
}
/Character/Hitman.h
// Copyright 2021. Elysia-ff
#pragma once
#include "CoreMinimal.h"
#include "Character/StealthCharacter.h"
#include "Hitman.generated.h"
class ULosComponent;
/**
*
*/
UCLASS()
class STEALTHTUTORIAL_API AHitman : public AStealthCharacter
{
GENERATED_BODY()
public:
AHitman(const FObjectInitializer& ObjectInitializer);
public:
UPROPERTY(VisibleAnywhere, Category = "Line Of Sight")
ULosComponent* LosComponent;
};
/Character/Hitman.cpp
// Copyright 2021. Elysia-ff
#include "Hitman.h"
#include "Character/Controller/HitmanAIController.h"
#include "LineOfSight/LosComponent.h"
AHitman::AHitman(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
AIControllerClass = AHitmanAIController::StaticClass();
LosComponent = CreateDefaultSubobject<ULosComponent>(TEXT("LosComponent"));
LosComponent->SetupAttachment(RootComponent);
}