// Copyright Epic Games, Inc. All Rights Reserved. #include "MassLODSubsystem.h" #include "MassLODTypes.h" #include "GameFramework/PlayerController.h" #include "WorldPartition/WorldPartition.h" #include "WorldPartition/WorldPartitionStreamingSource.h" #include "Engine/World.h" #include "MassSimulationSubsystem.h" #include "Camera/PlayerCameraManager.h" #include "GameFramework/Pawn.h" #if WITH_EDITOR #include "CoreGlobals.h" // GIsEditor #include "Editor.h" // GEditor #include "LevelEditorViewport.h" #include "Editor/EditorEngine.h" #endif // WITH_EDITOR namespace UE::MassLOD { FColor LODColors[] = { FColor::Red, FColor::Yellow, FColor::Emerald, FColor::White, }; namespace Tweakables { bool bLODSubsystemIncludeAllPlayerControllers = true; namespace { static FAutoConsoleVariableRef AnonymousCVars[] = { { TEXT("mass.LODSubsystem.IncludeAllPlayerControllers"), bLODSubsystemIncludeAllPlayerControllers, TEXT("Include all player controllers, even those without a camera or pawn."), ECVF_Default } }; } } // UE::Mass::Tweakables #if WITH_MASSGAMEPLAY_DEBUG namespace Debug { /** Returns whether getting the UMassLODSubsystem and the bool parameter was successful */ bool GetSubsystemAndBoolArgument(const TArray& Args, UWorld* World, UMassLODSubsystem*& OutMassLODSubsystem, bool& bOutBool) { if (!World) { UE_LOG(LogConsoleResponse, Display, TEXT("Error: invalid world")); return false; } OutMassLODSubsystem = World->GetSubsystem(); if (OutMassLODSubsystem == nullptr) { UE_LOG(LogConsoleResponse, Display, TEXT("Error: Unable to fetch MassLODSubsystem instance")); return false; } if (Args.Num() < 1) { UE_LOG(LogConsoleResponse, Display, TEXT("Error: Expecting 1 parameter")); return false; } if (!LexTryParseString(bOutBool, *Args[0])) { UE_LOG(LogConsoleResponse, Display, TEXT("Error: parameter must be an integer or a boolean")); return false; } return true; } FAutoConsoleCommandWithWorldArgsAndOutputDevice ToggleUsePlayerLocationCmd( TEXT("mass.debug.LODSubsystem.UsePlayerLocation"), TEXT("Sets UMassLODSubsystem::bUsePlayerPawnLocationInsteadOfCamera. Note that this is a command that doesn't retain state and usually needs running both for the client and the server"), FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray& Args, UWorld* World, FOutputDevice& Ar) { UMassLODSubsystem* MassLODSubsystem = nullptr; bool bNewValue = false; if (GetSubsystemAndBoolArgument(Args, World, MassLODSubsystem, bNewValue)) { MassLODSubsystem->DebugSetUsePlayerPawnLocationInsteadOfCamera(bNewValue); } })); FAutoConsoleCommandWithWorldArgsAndOutputDevice ToggleGatherPlayers( TEXT("mass.debug.LODSubsystem.GatherPlayers"), TEXT("Sets UMassLODSubsystem::bGatherPlayerControllers. Note that this is a command that doesn't retain state and usually needs running both for the client and the server"), FConsoleCommandWithWorldArgsAndOutputDeviceDelegate::CreateLambda([](const TArray& Args, UWorld* World, FOutputDevice& Ar) { UMassLODSubsystem* MassLODSubsystem = nullptr; bool bNewValue = false; if (GetSubsystemAndBoolArgument(Args, World, MassLODSubsystem, bNewValue)) { MassLODSubsystem->DebugSetGatherPlayers(bNewValue); } })); } #endif // WITH_MASSGAMEPLAY_DEBUG } //----------------------------------------------------------------------------- // UMassLODSubsystem //----------------------------------------------------------------------------- void UMassLODSubsystem::Initialize(FSubsystemCollectionBase& Collection) { Collection.InitializeDependency(UMassSimulationSubsystem::StaticClass()); Super::Initialize(Collection); if (UWorld* World = GetWorld()) { UMassSimulationSubsystem* SimSystem = World->GetSubsystem(); check(SimSystem); SimSystem->GetOnProcessingPhaseStarted(EMassProcessingPhase::PrePhysics).AddUObject(this, &UMassLODSubsystem::OnPrePhysicsPhaseStarted); #if WITH_EDITOR bIgnorePlayerControllersDueToSimulation = (GEditor && GEditor->IsSimulateInEditorInProgress()); bUseEditorLevelViewports = bIgnorePlayerControllersDueToSimulation || (GIsEditor && World->WorldType == EWorldType::Editor); #endif // WITH_EDITOR } SynchronizeViewers(); } void UMassLODSubsystem::OnPrePhysicsPhaseStarted(float DeltaTime) { SynchronizeViewers(); } void UMassLODSubsystem::Deinitialize() { // Remove all viewers from the viewer info list for (FViewerInfo& ViewerInfo : Viewers) { if (ViewerInfo.Handle.IsValid()) { // Safe to remove while iterating as it is a sparse array with a free list RemoveViewer(ViewerInfo.Handle); } } if (UWorld* World = GetWorld()) { if (UMassSimulationSubsystem* SimSystem = World->GetSubsystem()) { SimSystem->GetOnProcessingPhaseStarted(EMassProcessingPhase::PrePhysics).RemoveAll(this); } } Super::Deinitialize(); } int32 UMassLODSubsystem::GetValidViewerIdx(const FMassViewerHandle& ViewerHandle) const { // Check against invalid handle if (!ViewerHandle.IsValid()) { return INDEX_NONE; } // Check against removed viewers const int32 ViewerIdx = ViewerHandle.GetIndex(); if (ViewerIdx >= Viewers.Num() || ViewerHandle != Viewers[ViewerIdx].Handle) { return INDEX_NONE; } return ViewerIdx; } const TArray& UMassLODSubsystem::GetSynchronizedViewers() { SynchronizeViewers(); return Viewers; } FMassViewerHandle UMassLODSubsystem::GetViewerHandleFromActor(const AActor& Actor) const { const FMassViewerHandle* Handle = ViewerMap.Find(GetTypeHash(Actor.GetFName())); return Handle ? *Handle : FMassViewerHandle(); } FMassViewerHandle UMassLODSubsystem::GetViewerHandleFromStreamingSource(const FName StreamingSourceName) const { const FMassViewerHandle* Handle = ViewerMap.Find(GetTypeHash(StreamingSourceName)); return Handle ? *Handle : FMassViewerHandle(); } APlayerController* UMassLODSubsystem::GetPlayerControllerFromViewerHandle(const FMassViewerHandle& ViewerHandle) const { const int32 ViewerIdx = GetValidViewerIdx(ViewerHandle); return ViewerIdx != INDEX_NONE ? Viewers[ViewerIdx].GetPlayerController() : nullptr; } void UMassLODSubsystem::SynchronizeViewers() { if (LastSynchronizedFrame == GFrameCounter) { return; } LastSynchronizedFrame = GFrameCounter; bool bNeedShrinking = false; const UWorld* World = GetWorld(); UWorldPartition* WorldPartition = World ? World->GetWorldPartition() : nullptr; const TArray& StreamingSources = WorldPartition ? WorldPartition->GetStreamingSources() : TArray(); // Go through the list and check validity and store the valid one into a map TMap LocalViewerMap; for (int32 ViewerIdx = 0; ViewerIdx < Viewers.Num(); ++ViewerIdx) { FViewerInfo& ViewerInfo = Viewers[ViewerIdx]; if (!ViewerInfo.Handle.IsValid()) { check(ViewerFreeIndices.Find(ViewerIdx) != INDEX_NONE); continue; } APlayerController* ViewerAsPlayerController = ViewerInfo.GetPlayerController(); if (ViewerAsPlayerController != nullptr #if WITH_EDITOR && bIgnorePlayerControllersDueToSimulation == false #endif // WITH_EDITOR ) { LocalViewerMap.Add(GetTypeHash(ViewerAsPlayerController->GetFName()), ViewerInfo.Handle); } else if (!ViewerInfo.StreamingSourceName.IsNone() && StreamingSources.FindByPredicate([&ViewerInfo](const FWorldPartitionStreamingSource& Source){ return Source.Name == ViewerInfo.StreamingSourceName; }) != nullptr) { LocalViewerMap.Add(GetTypeHash(ViewerInfo.StreamingSourceName), ViewerInfo.Handle); } #if WITH_EDITOR else if (bUseEditorLevelViewports && ViewerInfo.EditorViewportClientIndex != INDEX_NONE && GEditor && GEditor->GetLevelViewportClients().IsValidIndex(ViewerInfo.EditorViewportClientIndex) && GEditor->GetLevelViewportClients()[ViewerInfo.EditorViewportClientIndex]) { const int32 HashValue = GetTypeHash(GEditor->GetLevelViewportClients()[ViewerInfo.EditorViewportClientIndex]); LocalViewerMap.Add(HashValue, ViewerInfo.Handle); } #endif // WITH_EDITOR else { // Safe to remove while iterating as it is a sparse array with a free list RemoveViewer(ViewerInfo.Handle); bNeedShrinking |= ViewerIdx == Viewers.Num() - 1; } } if (World) { if (bGatherPlayerControllers) { // Now go through all current player controllers and add if they do not exist for (FConstPlayerControllerIterator PlayerIterator = World->GetPlayerControllerIterator(); PlayerIterator; ++PlayerIterator) { APlayerController* PlayerController = (*PlayerIterator).Get(); check(PlayerController); // Check if the controller already exists by trying to remove it from the map which was filled up with controllers we were tracking if (LocalViewerMap.Remove(GetTypeHash(PlayerController->GetFName())) == 0) { // If not add it to the list AddPlayerViewer(*PlayerController); } } } if (bGatherStreamingSources) { // Now go through all current streaming source and add if they do not exist for (const FWorldPartitionStreamingSource& StreamingSource : StreamingSources) { if (LocalViewerMap.Remove(GetTypeHash(StreamingSource.Name)) == 0) { AddStreamingSourceViewer(StreamingSource.Name); } } } if (bAllowNonPlayerViwerActors) { for (int32 ActorViewerIndex = RegisteredActorViewers.Num() - 1; ActorViewerIndex >= 0; --ActorViewerIndex) { if (RegisteredActorViewers[ActorViewerIndex]) { AddActorViewer(*RegisteredActorViewers[ActorViewerIndex]); } else { RegisteredActorViewers.RemoveAtSwap(ActorViewerIndex, EAllowShrinking::No); } } } } #if WITH_EDITOR if (bUseEditorLevelViewports) { CA_ASSUME(GEditor); for (int32 ClientIndex = 0; ClientIndex < GEditor->GetLevelViewportClients().Num(); ++ClientIndex) { const FLevelEditorViewportClient* LevelVC = GEditor->GetLevelViewportClients()[ClientIndex]; if (LevelVC && LevelVC->IsPerspective()) { const int32 HashValue = GetTypeHash(LevelVC); if (LocalViewerMap.Remove(HashValue) == 0) { AddEditorViewer(HashValue, ClientIndex); } } } } #endif // WITH_EDITOR // Anything left in the map need to be removed from the list for (TMap::TIterator Itr = LocalViewerMap.CreateIterator(); Itr; ++Itr) { const int32 ViewerIdx = Itr->Value.GetIndex(); RemoveViewer(Viewers[ViewerIdx].Handle); bNeedShrinking |= ViewerIdx == Viewers.Num() - 1; } if (bNeedShrinking) { // Check to shrink the array of viewers if possible while (Viewers.Num() > 0 && ViewerFreeIndices.Num() > 0 && ViewerFreeIndices.Remove(Viewers.Num() - 1)) { Viewers.Pop(EAllowShrinking::No); } } // Update location and direction for every type of viewer for (FViewerInfo& ViewerInfo : Viewers) { if (!ViewerInfo.Handle.IsValid()) { continue; } if (APlayerController* ViewerAsPlayerController = ViewerInfo.GetPlayerController()) { ViewerInfo.bEnabled = !WorldPartition || ViewerAsPlayerController->bEnableStreamingSource; // Note: Using bUsePlayerPawnLocationInsteadOfCamera will not work correctly with FOV based LOD, since the // camera will be at wrong location. // @todo: separate "player location" and "view location", and use the player location on distance based LOD // calculations for stability, and view location in FOV based LOD for view precision. if (bUsePlayerPawnLocationInsteadOfCamera && ViewerAsPlayerController->GetPawn()) { ViewerInfo.Location = ViewerAsPlayerController->GetPawn()->GetActorLocation(); ViewerInfo.Rotation = ViewerAsPlayerController->GetPawn()->GetActorRotation(); } else { FVector PlayerCameraLocation(ForceInitToZero); FRotator PlayerCameraRotation(FRotator::ZeroRotator); ViewerAsPlayerController->GetPlayerViewPoint(PlayerCameraLocation, PlayerCameraRotation); ViewerInfo.Location = PlayerCameraLocation; ViewerInfo.Rotation = PlayerCameraRotation; } // Try to fetch a more precise FOV if (ViewerAsPlayerController->PlayerCameraManager) { ViewerInfo.FOV = ViewerAsPlayerController->PlayerCameraManager->GetFOVAngle(); // @todo need to find a way to retrieve aspect ratio, this does not seems to work //ViewerInfo.AspectRatio = MinViewInfo.AspectRatio; } } else if (AActor* Actor = ViewerInfo.ActorViewer.Get()) { ViewerInfo.Location = Actor->GetActorLocation(); ViewerInfo.Rotation = Actor->GetActorRotation(); } #if WITH_EDITOR else if (bUseEditorLevelViewports && ViewerInfo.EditorViewportClientIndex != INDEX_NONE) { CA_ASSUME(GEditor); const FLevelEditorViewportClient* LevelVC = GEditor->GetLevelViewportClients()[ViewerInfo.EditorViewportClientIndex]; checkSlow(LevelVC); ViewerInfo.bEnabled = LevelVC && LevelVC->IsPerspective(); ViewerInfo.Location = LevelVC->GetViewLocation(); ViewerInfo.Rotation = LevelVC->GetViewRotation(); } #endif // WITH_EDITOR else { checkf(!ViewerInfo.StreamingSourceName.IsNone(), TEXT("Expecting to have a streamingsourcename if the playercontroller is null")); const FWorldPartitionStreamingSource* StreamingSource = StreamingSources.FindByPredicate([&ViewerInfo](const FWorldPartitionStreamingSource& Source) { return Source.Name == ViewerInfo.StreamingSourceName; }); checkf(StreamingSource, TEXT("Expecting to be pointing to a valid streaming source")); ViewerInfo.bEnabled = StreamingSource != nullptr; if (StreamingSource) { ViewerInfo.Location = StreamingSource->Location; ViewerInfo.Rotation = StreamingSource->Rotation; } } } } void UMassLODSubsystem::AddPlayerViewer(APlayerController& PlayerController) { #if WITH_EDITOR if (bIgnorePlayerControllersDueToSimulation) { return; } #endif // WITH_EDITOR // ignore players that don't have a pawn nor a camera if (UE::MassLOD::Tweakables::bLODSubsystemIncludeAllPlayerControllers == false && PlayerController.GetPawn() == nullptr && (bool(PlayerController.PlayerCameraManager) == false || PlayerController.PlayerCameraManager->GetLastFrameCameraCacheTime() == 0.f) ) { return; } const int32 HashValue = GetTypeHash(PlayerController.GetFName()); FMassViewerHandle& ViewerHandle = ViewerMap.FindOrAdd(HashValue, FMassViewerHandle()); if (ViewerHandle.IsValid()) { // We are only interested to set the player controller if it was not already set. const int32 ViewerHandleIdx = GetValidViewerIdx(ViewerHandle); check(ViewerHandleIdx != INDEX_NONE); FViewerInfo& ViewerInfo = Viewers[ViewerHandleIdx]; check(ViewerInfo.ActorViewer == nullptr); ViewerInfo.ActorViewer = &PlayerController; } else { // Add new viewer #if UE_ALLOW_DEBUG_REPLICATION_DUPLICATE_VIEWERS_PER_CONTROLLER //for debugging / profiling purposes create DebugNumberViwersPerController //in this case ViewerMap will only contain a hash to the most recent viewer handle created. for (int Idx = 0; Idx < UE::MassLOD::DebugNumberViewersPerController; ++Idx) #endif //UE_ALLOW_DEBUG_REPLICATION_DUPLICATE_VIEWERS_PER_CONTROLLER { const bool bAddNew = ViewerFreeIndices.Num() == 0; const int NewIdx = bAddNew ? Viewers.Num() : ViewerFreeIndices.Pop(); FViewerInfo& NewViewerInfo = bAddNew ? Viewers.AddDefaulted_GetRef() : Viewers[NewIdx]; NewViewerInfo.ActorViewer = &PlayerController; NewViewerInfo.Handle.Index = NewIdx; NewViewerInfo.Handle.SerialNumber = GetNextViewerSerialNumber(); NewViewerInfo.HashValue = HashValue; ViewerHandle = NewViewerInfo.Handle; OnViewerAddedDelegate.Broadcast(NewViewerInfo); } } PlayerController.OnEndPlay.AddUniqueDynamic(this, &UMassLODSubsystem::OnPlayerControllerEndPlay); } void UMassLODSubsystem::AddStreamingSourceViewer(const FName StreamingSourceName) { const int32 HashValue = GetTypeHash(StreamingSourceName); FMassViewerHandle& ViewerHandle = ViewerMap.FindOrAdd(HashValue, FMassViewerHandle()); // only add new viewer if it hasn't been added yet if (ViewerHandle.IsValid() == false) { const bool bAddNew = ViewerFreeIndices.Num() == 0; const int NewIdx = bAddNew ? Viewers.Num() : ViewerFreeIndices.Pop(); FViewerInfo& NewViewerInfo = bAddNew ? Viewers.AddDefaulted_GetRef() : Viewers[NewIdx]; NewViewerInfo.StreamingSourceName = StreamingSourceName; NewViewerInfo.Handle.Index = NewIdx; NewViewerInfo.Handle.SerialNumber = GetNextViewerSerialNumber(); NewViewerInfo.HashValue = HashValue; ViewerHandle = NewViewerInfo.Handle; OnViewerAddedDelegate.Broadcast(NewViewerInfo); } } void UMassLODSubsystem::AddActorViewer(AActor& ActorViewer) { // @todo we might need to use PathName instead const int32 HashValue = GetTypeHash(ActorViewer.GetFName()); FMassViewerHandle& ViewerHandle = ViewerMap.FindOrAdd(HashValue, FMassViewerHandle()); if (ViewerHandle.IsValid()) { // We are only interested to set the player controller if it was not already set. const int32 ViewerHandleIdx = GetValidViewerIdx(ViewerHandle); check(ViewerHandleIdx != INDEX_NONE); FViewerInfo& ViewerInfo = Viewers[ViewerHandleIdx]; ViewerInfo.ActorViewer = &ActorViewer; } else { // Add new viewer const bool bAddNew = ViewerFreeIndices.Num() == 0; const int NewIdx = bAddNew ? Viewers.Num() : ViewerFreeIndices.Pop(); FViewerInfo& NewViewerInfo = bAddNew ? Viewers.AddDefaulted_GetRef() : Viewers[NewIdx]; NewViewerInfo.ActorViewer = &ActorViewer; NewViewerInfo.Handle.Index = NewIdx; NewViewerInfo.Handle.SerialNumber = GetNextViewerSerialNumber(); NewViewerInfo.HashValue = HashValue; ViewerHandle = NewViewerInfo.Handle; OnViewerAddedDelegate.Broadcast(NewViewerInfo); } } #if WITH_EDITOR void UMassLODSubsystem::AddEditorViewer(const int32 HashValue, const int32 ClientIndex) { FMassViewerHandle& ViewerHandle = ViewerMap.FindOrAdd(HashValue, FMassViewerHandle()); // only add new viewer if it hasn't been added yet if (ViewerHandle.IsValid() == false) { const bool bAddNew = ViewerFreeIndices.Num() == 0; const int NewIdx = bAddNew ? Viewers.Num() : ViewerFreeIndices.Pop(); FViewerInfo& NewViewerInfo = bAddNew ? Viewers.AddDefaulted_GetRef() : Viewers[NewIdx]; using ClientIndexType = decltype(NewViewerInfo.EditorViewportClientIndex); check(ClientIndex >= std::numeric_limits::min() && ClientIndex <= std::numeric_limits::max()); NewViewerInfo.EditorViewportClientIndex = static_cast(ClientIndex); NewViewerInfo.Handle.Index = NewIdx; NewViewerInfo.Handle.SerialNumber = GetNextViewerSerialNumber(); NewViewerInfo.HashValue = HashValue; ViewerHandle = NewViewerInfo.Handle; OnViewerAddedDelegate.Broadcast(NewViewerInfo); } } #endif // WITH_EDITOR void UMassLODSubsystem::RemoveViewer(const FMassViewerHandle& ViewerHandle) { #if UE_ALLOW_DEBUG_REPLICATION_DUPLICATE_VIEWERS_PER_CONTROLLER const int32 ViewerHandleIdx = GetValidViewerIdx(ViewerHandle); check(ViewerHandleIdx != INDEX_NONE); //find all the viewer handles the slow way and remove them for (int32 ViewerIdx = 0; ViewerIdx < Viewers.Num(); ++ViewerIdx) { FViewerInfo& ViewerInfo = Viewers[ViewerIdx]; if (ViewerInfo.PlayerController == Viewers[ViewerHandleIdx].PlayerController) { RemoveViewerInternal(ViewerInfo.Handle); } } #else RemoveViewerInternal(ViewerHandle); #endif //UE_ALLOW_DEBUG_REPLICATION_DUPLICATE_VIEWERS_PER_CONTROLLER } void UMassLODSubsystem::RemoveViewerInternal(const FMassViewerHandle& ViewerHandle) { const int32 ViewerIdx = GetValidViewerIdx(ViewerHandle); check(ViewerIdx != INDEX_NONE); FViewerInfo& ViewerInfo = Viewers[ViewerIdx]; OnViewerRemovedDelegate.Broadcast(ViewerInfo); if (APlayerController* ViewerAsPlayerController = ViewerInfo.GetPlayerController()) { ViewerAsPlayerController->OnEndPlay.RemoveDynamic(this, &UMassLODSubsystem::OnPlayerControllerEndPlay); } ViewerMap.Remove(ViewerInfo.HashValue); ViewerInfo.Reset(); ViewerFreeIndices.Push(ViewerIdx); } void UMassLODSubsystem::OnPlayerControllerEndPlay(AActor* Actor, EEndPlayReason::Type EndPlayReason) { APlayerController* PlayerController = Cast(Actor); if (ensure(PlayerController)) { const FMassViewerHandle ViewerHandle = GetViewerHandleFromActor(*PlayerController); if (ensure(ViewerHandle.IsValid())) { RemoveViewer(ViewerHandle); } } } void UMassLODSubsystem::RegisterActorViewer(AActor& ActorViewer) { RegisteredActorViewers.AddUnique(&ActorViewer); } void UMassLODSubsystem::UnregisterActorViewer(AActor& ActorViewer) { if (RegisteredActorViewers.RemoveSingleSwap(&ActorViewer, EAllowShrinking::No)) { const FMassViewerHandle ViewerHandle = GetViewerHandleFromActor(ActorViewer); if (ensure(ViewerHandle.IsValid())) { RemoveViewer(ViewerHandle); } } } FMassViewerHandle UMassLODSubsystem::GetViewerHandleFromPlayerController(const APlayerController* PlayerController) const { PRAGMA_DISABLE_DEPRECATION_WARNINGS return PlayerController ? GetViewerHandleFromActor(*PlayerController) : FMassViewerHandle(); PRAGMA_ENABLE_DEPRECATION_WARNINGS } #if WITH_MASSGAMEPLAY_DEBUG void UMassLODSubsystem::DebugUnregisterActorViewer() { for (const TObjectPtr& RegisteredActorViewer : RegisteredActorViewers) { if (const AActor* ActorViewer = RegisteredActorViewer) { const FMassViewerHandle ViewerHandle = GetViewerHandleFromActor(*ActorViewer); if (ensure(ViewerHandle.IsValid())) { RemoveViewer(ViewerHandle); } } } RegisteredActorViewers.Reset(); } #endif // WITH_MASSGAMEPLAY_DEBUG //----------------------------------------------------------------------------- // FViewerInfo //----------------------------------------------------------------------------- void FViewerInfo::Reset() { Handle.Invalidate(); ActorViewer = nullptr; #if WITH_EDITOR EditorViewportClientIndex = INDEX_NONE; #endif // WITH_EDITOR HashValue = 0; } bool FViewerInfo::IsLocal() const { APlayerController* ViewerAsPlayerController = GetPlayerController(); return (ViewerAsPlayerController && ViewerAsPlayerController->IsLocalController()) || !StreamingSourceName.IsNone() #if WITH_EDITOR || EditorViewportClientIndex != INDEX_NONE #endif // WITH_EDITOR ; } APlayerController* FViewerInfo::GetPlayerController() const { return Cast(ActorViewer.Get()); }