// Copyright 2020 Phyronnaz #include "VoxelComponents/VoxelNoClippingComponent.h" #include "VoxelData/VoxelDataIncludes.h" #include "VoxelWorld.h" #include "VoxelMessages.h" #include "Async/Async.h" #include "EngineUtils.h" #include "GameFramework/Character.h" #include "GameFramework/CharacterMovementComponent.h" UVoxelNoClippingComponent::UVoxelNoClippingComponent() { PrimaryComponentTick.bCanEverTick = true; } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void UVoxelNoClippingComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) { VOXEL_FUNCTION_COUNTER(); Super::TickComponent(DeltaTime, TickType, ThisTickFunction); const double Time = FPlatformTime::Seconds(); if (!AsyncResult.IsValid() && LastTickTime + TickRate < Time) { // Time to start a new search LastTickTime = Time; StartAsyncTask(); } if (!AsyncResult.IsValid() || !AsyncResult.IsReady()) { // Search is not started/not complete if (bIsInsideSurface) { // We should have a search in progress already ensure(AsyncResult.IsValid()); // Always fire the event on tick BroadcastMoveTowardsSurface(); } return; } const TArray Results = AsyncResult.Get(); const TArray> VoxelWorlds = MoveTemp(PendingVoxelWorlds); AsyncResult.Reset(); PendingVoxelWorlds.Reset(); if (!ensure(Results.Num() == VoxelWorlds.Num()) || !ensure(Results.Num() > 0)) { return; } FAsyncResult WorstResult; TWeakObjectPtr WorstVoxelWorld; for (int32 Index = 0; Index < Results.Num(); Index++) { const FAsyncResult& Result = Results[Index]; const TWeakObjectPtr& VoxelWorld = VoxelWorlds[Index]; if (!Result.bInsideSurface) { continue; } WorstResult = Result; WorstVoxelWorld = VoxelWorld; // TODO smarter selection when we are inside multiple voxel worlds? break; } if (!WorstResult.bInsideSurface) { // We're safe if (bIsInsideSurface) { bIsInsideSurface = false; BroadcastStopMovingTowardsSurface(); } return; } if (!WorstVoxelWorld.IsValid() || !WorstVoxelWorld->IsCreated()) { LOG_VOXEL(Warning, TEXT("NoClippingComponent: Clearing task result as voxel world is now invalid")); WorstVoxelWorld = nullptr; WorstResult.ClosestSafeLocation.Reset(); } // We're not safe! if (WorstResult.ClosestSafeLocation) { // We found a safe location: teleport there const FVector NewPosition = WorstVoxelWorld->LocalToGlobal(WorstResult.ClosestSafeLocation.GetValue()); const FVector Delta = NewPosition - GetComponentLocation(); AActor& Owner = *GetOwner(); auto* Root = Owner.GetRootComponent(); if (!ensure(Root)) { return; } LOG_VOXEL(Log, TEXT("NoClippingComponent: teleporting %s to %s (delta: %s)"), *Owner.GetName(), *(Root->GetComponentLocation() + Delta).ToString(), *Delta.ToString()); Root->MoveComponent(Delta, Owner.GetActorQuat(), false, nullptr, MOVECOMP_NoFlags, ETeleportType::ResetPhysics); BroadcastOnTeleported(); // We're safe if (bIsInsideSurface) { bIsInsideSurface = false; BroadcastStopMovingTowardsSurface(); } } else { bIsInsideSurface = true; // Start a task now, so we can read the result as soon as possible StartAsyncTask(); LOG_VOXEL(Log, TEXT("NoClippingComponent: could not find a safe location, firing MoveTowardsSurface. SearchRange: %d"), SearchRange); BroadcastMoveTowardsSurface(); } ensure(AsyncResult.IsValid() || !bIsInsideSurface); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void UVoxelNoClippingComponent::StartAsyncTask() { if (!ensure(!AsyncResult.IsValid())) { return; } ensure(PendingVoxelWorlds.Num() == 0); struct FVoxelWorldInfo { AVoxelWorld* VoxelWorld = nullptr; TVoxelSharedPtr Data; FVoxelVector Location{ ForceInit }; }; TArray VoxelWorldInfos; for (auto* VoxelWorld : TActorRange(GetWorld())) { if (!VoxelWorld->IsCreated() || !ShouldUseVoxelWorld(VoxelWorld)) { continue; } const auto Location = VoxelWorld->GlobalToLocalFloat(GetComponentLocation()); if (VoxelWorld->GetWorldBounds().ContainsFloat(Location)) { VoxelWorldInfos.Add(FVoxelWorldInfo{ VoxelWorld, VoxelWorld->GetDataSharedPtr(), Location }); } } if (VoxelWorldInfos.Num() > 0) { PendingVoxelWorlds.Reset(); for (auto& It : VoxelWorldInfos) { PendingVoxelWorlds.Add(It.VoxelWorld); } AsyncResult = Async(EAsyncExecution::TaskGraph, [VoxelWorldInfos, SearchRange = SearchRange]() { TArray Results; for (auto& It : VoxelWorldInfos) { Results.Add(AsyncTask(*It.Data, It.Location, SearchRange)); } return Results; }); } else { // No voxel world found, stop moving towards surface if needed if (bIsInsideSurface) { LOG_VOXEL(Log, TEXT("NoClippingComponent: went outside of the voxel world bounds, stopping moving towards the surface")); bIsInsideSurface = false; BroadcastStopMovingTowardsSurface(); } } } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void UVoxelNoClippingComponent::BroadcastMoveTowardsSurface() const { if (bEnableDefaultBehavior) { auto* CharacterMovement = GetCharacterMovement(); if (CharacterMovement) { ResetVelocity(*CharacterMovement); FVector Direction; if (bIsPlanet) { Direction = (CharacterMovement->GetActorLocation() - PlanetCenter).GetSafeNormal(); } else { Direction = FVector::UpVector; } CharacterMovement->AddImpulse(Direction * Speed, true); } } MoveTowardsSurface.Broadcast(); } void UVoxelNoClippingComponent::BroadcastStopMovingTowardsSurface() const { if (bEnableDefaultBehavior) { auto* CharacterMovement = GetCharacterMovement(); if (CharacterMovement) { ResetVelocity(*CharacterMovement); } } StopMovingTowardsSurface.Broadcast(); } void UVoxelNoClippingComponent::BroadcastOnTeleported() const { if (bEnableDefaultBehavior) { auto* CharacterMovement = GetCharacterMovement(); if (CharacterMovement) { ResetVelocity(*CharacterMovement); } } OnTeleported.Broadcast(); } UCharacterMovementComponent* UVoxelNoClippingComponent::GetCharacterMovement() const { auto* Character = Cast(GetOwner()); if (Character) { auto* CharacterMovement = Character->GetCharacterMovement(); ensure(CharacterMovement); return CharacterMovement; } else { return nullptr; } } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// void UVoxelNoClippingComponent::ResetVelocity(UCharacterMovementComponent& CharacterMovement) { CharacterMovement.Velocity = FVector::ZeroVector; CharacterMovement.ClearAccumulatedForces(); } /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////// UVoxelNoClippingComponent::FAsyncResult UVoxelNoClippingComponent::AsyncTask(const FVoxelData& Data, const FVoxelVector& ComponentLocation, int32 SearchRange) { VOXEL_ASYNC_FUNCTION_COUNTER(); const FVoxelIntBox Bounds = FVoxelIntBox(ComponentLocation).Extend(FMath::Max(1, SearchRange)); FVoxelReadScopeLock Lock(Data, Bounds, FUNCTION_FNAME); const FVoxelConstDataAccelerator Accelerator(Data); for (auto& Neighbor : FVoxelUtilities::GetNeighbors(ComponentLocation)) { if (Accelerator.GetValue(Neighbor, 0).IsEmpty()) { // At least one of the voxel near us is not empty: consider ourselves safe return {}; } } const double StartTime = FPlatformTime::Seconds(); FAsyncResult Result; Result.bInsideSurface = true; float BestDistance = 1e9; // Somewhat suboptimal: we could query the voxels in "circles" instead // It's so fast that it's probably not worth the hassle const auto Values = Data.GetValues(Bounds); const FIntVector Size = Bounds.Size(); for (int32 X = 0; X < Size.X; X++) { for (int32 Y = 0; Y < Size.Y; Y++) { for (int32 Z = 0; Z < Size.Z; Z++) { if (!Values[X + Size.X * Y + Size.X * Size.Y * Z].IsEmpty()) { continue; } const FIntVector Position = Bounds.Min + FIntVector(X, Y, Z); const float Distance = FVoxelVector::Distance(FVoxelVector(Position), ComponentLocation); if (Distance < BestDistance) { BestDistance = Distance; Result.ClosestSafeLocation = Position; } } } } const double EndTime = FPlatformTime::Seconds(); LOG_VOXEL(Verbose, TEXT("NoClippingComponent search took %.3fms. Success: %s"), (EndTime - StartTime) * 1000, *LexToString(Result.ClosestSafeLocation.IsSet())); return Result; }