486 lines
16 KiB
C++
486 lines
16 KiB
C++
// Copyright 2025 https://yuewu.dev/en All Rights Reserved.
|
|
|
|
#include "Collision/GCS_TraceSubsystem.h"
|
|
|
|
#include "GCS_LogChannels.h"
|
|
#include "KismetTraceUtils.h"
|
|
#include "Async/ParallelFor.h"
|
|
#include "Components/StaticMeshComponent.h"
|
|
#include "Collision/GCS_TraceSystemComponent.h"
|
|
#include "Kismet/KismetMathLibrary.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "UGCS_CollisionTraceSubsystem"
|
|
|
|
// CVars
|
|
namespace GenericCombatSystemCVars
|
|
{
|
|
#if ENABLE_DRAW_DEBUG
|
|
|
|
static bool bEnableTraceDebugging = false;
|
|
FAutoConsoleVariableRef CvarForceEnableTraceDebugging(
|
|
TEXT("gcs.debug.EnableTraceDebugging"),
|
|
bEnableTraceDebugging,
|
|
TEXT("Toggles whether enable draw debugs for all traces. (Enabled: true, Disabled: false)"));
|
|
|
|
static float OverrideTraceDebuggingLifeTime = 0.f;
|
|
FAutoConsoleVariableRef CvarOverrideTraceDebuggingLifeTime(
|
|
TEXT("gcs.debug.OverrideTraceDebuggingLifeTime"),
|
|
OverrideTraceDebuggingLifeTime,
|
|
TEXT("Overrides the draws life time to ease the trace debugging"));
|
|
#endif
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::Initialize(FSubsystemCollectionBase& Collection)
|
|
{
|
|
Super::Initialize(Collection);
|
|
this->TraceStates.Reserve(4068);
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::Tick(float DeltaTime)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::Tick"), STAT_UGCS_TraceSubsystem_Tick, STATGROUP_GCS)
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__)
|
|
|
|
TickIdx++;
|
|
// Lock removals so we don't get any modifications to the array while we are iterating
|
|
RemovalLock = true;
|
|
|
|
PreTraceTick(DeltaTime);
|
|
|
|
PrepareSubTicks(DeltaTime);
|
|
PerformSubTicks(DeltaTime);
|
|
|
|
PostTraceTick();
|
|
|
|
// Reverse iterate, remove pending removals
|
|
RemovalLock = false;
|
|
for (int Idx = TraceStates.Num() - 1; Idx >= 0; Idx--)
|
|
{
|
|
const auto& TraceState = TraceStates[Idx];
|
|
if (!TraceStates.IsValidIndex(Idx))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (TraceState.IsPendingRemoval)
|
|
{
|
|
RemoveTraceStateAt(Idx, TraceState.Handle.Guid);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::RemoveTraceState(const int Idx, const FGuid Guid)
|
|
{
|
|
if (RemovalLock)
|
|
{
|
|
if (TraceStates.IsValidIndex(Idx) && TraceStates[Idx].Handle.Guid == Guid)
|
|
{
|
|
TraceStates[Idx].IsPendingRemoval = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
RemoveTraceStateAt(Idx, Guid);
|
|
}
|
|
}
|
|
|
|
int32 UGCS_TraceSubsystem::AddTraceState()
|
|
{
|
|
FScopeLock ScopeLock(&CriticalSection);
|
|
return TraceStates.AddDefaulted();
|
|
}
|
|
|
|
bool UGCS_TraceSubsystem::IsValidStateIdx(int32 StateIdx) const
|
|
{
|
|
return TraceStates.IsValidIndex(StateIdx);
|
|
}
|
|
|
|
FGCS_TraceState& UGCS_TraceSubsystem::GetTraceStateAt(const int Index)
|
|
{
|
|
return TraceStates[Index];
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::RemoveTraceStateAt(const int Idx, const FGuid Guid)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::RemoveTraceStateAt"), STAT_UGCS_TraceSubsystem_RemoveTraceStateAt, STATGROUP_GCS)
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__)
|
|
FScopeLock ScopeLock(&CriticalSection);
|
|
if (TraceStates.IsValidIndex(Idx) && TraceStates[Idx].Handle.Guid == Guid)
|
|
{
|
|
// 移除并交换
|
|
TraceStates.RemoveAtSwap(Idx);
|
|
|
|
// If a state was moved to Idx, update its corresponding handle in the OwningSystem
|
|
if (TraceStates.IsValidIndex(Idx))
|
|
{
|
|
FGCS_TraceState& MovedState = TraceStates[Idx];
|
|
if (IsValid(MovedState.OwningSystem))
|
|
{
|
|
// Update the Component's HandleToStateIdx map
|
|
MovedState.OwningSystem->HandleToStateIdx.Remove(MovedState.Handle); // Remove old mapping
|
|
MovedState.OwningSystem->HandleToStateIdx.Add(MovedState.Handle, Idx); // Add new mapping
|
|
}
|
|
// mark pending removal if owning system become invalid.
|
|
else
|
|
{
|
|
MovedState.IsPendingRemoval = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::PreTraceTick(const float DeltaTime)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::PreTraceTick"), STAT_UGCS_TraceSubsystem_PreTraceTick, STATGROUP_GCS)
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__)
|
|
ParallelFor(TraceStates.Num(), [&](const int32 Idx)
|
|
{
|
|
if (!TraceStates.IsValidIndex(Idx))
|
|
{
|
|
return;
|
|
}
|
|
auto& TraceState = TraceStates[Idx];
|
|
if (TraceState.ExecutionState == EGCS_TraceExecutionState::Stopped)
|
|
{
|
|
return;
|
|
}
|
|
|
|
check(TraceState.bShouldTickThisFrame == false)
|
|
|
|
if (!IsValid(TraceState.SourceComponent))
|
|
{
|
|
TraceState.ExecutionState = EGCS_TraceExecutionState::Stopped;
|
|
TraceState.bShouldTickThisFrame = false;
|
|
return;
|
|
}
|
|
|
|
const FTransform& CurrentTransform = TraceState.GetCurrentTransform();
|
|
|
|
if (TraceState.TransformsOverTime.IsEmpty())
|
|
{
|
|
TraceState.TransformsOverTime.Add(CurrentTransform);
|
|
}
|
|
|
|
if (TraceState.ExecutionState == EGCS_TraceExecutionState::PendingStop)
|
|
{
|
|
TraceState.bShouldTickThisFrame = true;
|
|
TraceState.TransformsOverTime.Add(CurrentTransform);
|
|
return;
|
|
}
|
|
|
|
switch (TraceState.TickPolicy)
|
|
{
|
|
case EGCS_TraceTickType::Default:
|
|
TraceState.TransformsOverTime.Add(CurrentTransform);
|
|
TraceState.bShouldTickThisFrame = true;
|
|
return;
|
|
case EGCS_TraceTickType::DistanceBased:
|
|
{
|
|
const FVector& PreviousLocation = TraceState.TransformsOverTime[0].GetLocation();
|
|
const FVector& CurrentLocation = CurrentTransform.GetLocation();
|
|
const FRotator& PreviousRotation = TraceState.TransformsOverTime[0].GetRotation().Rotator();
|
|
const FRotator& CurrentRotation = CurrentTransform.GetRotation().Rotator();
|
|
|
|
bool bDistanceThresholdMet = (PreviousLocation - CurrentLocation).Length() >= TraceState.TickInterval;
|
|
bool bAngleThresholdMet = FMath::Abs(PreviousRotation.Yaw - CurrentRotation.Yaw) >= TraceState.AngleThreshold ||
|
|
FMath::Abs(PreviousRotation.Pitch - CurrentRotation.Pitch) >= TraceState.AngleThreshold ||
|
|
FMath::Abs(PreviousRotation.Roll - CurrentRotation.Roll) >= TraceState.AngleThreshold;
|
|
|
|
if (bDistanceThresholdMet || bAngleThresholdMet)
|
|
{
|
|
TraceState.TransformsOverTime.Add(CurrentTransform);
|
|
TraceState.bShouldTickThisFrame = true;
|
|
}
|
|
return;
|
|
}
|
|
|
|
case EGCS_TraceTickType::FixedFrameRate:
|
|
TraceState.TimeSinceLastTick += DeltaTime;
|
|
int32 SubTickCountPreview = FMath::FloorToInt(TraceState.TimeSinceLastTick / TraceState.TickInterval);
|
|
if (SubTickCountPreview > 0)
|
|
{
|
|
TraceState.TransformsOverTime.Add(CurrentTransform);
|
|
TraceState.bShouldTickThisFrame = true;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::PostTraceTick()
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::PostTraceTick"), STAT_UGCS_TraceSubsystem_PostTraceTick, STATGROUP_GCS)
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__)
|
|
ParallelFor(TraceStates.Num(), [&](const int32 Idx)
|
|
{
|
|
auto& TraceState = TraceStates[Idx];
|
|
if (!TraceState.bShouldTickThisFrame)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Reset time since last tick.
|
|
if (TraceState.TickPolicy == EGCS_TraceTickType::FixedFrameRate)
|
|
{
|
|
int32 SubTickCount = TraceState.SubTicks.Num();
|
|
TraceState.TimeSinceLastTick -= SubTickCount * TraceState.TickInterval;
|
|
|
|
TraceState.TimeSinceLastTick = FMath::Max(TraceState.TimeSinceLastTick, 0.0f);
|
|
}
|
|
else
|
|
{
|
|
TraceState.TimeSinceLastTick = 0;
|
|
}
|
|
|
|
TraceState.bShouldTickThisFrame = false;
|
|
|
|
if (TraceState.ExecutionState == EGCS_TraceExecutionState::PendingStop)
|
|
{
|
|
TraceState.ExecutionState = EGCS_TraceExecutionState::Stopped;
|
|
TraceState.TransformsOverTime.Empty();
|
|
TraceState.TimeSinceActive = 0;
|
|
}
|
|
else
|
|
{
|
|
if (!TraceState.TransformsOverTime.IsEmpty())
|
|
{
|
|
TraceState.TransformsOverTime.RemoveAtSwap(0);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::PrepareSubTicks(const float DeltaTime)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::PrepareSubTicks"), STAT_UGCS_TraceSubsystem_PrepareSubTicks, STATGROUP_GCS)
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__)
|
|
|
|
ParallelFor(TraceStates.Num(), [&](const int32 Idx)
|
|
{
|
|
if (!TraceStates.IsValidIndex(Idx))
|
|
{
|
|
return;
|
|
}
|
|
auto& TraceState = TraceStates[Idx];
|
|
|
|
TraceState.SubTicks.Reset();
|
|
if (TraceState.bShouldTickThisFrame)
|
|
{
|
|
int32 SubTickCount = 1;
|
|
if (TraceState.TickPolicy == EGCS_TraceTickType::DistanceBased)
|
|
{
|
|
SubTickCount = FMath::CeilToInt((TraceState.TransformsOverTime[0].GetLocation() - TraceState.TransformsOverTime[1].GetLocation()).Length() / TraceState.TickInterval);
|
|
}
|
|
else if (TraceState.TickPolicy == EGCS_TraceTickType::FixedFrameRate)
|
|
{
|
|
SubTickCount = FMath::FloorToInt(TraceState.TimeSinceLastTick / TraceState.TickInterval);
|
|
}
|
|
SubTickCount = FMath::Min(10, SubTickCount);
|
|
|
|
if (TraceState.TransformsOverTime.Num() > 1)
|
|
{
|
|
const float SubTickRatio = 1.0 / SubTickCount;
|
|
const FTransform CurrentTransform = TraceState.TransformsOverTime.Last();
|
|
const FTransform PreviousTransform = TraceState.TransformsOverTime[0];
|
|
TraceState.CollisionShapeOverTime = TraceState.Shape.Get<FGCS_CollisionShape>().GetDynamicCollisionShape(TraceState.SourceComponent, TraceState.TimeSinceActive);
|
|
for (int32 i = 0; i < SubTickCount; i++)
|
|
{
|
|
FGCS_TraceSubTick SubTick{};
|
|
SubTick.StartTransform = UKismetMathLibrary::TLerp(PreviousTransform, CurrentTransform, SubTickRatio * i, ELerpInterpolationMode::DualQuatInterp);
|
|
SubTick.EndTransform = UKismetMathLibrary::TLerp(PreviousTransform, CurrentTransform, SubTickRatio * (i + 1), ELerpInterpolationMode::DualQuatInterp);
|
|
SubTick.AverageTransform = UKismetMathLibrary::TLerp(SubTick.StartTransform, SubTick.EndTransform, 0.5, ELerpInterpolationMode::DualQuatInterp);
|
|
TraceState.SubTicks.Add(SubTick);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GCS_LOG(Warning, "Trace [%s] has less than 2 transforms in TransformsOverTime array!", *TraceState.Handle.TraceTag.ToString())
|
|
}
|
|
}
|
|
|
|
if (TraceState.ExecutionState == EGCS_TraceExecutionState::InProgress)
|
|
{
|
|
TraceState.TimeSinceActive += DeltaTime;
|
|
}
|
|
});
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::PerformSubTicks(const float DeltaTime)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::PerformSubTicks"), STAT_UGCS_TraceSubsystem_PerformSubTicks, STATGROUP_GCS)
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__)
|
|
|
|
for (int32 Idx = 0; Idx < TraceStates.Num(); Idx++)
|
|
{
|
|
if (!TraceStates.IsValidIndex(Idx))
|
|
{
|
|
continue;
|
|
}
|
|
auto& TraceState = TraceStates[Idx];
|
|
|
|
if (TraceState.bShouldTickThisFrame)
|
|
{
|
|
for (int32 SubTickIdx = 0; SubTickIdx < TraceState.SubTicks.Num(); SubTickIdx++)
|
|
{
|
|
const FGCS_TraceSubTick& SubTick = TraceState.SubTicks[SubTickIdx];
|
|
|
|
// Respect cancellations by user-defined code immediately.
|
|
if (TraceState.ExecutionState == EGCS_TraceExecutionState::Stopped)
|
|
{
|
|
break;
|
|
}
|
|
TraceState.TotalTickNumDuringExecution += 1;
|
|
|
|
FTraceDelegate Delegate = FTraceDelegate::CreateUObject(this, &UGCS_TraceSubsystem::HandleTraceResults, Idx, TickIdx, TraceState.TimeSinceActive);
|
|
|
|
PerformAsyncTrace(SubTick.StartTransform, SubTick.EndTransform, SubTick.AverageTransform, TraceState.World, TraceState.SweepSetting,
|
|
TraceState.CollisionShapeOverTime,
|
|
TraceState.CollisionParams,
|
|
TraceState.ResponseParams,
|
|
TraceState.ObjectQueryParams, &Delegate);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::HandleTraceResults(const FTraceHandle& InTraceHandle, FTraceDatum& InTraceDatum, int32 InTraceStateIdx, uint32 InTickIdx, float InShapeTime)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::HandleTraceResults"), STAT_UGCS_TraceSubsystem_HandleTraceResults, STATGROUP_GCS)
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__)
|
|
|
|
if (!TraceStates.IsValidIndex(InTraceStateIdx))
|
|
{
|
|
return;
|
|
}
|
|
|
|
auto& TraceState = TraceStates[InTraceStateIdx];
|
|
|
|
if (!IsValid(TraceState.OwningSystem) || TraceState.ExecutionState == EGCS_TraceExecutionState::Stopped)
|
|
{
|
|
TraceState.TimeSinceActive = 0;
|
|
return;
|
|
}
|
|
|
|
// GCS_LOG(Warning, "Trace [%s] has ticked %d within %f s", *TraceState.Handle.TraceTag.ToString(), TraceState.TotalTickNumDuringExecution, TraceState.TimeSinceActive)
|
|
|
|
#if ENABLE_DRAW_DEBUG
|
|
if (GenericCombatSystemCVars::bEnableTraceDebugging)
|
|
{
|
|
const EDrawDebugTrace::Type DrawDebugType = GenericCombatSystemCVars::OverrideTraceDebuggingLifeTime > 0 ? EDrawDebugTrace::ForDuration : EDrawDebugTrace::ForOneFrame;
|
|
const float DrawDebugTime = GenericCombatSystemCVars::OverrideTraceDebuggingLifeTime > 0
|
|
? GenericCombatSystemCVars::OverrideTraceDebuggingLifeTime
|
|
: (DrawDebugType == EDrawDebugTrace::ForOneFrame ? 0.0f : 0.5f);
|
|
|
|
const bool bHasAuthority = TraceState.SourceComponent->GetOwner()->HasAuthority();
|
|
|
|
|
|
DrawDebug(InTraceDatum.Start,
|
|
InTraceDatum.End,
|
|
InTraceDatum.Rot,
|
|
InTraceDatum.OutHits, TraceState.Shape.Get<FGCS_CollisionShape>().GetDynamicCollisionShape(TraceState.SourceComponent, InShapeTime), TraceState.World,
|
|
DrawDebugType,
|
|
DrawDebugTime,
|
|
bHasAuthority ? FLinearColor::Red : FLinearColor::White,
|
|
bHasAuthority ? FLinearColor::Green : FLinearColor::Black);
|
|
// GCS_OWNED_CLOG(TraceState.SourceComponent, Display, "Did collision trace: start(%s) end(%s)", *InTraceDatum.Start.ToCompactString(), *InTraceDatum.End.ToCompactString())
|
|
}
|
|
#endif // ENABLE_DRAW_DEBUG
|
|
|
|
TArray<FHitResult> FilteredHits;
|
|
for (const FHitResult& NewHit : InTraceDatum.OutHits)
|
|
{
|
|
if (!TraceState.HitActors.Contains(NewHit.GetActor()))
|
|
{
|
|
FilteredHits.Add(NewHit);
|
|
TraceState.HitActors.Add(NewHit.GetActor());
|
|
}
|
|
}
|
|
if (FilteredHits.Num() > 0)
|
|
{
|
|
TraceState.OwningSystem->OnTraceHitDetected(TraceState.Handle, FilteredHits, TraceState.TimeSinceLastTick, InTickIdx);
|
|
}
|
|
}
|
|
|
|
void UGCS_TraceSubsystem::PerformAsyncTrace(const FTransform& StartTransform, const FTransform& EndTransform, const FTransform& AverageTransform, UWorld* World,
|
|
const FGCS_TraceSweepSetting& TraceSettings, const FCollisionShape& CollisionShape, const FCollisionQueryParams& CollisionParams,
|
|
const FCollisionResponseParams& CollisionResponseParams,
|
|
const FCollisionObjectQueryParams& ObjectQueryParams, const FTraceDelegate* InDelegate)
|
|
{
|
|
switch (TraceSettings.SweepType)
|
|
{
|
|
case EGCS_TraceSweepType::ByChannel:
|
|
World->AsyncSweepByChannel(
|
|
EAsyncTraceType::Multi,
|
|
StartTransform.GetLocation(),
|
|
EndTransform.GetLocation(),
|
|
AverageTransform.GetRotation(),
|
|
TraceSettings.TraceChannel,
|
|
CollisionShape,
|
|
CollisionParams,
|
|
CollisionResponseParams, InDelegate);
|
|
break;
|
|
case EGCS_TraceSweepType::ByObject:
|
|
World->AsyncSweepByObjectType(
|
|
EAsyncTraceType::Multi,
|
|
StartTransform.GetLocation(),
|
|
EndTransform.GetLocation(),
|
|
AverageTransform.GetRotation(),
|
|
ObjectQueryParams,
|
|
CollisionShape,
|
|
CollisionParams, InDelegate);
|
|
break;
|
|
case EGCS_TraceSweepType::ByProfile:
|
|
World->AsyncSweepByProfile(
|
|
EAsyncTraceType::Multi,
|
|
StartTransform.GetLocation(),
|
|
EndTransform.GetLocation(),
|
|
AverageTransform.GetRotation(),
|
|
TraceSettings.ProfileName,
|
|
CollisionShape,
|
|
CollisionParams, InDelegate);
|
|
break;
|
|
}
|
|
}
|
|
|
|
#if ENABLE_DRAW_DEBUG
|
|
void UGCS_TraceSubsystem::DrawDebug(const FVector& StartLocation, const FVector& EndLocation, const FQuat& Orientation, TArray<FHitResult> Hits, const FCollisionShape& CollisionShape,
|
|
const UWorld* World,
|
|
const EDrawDebugTrace::Type DrawDebugType, float DrawDebugTime, const FLinearColor& DrawDebugColor,
|
|
const FLinearColor& DrawDebugHitColor)
|
|
{
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("UGCS_TraceSubsystem::DrawDebug"), STAT_UGCS_TraceSubsystem_DrawDebug, STATGROUP_GCS)
|
|
TRACE_CPUPROFILER_EVENT_SCOPE_STR(__FUNCTION__)
|
|
|
|
// We have to manually find if there is a blocking hit.
|
|
bool bHasBlockingHit = false;
|
|
for (const FHitResult& HitResult : Hits)
|
|
{
|
|
if (HitResult.bBlockingHit)
|
|
{
|
|
bHasBlockingHit = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
switch (CollisionShape.ShapeType)
|
|
{
|
|
case ECollisionShape::Sphere:
|
|
DrawDebugSphereTraceMulti(World, StartLocation, EndLocation, CollisionShape.GetSphereRadius(), DrawDebugType, bHasBlockingHit, Hits, DrawDebugColor, DrawDebugHitColor, DrawDebugTime);
|
|
break;
|
|
case ECollisionShape::Capsule:
|
|
DrawDebugCapsuleTraceMulti(World, StartLocation, EndLocation, CollisionShape.GetCapsuleRadius(), CollisionShape.GetCapsuleHalfHeight(), Orientation.Rotator(), DrawDebugType,
|
|
bHasBlockingHit, Hits, DrawDebugColor, DrawDebugHitColor, DrawDebugTime);
|
|
break;
|
|
case ECollisionShape::Box:
|
|
DrawDebugBoxTraceMulti(World, StartLocation, EndLocation, CollisionShape.GetBox(), Orientation.Rotator(), DrawDebugType, bHasBlockingHit, Hits, DrawDebugColor, DrawDebugHitColor,
|
|
DrawDebugTime);
|
|
break;
|
|
default:
|
|
case ECollisionShape::Line:
|
|
DrawDebugLineTraceMulti(World, StartLocation, EndLocation, DrawDebugType, bHasBlockingHit, Hits, DrawDebugColor, DrawDebugHitColor, DrawDebugTime);
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#undef LOCTEXT_NAMESPACE
|