My solution relies on a frequently used technique in game development, physics engine and graphics programming, which is called Raycasting, or in UE4’s term: Line tracing (for clarity and ease of understanding, I would use the term “Line tracing” through most of the post). Simply put, the technique is used to check whether anything lies between point A and point B in the game world.
The algorithm is dead simple. Let us say you have an arc area, with the arc angle of your choice (can be any value from zero to 360 degrees). So all we need to do is sweep around (from half of the arc angle to the opposite value) and perform a line trace operation, with the max distance of the trace operation would be the arc radius. If the line goes freely from the start to the end, that means it is not blocked by something in the world. Otherwise, we just need to save the blocked point.
Of course, this is a naive approach, but given that line tracing is a cheap operation in UE, we can stop here. But you can go much further and check out some optimization of the algorithm here: 2d Visibility from Red Blob Games. This is an amazing article, in my opinion. In fact, this post would not even exist without it.
When we have a clue about what are around us in the game world, we need to build a mesh. Luckily, UE4 does provide a component, called Procedural Mesh Component for this purpose. To know more about building a mesh at runtime, you should first read this article: Anatomy of a Mesh. This is an article from Unity, yet it is simple enough for novices to grab basic knowledge about how a mesh is made. In this post we would only care about vertices and triangles.
The first step here, obviously, is to create a project from the Epic Games’ top-down template, which can be found when you open the engine and see the Project Browser window. Make sure to use the C++ version of the top-down template. In case you did not choose the C++ version, it would not be disastrous, as the underlying logic can be applied with Blueprint, I believe.
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "HeadMountedDisplay", "ProceduralMeshComponent" });
.. then open the Unreal project file (located at
"AdditionalDependencies": [
"Engine",
"ProceduralMeshComponent"
]
Here is how the Unreal project file looks like when being opened inside a text editor on my machine:
Next, we need a channel to perform our line tracing operation. I always prefer to add a custom channel so that I can easily make modifications when needed. To do this, open Edit -> Project Settings -> Collision, and add a trace channel. I’ll call it “Obstacle”.
The Project Settings window also says that “These settings are saved in DefaultEngine.ini (and blah blah blah)”. Let us take a look at the configuration file.
This is a part of my DefaultEngine.ini file. Look at line 57, it says channel ECC_GameTraceChannel1 has the name “Obstacle”. You should find the similar line in your configuration file, and save the ECC_ name of the channel, we would need it right away.
The top-down template comes with a bunch of C++ source file, but in the scope of this tutorial, we only need to care about the *Character file. Open the header and add this code at the end of the class:
ublic:
virtual void BeginPlay() override;
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Line of Sight", meta = (AllowPrivateAccess = "true"))
class UProceduralMeshComponent * LOSMesh;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Line of Sight", meta = (AllowPrivateAccess = "true"))
UMaterial* LOSMaterial;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Line of Sight", meta = (AllowPrivateAccess = "true", UIMin = "1.0", UIMax = "360.0"))
float ArcAngle;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Line of Sight", meta = (AllowPrivateAccess = "true", UIMin = "1.0", UIMax = "5.0"))
float AngleStep;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Line of Sight", meta = (AllowPrivateAccess = "true", UIMin = "200", UIMax = "1000"))
float Radius;
private:
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Line of Sight", meta = (AllowPrivateAccess = "true"))
TArray<FVector> LOSVertices;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Line of Sight", meta = (AllowPrivateAccess = "true"))
TArray<int32> LOSTriangles;
private:
void InitLOSMesh();
void TickLOSMesh(float DeltaSeconds);
void UpdateLOSMeshData(const TArray<FVector>& Vertices, const TArray<int32>& Triangles);
And in the source .cpp file, add implementations for methods that we declared before:
void AUE4StealthCharacter::InitLOSMesh()
{
int NumVertices = FMath::RoundToInt(ArcAngle / AngleStep) + 2;
LOSVertices.Init(FVector::ZeroVector, NumVertices);
int NumTriangles = (ArcAngle == 360) ? ((NumVertices - 1) * 3) : ((NumVertices - 2) * 3);
LOSTriangles.Init(0, NumTriangles);
FVector LineStartLocation = GetActorLocation();
FVector CurrentActorForward = GetActorForwardVector();
float MinAngle = -ArcAngle / 2;
float MaxAngle = ArcAngle / 2;
int VertexIndex = 1;
for (float CurrentAngle = MinAngle;
CurrentAngle <= MaxAngle;
CurrentAngle += AngleStep)
{
FVector CurrentAngleDirection = CurrentActorForward.RotateAngleAxis(CurrentAngle, FVector(0, 0, 1));
FVector LineEndLocation = LineStartLocation + CurrentAngleDirection * Radius;
FVector HitResultInCharacterLocalSpace = GetActorTransform().InverseTransformPosition(LineEndLocation);
LOSVertices[VertexIndex] = HitResultInCharacterLocalSpace;
VertexIndex++;
}
VertexIndex = 0;
for (int Triangle = 0; Triangle < LOSTriangles.Num(); Triangle += 3)
{
LOSTriangles[Triangle] = 0;
LOSTriangles[Triangle + 1] = VertexIndex + 1;
if (Triangle == (NumVertices - 2) * 3) // the arc has 360 angle, or in other words, it's a circle
{
LOSTriangles[Triangle + 2] = 1;
}
else
{
LOSTriangles[Triangle + 1] = VertexIndex + 2;
}
VertexIndex++;
}
UpdateLOSMeshData(LOSVertices, LOSTriangles);
LOSMesh->SetRelativeLocation(FVector(0, 0, -90));
LOSMesh->SetMaterial(0, LOSMaterial);
}
void AUE4StealthCharacter::BeginPlay()
{
Super::BeginPlay();
InitLOSMesh();
}
void AUE4StealthCharacter::TickLOSMesh(float DeltaSeconds)
{
UWorld* World = GetWorld();
if (World == nullptr)
return;
const FName TraceTag("LoSTraceTag");
FCollisionQueryParams TraceParams = FCollisionQueryParams(TraceTag, false, this);
FVector LineStartLocation = GetActorLocation();
FVector CurrentActorForward = GetActorForwardVector();
float MinAngle = -ArcAngle / 2;
float MaxAngle = ArcAngle / 2;
int VertexIndex = 1;
for (float CurrentAngle = MaxAngle;
CurrentAngle >= MinAngle;
CurrentAngle -= AngleStep)
{
FVector CurrentAngleDirection = CurrentActorForward.RotateAngleAxis(CurrentAngle, FVector(0, 0, 1));
FVector LineEndLocation = LineStartLocation + CurrentAngleDirection * Radius;
FHitResult HitResult;
FVector HitPoint;
// In DefaultEngine.ini: Channel=ECC_GameTraceChannel1,Name="Obstacle"
bool bHit = World->LineTraceSingleByChannel(HitResult, LineStartLocation, LineEndLocation, ECollisionChannel::ECC_GameTraceChannel1, TraceParams, FCollisionResponseParams());
if (bHit)
{
HitPoint = HitResult.ImpactPoint;
}
else
{
HitPoint = LineEndLocation;
}
FVector HitResultInCharacterLocalSpace = GetActorTransform().InverseTransformPosition(HitPoint);
LOSVertices[VertexIndex] = HitResultInCharacterLocalSpace;
VertexIndex++;
}
VertexIndex = 0;
int NumVertices = LOSVertices.Num();
for (int Triangle = 0; Triangle < LOSTriangles.Num(); Triangle += 3)
{
LOSTriangles[Triangle] = 0;
LOSTriangles[Triangle + 1] = VertexIndex + 1;
if (Triangle == (NumVertices - 2) * 3) // the arc has 360 angle, or in other words, it's a circle
{
LOSTriangles[Triangle + 2] = 1;
}
else
{
LOSTriangles[Triangle + 2] = VertexIndex + 2;
}
VertexIndex++;
}
UpdateLOSMeshData(LOSVertices, LOSTriangles);
}
void AUE4StealthCharacter::UpdateLOSMeshData(const TArray<FVector>& Vertices, const TArray<int32>& Triangles)
{
TArray<FVector> TempNormals;
TArray<FVector2D> TempUV0;
TArray<FProcMeshTangent> TempTangents;
TArray<FLinearColor> TempVertexColors;
LOSMesh->CreateMeshSection_LinearColor(0, Vertices, Triangles, TempNormals, TempUV0, TempVertexColors, TempTangents, false);
}
The key function here is the TickLOSMesh function. It performs the line tracing work that we discussed above in the Theory section, and arranges all the triangles respectively. Please notice that I put a special case for circle area. Lastly, remember to create the actual mesh instance in the character class’ constructor and update its shape every frame:
AUE4StealthCharacter::AUE4StealthCharacter()
{
// Some code above
// Create the line of sight mesh
LOSMesh = CreateDefaultSubobject<UProceduralMeshComponent>(TEXT("LineOfSightMesh"));
LOSMesh->bUseAsyncCooking = true;
LOSMesh->ContainsPhysicsTriMeshData(false);
LOSMesh->AttachToComponent(RootComponent, FAttachmentTransformRules(EAttachmentRule::SnapToTarget, true));
// Assign some default numeric values to avoid divide-by-zero problem
if (ArcAngle == 0)
ArcAngle = 120;
if (AngleStep == 0)
AngleStep = 1;
if (Radius == 0)
Radius = 500;
// Activate ticking in order to update the cursor every frame.
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
}
void AUE4StealthCharacter::Tick(float DeltaSeconds)
{
// Some code above
TickLOSMesh(DeltaSeconds);
}
Now, all you need to do is to build the C++ code, press play and enjoy your hard work! However, as you can see, the line of sight mesh is not so pretty. I use a custom transparent material with yellow color, it would imitate the light effect much better. However, this tutorial does not cover how to make such material, as there are many articles online about this.
Please keep in mind that when we fix the vertices array’s length at first, changing the arc angle or the angle step value is quite dangerous. It can make your line of sight looks weird, or worse, make your game crash due to an unallowed access to an array member. If you need an area that can be changed at runtime, you should consider using dynamic array feature of the TArray class, of course, with some sacrifices on performance.
Also, the line trace mechanism can be used for fast check of whether a point is visible inside the view range of the player. I would leave this to the reader, as an exercise.
If you want to use multiple LOS mesh with a single character, the character class would need a few modifications while keeping all underlying logic. I would suggest using a separate actor, or a component that can be attached to the character. That’s totally up to you as well.
And last but not least, in case you have any trouble that prevents you when following my instruction, this is the whole source code of the solution on GitHub. Take a look at this, maybe it would help you in a way or another.
That’s all, no more blog post in 2017! Happy holiday and happy new year, every one!