├── .gitignore ├── Config └── FilterPlugin.ini ├── LICENSE ├── README.md ├── Resources └── Icon128.png ├── SCREENSHOT.jpg ├── Source ├── .clang-format └── VaFogOfWar │ ├── Private │ ├── VaFogAgentComponent.cpp │ ├── VaFogBlockingVolume.cpp │ ├── VaFogBoundsVolume.cpp │ ├── VaFogController.cpp │ ├── VaFogDefines.h │ ├── VaFogLayer.cpp │ ├── VaFogLibrary.cpp │ ├── VaFogOfWar.cpp │ ├── VaFogRadiusStrategy.cpp │ ├── VaFogSettings.cpp │ └── VaFogTerrainLayer.cpp │ ├── Public │ ├── VaFogAgentComponent.h │ ├── VaFogBlockingVolume.h │ ├── VaFogBoundsVolume.h │ ├── VaFogController.h │ ├── VaFogLayer.h │ ├── VaFogLibrary.h │ ├── VaFogOfWar.h │ ├── VaFogRadiusStrategy.h │ ├── VaFogSettings.h │ ├── VaFogTerrainLayer.h │ └── VaFogTypes.h │ └── VaFogOfWar.Build.cs └── VaFogOfWar.uplugin /.gitignore: -------------------------------------------------------------------------------- 1 | # Visual Studio 2015 user specific files 2 | .vs/ 3 | 4 | # Visual Studio 2015 database file 5 | *.VC.db 6 | 7 | # Compiled Object files 8 | *.slo 9 | *.lo 10 | *.o 11 | *.obj 12 | 13 | # Precompiled Headers 14 | *.gch 15 | *.pch 16 | 17 | # Compiled Dynamic libraries 18 | *.so 19 | *.dylib 20 | *.dll 21 | 22 | # Fortran module files 23 | *.mod 24 | 25 | # Compiled Static libraries 26 | *.lai 27 | *.la 28 | *.a 29 | *.lib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.ipa 36 | 37 | # These project files can be generated by the engine 38 | *.xcodeproj 39 | *.xcworkspace 40 | *.sln 41 | *.suo 42 | *.opensdf 43 | *.sdf 44 | *.VC.db 45 | *.VC.opendb 46 | 47 | # Precompiled Assets 48 | SourceArt/**/*.png 49 | SourceArt/**/*.tga 50 | 51 | # Binary Files 52 | Binaries/* 53 | Plugins/*/Binaries/* 54 | 55 | # Builds 56 | Build/* 57 | 58 | # Whitelist PakBlacklist-.txt files 59 | !Build/*/ 60 | Build/*/** 61 | !Build/*/PakBlacklist*.txt 62 | 63 | # Don't ignore icon files in Build 64 | !Build/**/*.ico 65 | 66 | # Built data for maps 67 | *_BuiltData.uasset 68 | 69 | # Configuration files generated by the Editor 70 | Saved/* 71 | 72 | # Compiled source files for the engine to use 73 | Intermediate/* 74 | Plugins/*/Intermediate/* 75 | 76 | # Cache files for the editor to use 77 | DerivedDataCache/* 78 | -------------------------------------------------------------------------------- /Config/FilterPlugin.ini: -------------------------------------------------------------------------------- 1 | [FilterPlugin] 2 | /README.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vladimir Alyamkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub](https://img.shields.io/github/license/ufna/VaFogOfWar) 2 | ![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/ufna/VaFogOfWar?include_prereleases) 3 | 4 | > [!IMPORTANT] 5 | > Thank you for your support and engagement with the VaFogOfWar project! I want to let you know that I will be archiving this repository. While I will no longer be actively maintaining it, I encourage anyone who is interested to fork the project and continue development as you see fit. 6 | 7 | # VaFogOfWar 8 | 9 | A clear and simple solution of Fog of War for Unreal Engine 4. 10 | 11 | Full demo available [here](https://github.com/ufna/VaFogOfWarDemoBP). 12 | 13 | ![SCREENSHOT](SCREENSHOT.jpg) 14 | 15 | **VaFogOfWar** implements fast and optimized fog of war solution for topdown games like Dota, StarCraft or League of Legends. It fast enough even to work on mid-end mobile devices, so it works like a charm on desktop. 16 | 17 | Check [wiki](https://bit.ly/VaFogOfWar-Docs) for usage examples and development notes. 18 | 19 | ### Main features 20 | 21 | - Three types of layers: current visibility, global visibility (permanent from black), terrain 22 | - Eight height levels supported (river, lowground, highground, etc.) 23 | - Dynamic and static obstacles (trees, rocks, etc) 24 | - Different types of radius strategy: circle, square and others 25 | - Freeform fog blocking volumes as tool for terrain level painting 26 | - Initial terrain levels can be set with heightmap 27 | 28 | ### Release notes 29 | 30 | Plugin is released for free without any limitations, you can use it and modify as you want. Feel free to post your comments and feedback. 31 | 32 | Demo content is not provided, but it's released as separate paid demo project here: https://gum.co/TFflr . It's also planned to be released at the Unreal Marketplace a bit later. 33 | 34 | 35 | ## Legal info 36 | 37 | Unreal® is a trademark or registered trademark of Epic Games, Inc. in the United States of America and elsewhere. 38 | 39 | Unreal® Engine, Copyright 1998 – 2020, Epic Games, Inc. All rights reserved. 40 | 41 | -------------------------------------------------------------------------------- /Resources/Icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufna/VaFogOfWar/a91ff40bd0fd3909c61855b517710fafd10beda8/Resources/Icon128.png -------------------------------------------------------------------------------- /SCREENSHOT.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ufna/VaFogOfWar/a91ff40bd0fd3909c61855b517710fafd10beda8/SCREENSHOT.jpg -------------------------------------------------------------------------------- /Source/.clang-format: -------------------------------------------------------------------------------- 1 | --- 2 | Language: Cpp 3 | BasedOnStyle: LLVM 4 | IndentWidth: 4 5 | TabWidth: 4 6 | UseTab: ForContinuationAndIndentation 7 | Standard: Cpp11 8 | AccessModifierOffset: -4 9 | AlignAfterOpenBracket: DontAlign 10 | AlignEscapedNewlines: Right 11 | AlignTrailingComments: true 12 | AllowShortCaseLabelsOnASingleLine: true 13 | AllowShortFunctionsOnASingleLine: InlineOnly 14 | BreakBeforeBraces: Allman 15 | BreakConstructorInitializersBeforeComma: true 16 | ColumnLimit: 0 17 | PointerAlignment: Left 18 | SpacesInAngles: false 19 | --- 20 | Language: ObjC 21 | DisableFormat: true 22 | ... 23 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogAgentComponent.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogAgentComponent.h" 4 | 5 | #include "VaFogController.h" 6 | #include "VaFogDefines.h" 7 | 8 | #include "Engine/Texture2D.h" 9 | #include "Components/BillboardComponent.h" 10 | 11 | UVaFogAgentComponent::UVaFogAgentComponent(const FObjectInitializer& ObjectInitializer) 12 | : Super(ObjectInitializer) 13 | { 14 | bAutoActivate = true; 15 | bWantsInitializeComponent = true; 16 | 17 | bAgentEnabled = true; 18 | TargetChannels.Add(EVaFogLayerChannel::Permanent); 19 | RadiusStrategy = EVaFogRadiusStrategy::Circle; 20 | VisionRadius = 500; 21 | HeightLevel = EVaFogHeightLevel::HL_3; 22 | 23 | #if WITH_EDITORONLY_DATA 24 | bVisualizeComponent = true; 25 | #endif 26 | } 27 | 28 | void UVaFogAgentComponent::InitializeComponent() 29 | { 30 | Super::InitializeComponent(); 31 | } 32 | 33 | void UVaFogAgentComponent::UninitializeComponent() 34 | { 35 | Super::UninitializeComponent(); 36 | } 37 | 38 | void UVaFogAgentComponent::BeginPlay() 39 | { 40 | if (bAgentEnabled) 41 | { 42 | UVaFogController::Get(this)->OnFogAgentAdded(this); 43 | } 44 | 45 | Super::BeginPlay(); 46 | } 47 | 48 | void UVaFogAgentComponent::EndPlay(const EEndPlayReason::Type EndPlayReason) 49 | { 50 | if (bAgentEnabled && UVaFogController::Get(this, EGetWorldErrorMode::LogAndReturnNull)) 51 | { 52 | UVaFogController::Get(this)->OnFogAgentRemoved(this); 53 | } 54 | 55 | Super::EndPlay(EndPlayReason); 56 | } 57 | 58 | #if WITH_EDITORONLY_DATA 59 | void UVaFogAgentComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) 60 | { 61 | UpdateSpriteTexture(); 62 | 63 | Super::PostEditChangeProperty(PropertyChangedEvent); 64 | } 65 | 66 | void UVaFogAgentComponent::OnRegister() 67 | { 68 | Super::OnRegister(); 69 | 70 | UpdateSpriteTexture(); 71 | } 72 | #endif 73 | 74 | void UVaFogAgentComponent::EnableAgent(bool bEnable) 75 | { 76 | bAgentEnabled = bEnable; 77 | 78 | UpdateAgentRegistration(); 79 | } 80 | 81 | void UVaFogAgentComponent::DisableAgent() 82 | { 83 | bAgentEnabled = false; 84 | 85 | UpdateAgentRegistration(); 86 | } 87 | 88 | bool UVaFogAgentComponent::IsAgentEnabled() const 89 | { 90 | return bAgentEnabled; 91 | } 92 | 93 | void UVaFogAgentComponent::SetVisionRadius(int32 NewVisionRadius) 94 | { 95 | VisionRadius = FMath::Max(NewVisionRadius, 0); 96 | } 97 | 98 | void UVaFogAgentComponent::SetHeightLevel(EVaFogHeightLevel NewHeightLevel) 99 | { 100 | HeightLevel = NewHeightLevel; 101 | } 102 | 103 | void UVaFogAgentComponent::UpdateAgentRegistration() 104 | { 105 | if (bAgentEnabled) 106 | { 107 | UVaFogController::Get(this)->OnFogAgentAdded(this); 108 | } 109 | else 110 | { 111 | UVaFogController::Get(this)->OnFogAgentRemoved(this); 112 | } 113 | } 114 | 115 | #if WITH_EDITORONLY_DATA 116 | void UVaFogAgentComponent::UpdateSpriteTexture() 117 | { 118 | if (SpriteComponent) 119 | { 120 | SpriteComponent->SpriteInfo.Category = TEXT("Misc"); 121 | SpriteComponent->SpriteInfo.DisplayName = NSLOCTEXT("SpriteCategory", "Misc", "Misc"); 122 | 123 | if (TargetChannels.Contains(EVaFogLayerChannel::Terrain)) 124 | { 125 | SpriteComponent->SetSprite(LoadObject(nullptr, TEXT("/Engine/EditorResources/S_Terrain.S_Terrain"))); 126 | } 127 | else 128 | { 129 | SpriteComponent->SetSprite(LoadObject(nullptr, TEXT("/Engine/EditorResources/S_Emitter.S_Emitter"))); 130 | } 131 | } 132 | } 133 | #endif 134 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogBlockingVolume.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogBlockingVolume.h" 4 | 5 | #include "VaFogDefines.h" 6 | #include "VaFogLayer.h" 7 | 8 | #include "Components/BillboardComponent.h" 9 | #include "Components/BrushComponent.h" 10 | #include "Engine/CollisionProfile.h" 11 | #include "UObject/ConstructorHelpers.h" 12 | #include "Engine/Texture2D.h" 13 | 14 | AVaFogBlockingVolume::AVaFogBlockingVolume(const FObjectInitializer& ObjectInitializer) 15 | : Super(ObjectInitializer) 16 | { 17 | #if WITH_EDITORONLY_DATA 18 | SpriteComponent = CreateEditorOnlyDefaultSubobject(TEXT("Sprite")); 19 | 20 | if (!IsRunningCommandlet() && (SpriteComponent != nullptr)) 21 | { 22 | // Structure to hold one-time initialization 23 | struct FConstructorStatics 24 | { 25 | ConstructorHelpers::FObjectFinderOptional TextRenderTexture; 26 | FConstructorStatics() 27 | : TextRenderTexture(TEXT("/Engine/EditorResources/S_VectorFieldVol")) 28 | { 29 | } 30 | }; 31 | static FConstructorStatics ConstructorStatics; 32 | 33 | SpriteComponent->Sprite = ConstructorStatics.TextRenderTexture.Get(); 34 | SpriteComponent->SetRelativeScale3D(FVector(1.f, 1.f, 1.f)); 35 | SpriteComponent->SetupAttachment(GetBrushComponent()); 36 | SpriteComponent->bIsScreenSizeScaled = true; 37 | SpriteComponent->SetUsingAbsoluteScale(true); 38 | SpriteComponent->bReceivesDecals = false; 39 | } 40 | #endif 41 | 42 | // GetBrushComponent()->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName); 43 | GetBrushComponent()->Mobility = EComponentMobility::Static; 44 | 45 | BrushColor = FColor(0, 255, 0, 255); 46 | bColored = true; 47 | 48 | HeightLevel = EVaFogHeightLevel::HL_3; 49 | Priority = 0; 50 | } 51 | 52 | void AVaFogBlockingVolume::PostLoad() 53 | { 54 | Super::PostLoad(); 55 | 56 | UpdateTargetLayer(); 57 | } 58 | 59 | void AVaFogBlockingVolume::PostActorCreated() 60 | { 61 | Super::PostActorCreated(); 62 | 63 | UpdateTargetLayer(); 64 | } 65 | 66 | void AVaFogBlockingVolume::OnConstruction(const FTransform& Transform) 67 | { 68 | UpdateTargetLayer(); 69 | 70 | #if WITH_EDITORONLY_DATA 71 | // Force update layer state for realtime preview 72 | if (Layer) 73 | { 74 | Layer->UpdateLayer(true); 75 | } 76 | #endif 77 | } 78 | 79 | void AVaFogBlockingVolume::Destroyed() 80 | { 81 | if (Layer) 82 | { 83 | Layer->RemoveFogBlockingVolume(this); 84 | } 85 | 86 | Super::Destroyed(); 87 | } 88 | 89 | #if WITH_EDITOR 90 | void AVaFogBlockingVolume::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) 91 | { 92 | UpdateTargetLayer(); 93 | 94 | // @TODO Force volume brush to be square or use custom BrushBuilder 95 | 96 | Super::PostEditChangeProperty(PropertyChangedEvent); 97 | 98 | // Force update layer state for realtime preview 99 | if (Layer) 100 | { 101 | Layer->UpdateLayer(true); 102 | } 103 | } 104 | #endif 105 | 106 | void AVaFogBlockingVolume::UpdateTargetLayer() 107 | { 108 | UE_LOG(LogVaFog, Log, TEXT("[%s] Volume [%s] Check layer we should update and apply self into"), *VA_FUNC_LINE, *GetName()); 109 | 110 | if (Layer) 111 | { 112 | Layer->AddFogBlockingVolume(this); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogBoundsVolume.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogBoundsVolume.h" 4 | 5 | #include "VaFogController.h" 6 | #include "VaFogDefines.h" 7 | #include "VaFogOfWar.h" 8 | #include "VaFogSettings.h" 9 | 10 | #include "Components/BillboardComponent.h" 11 | #include "Components/BrushComponent.h" 12 | #include "DrawDebugHelpers.h" 13 | #include "Engine/CollisionProfile.h" 14 | #include "UObject/ConstructorHelpers.h" 15 | #include "Engine/Texture2D.h" 16 | 17 | AVaFogBoundsVolume::AVaFogBoundsVolume(const FObjectInitializer& ObjectInitializer) 18 | : Super(ObjectInitializer) 19 | { 20 | #if WITH_EDITORONLY_DATA 21 | SpriteComponent = CreateEditorOnlyDefaultSubobject(TEXT("Sprite")); 22 | 23 | if (!IsRunningCommandlet() && (SpriteComponent != nullptr)) 24 | { 25 | // Structure to hold one-time initialization 26 | struct FConstructorStatics 27 | { 28 | ConstructorHelpers::FObjectFinderOptional TextRenderTexture; 29 | FConstructorStatics() 30 | : TextRenderTexture(TEXT("/Engine/EditorResources/S_SkyLight")) 31 | { 32 | } 33 | }; 34 | static FConstructorStatics ConstructorStatics; 35 | 36 | SpriteComponent->Sprite = ConstructorStatics.TextRenderTexture.Get(); 37 | SpriteComponent->SetRelativeScale3D(FVector(1.f, 1.f, 1.f)); 38 | SpriteComponent->SetupAttachment(GetBrushComponent()); 39 | SpriteComponent->bIsScreenSizeScaled = true; 40 | SpriteComponent->SetUsingAbsoluteScale(true); 41 | SpriteComponent->bReceivesDecals = false; 42 | } 43 | #endif 44 | #if WITH_EDITORONLY_DATA 45 | PrimaryActorTick.bCanEverTick = true; 46 | #endif 47 | 48 | GetBrushComponent()->SetCollisionProfileName(UCollisionProfile::NoCollision_ProfileName); 49 | GetBrushComponent()->Mobility = EComponentMobility::Static; 50 | 51 | BrushColor = FColor(200, 0, 0, 255); 52 | bColored = true; 53 | 54 | CachedFogLayerResolution = 128; 55 | LayerToTextureShift = 64; 56 | 57 | DebugTime = 20.f; 58 | } 59 | 60 | void AVaFogBoundsVolume::OnConstruction(const FTransform& Transform) 61 | { 62 | Super::OnConstruction(Transform); 63 | 64 | UpdateVolumeTransform(); 65 | } 66 | 67 | void AVaFogBoundsVolume::PostInitializeComponents() 68 | { 69 | Super::PostInitializeComponents(); 70 | 71 | UpdateVolumeTransform(); 72 | } 73 | 74 | void AVaFogBoundsVolume::Destroyed() 75 | { 76 | Super::Destroyed(); 77 | } 78 | 79 | bool AVaFogBoundsVolume::ShouldTickIfViewportsOnly() const 80 | { 81 | return true; 82 | } 83 | 84 | void AVaFogBoundsVolume::Tick(float DeltaTime) 85 | { 86 | Super::Tick(DeltaTime); 87 | 88 | if (bDebugVolume) 89 | { 90 | DrawDebugGrid(); 91 | 92 | // It's really not good idea to update it each tick 93 | bDebugVolume = false; 94 | } 95 | } 96 | 97 | #if WITH_EDITOR 98 | void AVaFogBoundsVolume::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) 99 | { 100 | UpdateVolumeTransform(); 101 | 102 | // @TODO Force volume brush to be square or use custom BrushBuilder 103 | 104 | Super::PostEditChangeProperty(PropertyChangedEvent); 105 | } 106 | #endif 107 | 108 | void AVaFogBoundsVolume::UpdateVolumeTransform() 109 | { 110 | // Cache layers resolution for coordinates transform 111 | CachedFogLayerResolution = FVaFogOfWarModule::Get().GetSettings()->FogLayerResolution; 112 | check(FMath::IsPowerOfTwo(CachedFogLayerResolution)); 113 | LayerToTextureShift = CachedFogLayerResolution / 2; 114 | 115 | // Calculate world to layet transform 116 | float VolumeScaleX = (GetBrushComponent()->Bounds.BoxExtent.X * 2) / CachedFogLayerResolution; 117 | float VolumeScaleY = (GetBrushComponent()->Bounds.BoxExtent.Y * 2) / CachedFogLayerResolution; 118 | VolumeTransform.SetScale3D({VolumeScaleX, VolumeScaleY, 1}); 119 | 120 | VolumeTransform.SetRotation(GetBrushComponent()->GetComponentTransform().GetRotation()); 121 | VolumeTransform.SetLocation(GetBrushComponent()->GetComponentTransform().GetLocation()); 122 | 123 | // Calculate and cache cell extent 124 | float CellExtentX = GetBrushComponent()->Bounds.BoxExtent.X / CachedFogLayerResolution; 125 | float CellExtentY = GetBrushComponent()->Bounds.BoxExtent.Y / CachedFogLayerResolution; 126 | CachedCellExtent = FVector(CellExtentX, CellExtentY, 0.f); 127 | } 128 | 129 | FVector AVaFogBoundsVolume::GetCellExtent() const 130 | { 131 | return CachedCellExtent; 132 | } 133 | 134 | FIntPoint AVaFogBoundsVolume::TransformWorldToLayer(const FVector& AgentLocation) const 135 | { 136 | // First transform position into volume local coordinates (scaled with fog rt resolution) 137 | FVector LayerPosition = VolumeTransform.InverseTransformPosition(AgentLocation); 138 | 139 | // Then transform it into texture coordinates 140 | return FIntPoint( 141 | FMath::Clamp(FMath::FloorToInt(LayerPosition.Y) + LayerToTextureShift, 0, CachedFogLayerResolution - 1), 142 | FMath::Clamp(LayerToTextureShift - FMath::CeilToInt(LayerPosition.X), 0, CachedFogLayerResolution - 1)); 143 | } 144 | 145 | int32 AVaFogBoundsVolume::ScaleDistanceToLayer(const int32 Distance) const 146 | { 147 | // For now we assume that volume is square and use X only 148 | return Distance / VolumeTransform.GetScale3D().X; 149 | } 150 | 151 | void AVaFogBoundsVolume::DrawDebugGrid() 152 | { 153 | FVector LayerPosition = FVector::ZeroVector; 154 | int32 LocationZ = GetActorLocation().Z; 155 | 156 | for (int32 i = 0; i < CachedFogLayerResolution; i++) 157 | { 158 | for (int32 j = 0; j < CachedFogLayerResolution; j++) 159 | { 160 | LayerPosition.X = i - LayerToTextureShift; 161 | LayerPosition.Y = j - LayerToTextureShift + 1; 162 | LayerPosition.Z = 0; 163 | 164 | LayerPosition = VolumeTransform.TransformPosition(LayerPosition); 165 | LayerPosition += GetCellExtent(); 166 | LayerPosition.Z = LocationZ; 167 | 168 | DrawDebugBox(GetWorld(), LayerPosition, GetCellExtent() * 0.95f, GetTransform().GetRotation(), FColor::Green, false, DebugTime); 169 | } 170 | } 171 | } 172 | 173 | FVector AVaFogBoundsVolume::SnapWorldToGrid(const FVector& InLocation) const 174 | { 175 | FIntPoint GridPoint = TransformWorldToLayer(InLocation) + FIntPoint(-LayerToTextureShift, 0); 176 | return VolumeTransform.TransformPosition(FVector(LayerToTextureShift - GridPoint.Y, GridPoint.X, 0.f)); 177 | } 178 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogController.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogController.h" 4 | 5 | #include "VaFogAgentComponent.h" 6 | #include "VaFogBoundsVolume.h" 7 | #include "VaFogDefines.h" 8 | #include "VaFogLayer.h" 9 | #include "VaFogOfWar.h" 10 | 11 | UVaFogController::UVaFogController(const FObjectInitializer& ObjectInitializer) 12 | : Super(ObjectInitializer) 13 | { 14 | } 15 | 16 | UVaFogController* UVaFogController::Get(const UObject* WorldContextObject, EGetWorldErrorMode ErrorMode) 17 | { 18 | if (UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject, ErrorMode)) 19 | { 20 | return FVaFogOfWarModule::Get().GetFogController(World); 21 | } 22 | 23 | return nullptr; 24 | } 25 | 26 | void UVaFogController::OnFogLayerAdded(AVaFogLayer* InFogLayer) 27 | { 28 | FogLayers.AddUnique(InFogLayer); 29 | 30 | UE_LOG(LogVaFog, Log, TEXT("[%s] Added: %s %d"), *VA_FUNC_LINE, *InFogLayer->GetName(), (int32)InFogLayer->LayerChannel); 31 | } 32 | 33 | void UVaFogController::OnFogLayerRemoved(AVaFogLayer* InFogLayer) 34 | { 35 | int32 RemovedLayersNum = FogLayers.Remove(InFogLayer); 36 | if (RemovedLayersNum == 0) 37 | { 38 | UE_LOG(LogVaFog, Error, TEXT("[%s] No cached data found for: %s"), *VA_FUNC_LINE, *InFogLayer->GetName()); 39 | } 40 | 41 | UE_LOG(LogVaFog, Log, TEXT("[%s] Removed: %s"), *VA_FUNC_LINE, *InFogLayer->GetName()); 42 | } 43 | 44 | void UVaFogController::OnFogAgentAdded(UVaFogAgentComponent* InFogAgent) 45 | { 46 | for (auto TargetChannel : InFogAgent->TargetChannels) 47 | { 48 | if (auto TargetFogLayer = GetFogLayer(TargetChannel)) 49 | { 50 | TargetFogLayer->AddFogAgent(InFogAgent); 51 | 52 | UE_LOG(LogVaFog, Log, TEXT("[%s] Added: %s"), *VA_FUNC_LINE, *InFogAgent->GetName()); 53 | } 54 | else 55 | { 56 | UE_LOG(LogVaFog, Warning, TEXT("[%s] No suitable fog layer found for: %s (%d)"), *VA_FUNC_LINE, *InFogAgent->GetName(), (int32)TargetChannel); 57 | } 58 | } 59 | } 60 | 61 | void UVaFogController::OnFogAgentRemoved(UVaFogAgentComponent* InFogAgent) 62 | { 63 | for (auto TargetChannel : InFogAgent->TargetChannels) 64 | { 65 | if (auto TargetFogLayer = GetFogLayer(TargetChannel)) 66 | { 67 | TargetFogLayer->RemoveFogAgent(InFogAgent); 68 | 69 | UE_LOG(LogVaFog, Log, TEXT("[%s] Removed: %s"), *VA_FUNC_LINE, *InFogAgent->GetName()); 70 | } 71 | } 72 | } 73 | 74 | AVaFogLayer* UVaFogController::GetFogLayer(EVaFogLayerChannel LayerChannel) const 75 | { 76 | auto FogLayerPtr = FogLayers.FindByPredicate([LayerChannel](const TWeakObjectPtr InLayer) 77 | { return (InLayer.IsValid()) ? (InLayer.Get()->LayerChannel == LayerChannel) : false; }); 78 | 79 | return (FogLayerPtr && (*FogLayerPtr).IsValid()) ? ((*FogLayerPtr).Get()) : nullptr; 80 | } 81 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogDefines.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Logging/LogCategory.h" 7 | #include "Logging/LogMacros.h" 8 | #include "Logging/LogVerbosity.h" 9 | 10 | DECLARE_STATS_GROUP(TEXT("VA Fog of War"), STATGROUP_VaFog, STATCAT_Advanced); 11 | 12 | DECLARE_LOG_CATEGORY_EXTERN(LogVaFog, Log, All); 13 | 14 | #define VA_FUNC (FString(__FUNCTION__)) // Current Class Name + Function Name where this is called 15 | #define VA_LINE (FString::FromInt(__LINE__)) // Current Line Number in the code where this is called 16 | #define VA_FUNC_LINE (VA_FUNC + "(" + VA_LINE + ")") // Current Class and Line Number where this is called! 17 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogLayer.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogLayer.h" 4 | 5 | #include "VaFogAgentComponent.h" 6 | #include "VaFogBlockingVolume.h" 7 | #include "VaFogBoundsVolume.h" 8 | #include "VaFogController.h" 9 | #include "VaFogDefines.h" 10 | #include "VaFogLibrary.h" 11 | #include "VaFogOfWar.h" 12 | #include "VaFogSettings.h" 13 | #include "VaFogTerrainLayer.h" 14 | 15 | #include "Components/BillboardComponent.h" 16 | #include "DrawDebugHelpers.h" 17 | #include "RenderingThread.h" 18 | #include "Engine/Texture2D.h" 19 | #include "Engine/TextureRenderTarget2D.h" 20 | #include "Materials/Material.h" 21 | #include "Rendering/Texture2DResource.h" 22 | #include "UObject/ConstructorHelpers.h" 23 | 24 | #include 25 | #include 26 | #include 27 | 28 | DECLARE_CYCLE_STAT(TEXT("UpdateAgents"), STAT_UpdateAgents, STATGROUP_VaFog); 29 | DECLARE_CYCLE_STAT(TEXT("UpdateBlockingVolumes"), STAT_UpdateBlockingVolumes, STATGROUP_VaFog); 30 | DECLARE_CYCLE_STAT(TEXT("UpdateUpscaleBuffer"), STAT_UpdateUpscaleBuffer, STATGROUP_VaFog); 31 | DECLARE_CYCLE_STAT(TEXT("FetchTexelFromSource"), STAT_FetchTexelFromSource, STATGROUP_VaFog); 32 | DECLARE_CYCLE_STAT(TEXT("DrawVisionCircle"), STAT_DrawVisionCircle, STATGROUP_VaFog); 33 | DECLARE_CYCLE_STAT(TEXT("DrawFieldOfView"), STAT_DrawFieldOfView, STATGROUP_VaFog); 34 | DECLARE_CYCLE_STAT(TEXT("DrawCircle"), STAT_DrawCircle, STATGROUP_VaFog); 35 | DECLARE_CYCLE_STAT(TEXT("Plot4Points"), STAT_Plot4Points, STATGROUP_VaFog); 36 | DECLARE_CYCLE_STAT(TEXT("DrawHorizontalLine"), STAT_DrawHorizontalLine, STATGROUP_VaFog); 37 | 38 | namespace std 39 | { 40 | template <> 41 | struct hash 42 | { 43 | size_t 44 | operator()(const FFogTexel2x2& obj) const 45 | { 46 | return hash()(obj.p11 * 1000 + obj.p12 * 100 + obj.p21 * 10 + obj.p22); 47 | } 48 | }; 49 | } // namespace std 50 | 51 | // clang-format off 52 | static const std::unordered_map UpscaleTemplate = {{ 53 | // 1 54 | { { 0x00, 0x00, 0x00, 0x00 }, {{ {0x00, 0x00, 0x00, 0x00}, 55 | {0x00, 0x00, 0x00, 0x00}, 56 | {0x00, 0x00, 0x00, 0x00}, 57 | {0x00, 0x00, 0x00, 0x00} }} }, 58 | // 2 59 | { { 0x00, 0xFF, 0x00, 0x00 }, {{ {0x00, 0x00, 0x80, 0xFF}, 60 | {0x00, 0x00, 0x00, 0x80}, 61 | {0x00, 0x00, 0x00, 0x00}, 62 | {0x00, 0x00, 0x00, 0x00} }} }, 63 | // 3 64 | { { 0x00, 0x00, 0xFF, 0x00 }, {{ {0x00, 0x00, 0x00, 0x00}, 65 | {0x00, 0x00, 0x00, 0x00}, 66 | {0x80, 0x00, 0x00, 0x00}, 67 | {0xFF, 0x80, 0x00, 0x00} }} }, 68 | // 4 69 | { { 0x00, 0xFF, 0xFF, 0x00 }, {{ {0x00, 0x00, 0x80, 0xFF}, 70 | {0x00, 0x00, 0x00, 0x80}, 71 | {0x80, 0x00, 0x00, 0x00}, 72 | {0xFF, 0x80, 0x00, 0x00} }} }, 73 | // 5 74 | { { 0x00, 0x00, 0x00, 0xFF }, {{ {0x00, 0x00, 0x00, 0x00}, 75 | {0x00, 0x00, 0x00, 0x00}, 76 | {0x00, 0x00, 0x00, 0x80}, 77 | {0x00, 0x00, 0x80, 0xFF} }} }, 78 | // 6 79 | { { 0x00, 0xFF, 0x00, 0xFF }, {{ {0x00, 0x00, 0xFF, 0xFF}, 80 | {0x00, 0x00, 0xFF, 0xFF}, 81 | {0x00, 0x00, 0xFF, 0xFF}, 82 | {0x00, 0x00, 0xFF, 0xFF} }} }, 83 | // 7 84 | { { 0x00, 0x00, 0xFF, 0xFF }, {{ {0x00, 0x00, 0x00, 0x00}, 85 | {0x00, 0x00, 0x00, 0x00}, 86 | {0xFF, 0xFF, 0xFF, 0xFF}, 87 | {0xFF, 0xFF, 0xFF, 0xFF} }} }, 88 | // 8 89 | { { 0x00, 0xFF, 0xFF, 0xFF }, {{ {0x00, 0x80, 0xFF, 0xFF}, 90 | {0x80, 0xFF, 0xFF, 0xFF}, 91 | {0xFF, 0xFF, 0xFF, 0xFF}, 92 | {0xFF, 0xFF, 0xFF, 0xFF} }} }, 93 | // 9 94 | { { 0xFF, 0x00, 0x00, 0x00 }, {{ {0xFF, 0x80, 0x00, 0x00}, 95 | {0x80, 0x00, 0x00, 0x00}, 96 | {0x00, 0x00, 0x00, 0x00}, 97 | {0x00, 0x00, 0x00, 0x00} }} }, 98 | // 10 99 | { { 0xFF, 0xFF, 0x00, 0x00 }, {{ {0xFF, 0xFF, 0xFF, 0xFF}, 100 | {0xFF, 0xFF, 0xFF, 0xFF}, 101 | {0x00, 0x00, 0x00, 0x00}, 102 | {0x00, 0x00, 0x00, 0x00} }} }, 103 | // 11 104 | { { 0xFF, 0x00, 0xFF, 0x00 }, {{ {0xFF, 0xFF, 0x00, 0x00}, 105 | {0xFF, 0xFF, 0x00, 0x00}, 106 | {0xFF, 0xFF, 0x00, 0x00}, 107 | {0xFF, 0xFF, 0x00, 0x00} }} }, 108 | // 12 109 | { { 0xFF, 0xFF, 0xFF, 0x00 }, {{ {0xFF, 0xFF, 0xFF, 0xFF}, 110 | {0xFF, 0xFF, 0xFF, 0xFF}, 111 | {0xFF, 0xFF, 0xFF, 0x80}, 112 | {0xFF, 0xFF, 0x80, 0x00} }} }, 113 | // 14 114 | { { 0xFF, 0x00, 0x00, 0xFF }, {{ {0xFF, 0x80, 0x00, 0x00}, 115 | {0x80, 0x00, 0x00, 0x00}, 116 | {0x00, 0x00, 0x00, 0x80}, 117 | {0x00, 0x00, 0x80, 0xFF} }} }, 118 | // 14 119 | { { 0xFF, 0xFF, 0x00, 0xFF }, {{ {0xFF, 0xFF, 0xFF, 0xFF}, 120 | {0xFF, 0xFF, 0xFF, 0xFF}, 121 | {0x80, 0xFF, 0xFF, 0xFF}, 122 | {0x00, 0x80, 0xFF, 0xFF} }} }, 123 | // 15 124 | { { 0xFF, 0x00, 0xFF, 0xFF }, {{ {0xFF, 0xFF, 0x80, 0x00}, 125 | {0xFF, 0xFF, 0xFF, 0x80}, 126 | {0xFF, 0xFF, 0xFF, 0xFF}, 127 | {0xFF, 0xFF, 0xFF, 0xFF} }} }, 128 | // 16 129 | { { 0xFF, 0xFF, 0xFF, 0xFF }, {{ {0xFF, 0xFF, 0xFF, 0xFF}, 130 | {0xFF, 0xFF, 0xFF, 0xFF}, 131 | {0xFF, 0xFF, 0xFF, 0xFF}, 132 | {0xFF, 0xFF, 0xFF, 0xFF} }} }, 133 | }}; 134 | 135 | static const std::vector OctantTransforms = { 136 | { 1, 0, 0, 1 }, 137 | { 1, 0, 0, -1 }, 138 | {-1, 0, 0, 1 }, 139 | {-1, 0, 0, -1 }, 140 | { 0, 1, 1, 0 }, 141 | { 0, 1, -1, 0 }, 142 | { 0, -1, 1, 0 }, 143 | { 0, -1, -1, 0 } 144 | }; 145 | // clang-format on 146 | 147 | AVaFogLayer::AVaFogLayer(const FObjectInitializer& ObjectInitializer) 148 | : Super(ObjectInitializer) 149 | { 150 | USceneComponent* SceneComponent = CreateDefaultSubobject(TEXT("RootComponent0")); 151 | RootComponent = SceneComponent; 152 | RootComponent->Mobility = EComponentMobility::Static; 153 | 154 | #if WITH_EDITORONLY_DATA 155 | SpriteComponent = CreateEditorOnlyDefaultSubobject(TEXT("Sprite")); 156 | 157 | if (!IsRunningCommandlet() && (SpriteComponent != nullptr)) 158 | { 159 | // Structure to hold one-time initialization 160 | struct FConstructorStatics 161 | { 162 | ConstructorHelpers::FObjectFinderOptional TextRenderTexture; 163 | FConstructorStatics() 164 | : TextRenderTexture(TEXT("/Engine/EditorResources/S_ExpoHeightFog")) 165 | { 166 | } 167 | }; 168 | static FConstructorStatics ConstructorStatics; 169 | 170 | SpriteComponent->Sprite = ConstructorStatics.TextRenderTexture.Get(); 171 | SpriteComponent->SetRelativeScale3D(FVector(0.5f, 0.5f, 0.5f)); 172 | SpriteComponent->SetupAttachment(RootComponent); 173 | SpriteComponent->bIsScreenSizeScaled = true; 174 | SpriteComponent->SetUsingAbsoluteScale(true); 175 | SpriteComponent->bReceivesDecals = false; 176 | } 177 | #endif 178 | 179 | PrimaryActorTick.bCanEverTick = true; 180 | PrimaryActorTick.TickGroup = TG_DuringPhysics; 181 | 182 | LayerChannel = EVaFogLayerChannel::Permanent; 183 | bUseUpscaleBuffer = true; 184 | ZeroBufferValue = 0x00; 185 | 186 | SourceBuffer = nullptr; 187 | UpscaleBuffer = nullptr; 188 | TerrainBuffer = nullptr; 189 | 190 | SourceW = 0; 191 | SourceH = 0; 192 | SourceBufferLength = 0; 193 | 194 | UpscaleW = 0; 195 | UpscaleH = 0; 196 | UpscaleBufferLength = 0; 197 | 198 | bDebugAgents = false; 199 | DebugAgentsColor = FColor::Red; 200 | bDebugBuffers = false; 201 | } 202 | 203 | void AVaFogLayer::PostLoad() 204 | { 205 | InitInternalBuffers(); 206 | 207 | Super::PostLoad(); 208 | } 209 | 210 | void AVaFogLayer::PostActorCreated() 211 | { 212 | InitInternalBuffers(); 213 | 214 | Super::PostActorCreated(); 215 | } 216 | 217 | void AVaFogLayer::PostInitializeComponents() 218 | { 219 | Super::PostInitializeComponents(); 220 | 221 | if (UVaFogController::Get(this, EGetWorldErrorMode::LogAndReturnNull)) 222 | { 223 | UVaFogController::Get(this)->OnFogLayerAdded(this); 224 | } 225 | } 226 | 227 | void AVaFogLayer::Destroyed() 228 | { 229 | if (UVaFogController::Get(this, EGetWorldErrorMode::LogAndReturnNull)) 230 | { 231 | UVaFogController::Get(this)->OnFogLayerRemoved(this); 232 | } 233 | 234 | CleanupInternalBuffers(); 235 | 236 | Super::Destroyed(); 237 | } 238 | 239 | void AVaFogLayer::BeginPlay() 240 | { 241 | UpdateLayer(true); 242 | 243 | // Cache terrain buffer as pointer for fast access or create empty one 244 | if (TerrainLayer) 245 | { 246 | TerrainBuffer = TerrainLayer->SourceBuffer; 247 | } 248 | else if (LayerChannel != EVaFogLayerChannel::Terrain) 249 | { 250 | UE_LOG(LogVaFog, Warning, TEXT("[%s] No Terrain layer found"), *VA_FUNC_LINE); 251 | } 252 | 253 | Super::BeginPlay(); 254 | } 255 | 256 | void AVaFogLayer::InitInternalBuffers() 257 | { 258 | // Force cleanup for re-init case 259 | CleanupInternalBuffers(); 260 | 261 | // Prepare radius strategies 262 | RadiusStrategies.Reserve(static_cast(EVaFogRadiusStrategy::Max)); 263 | RadiusStrategies.Emplace(EVaFogRadiusStrategy::Circle, MakeShared()); 264 | RadiusStrategies.Emplace(EVaFogRadiusStrategy::Square, MakeShared()); 265 | RadiusStrategies.Emplace(EVaFogRadiusStrategy::SquareStepped, MakeShared()); 266 | 267 | // Cache texture size values 268 | int32 CachedTextureResolution = FVaFogOfWarModule::Get().GetSettings()->FogLayerResolution; 269 | check(FMath::IsPowerOfTwo(CachedTextureResolution)); 270 | int32 CachedUpscaleResolution = CachedTextureResolution * 4; 271 | 272 | // Create texture buffer and initialize it 273 | check(!SourceBuffer); 274 | SourceW = CachedTextureResolution; 275 | SourceH = CachedTextureResolution; 276 | SourceBuffer = new uint8[SourceW * SourceH]; 277 | SourceBufferLength = SourceW * SourceH * sizeof(uint8); 278 | FMemory::Memset(SourceBuffer, ZeroBufferValue, SourceBufferLength); 279 | 280 | if (bUseUpscaleBuffer) 281 | { 282 | // Create texture buffer for upscaled texture and initialize it 283 | check(!UpscaleBuffer); 284 | UpscaleW = CachedUpscaleResolution; 285 | UpscaleH = CachedUpscaleResolution; 286 | UpscaleBuffer = new uint8[UpscaleW * UpscaleH]; 287 | UpscaleBufferLength = UpscaleW * UpscaleH * sizeof(uint8); 288 | FMemory::Memset(UpscaleBuffer, ZeroBufferValue, UpscaleBufferLength); 289 | } 290 | 291 | // Prepare debug textures if required 292 | if (bDebugBuffers) 293 | { 294 | SourceUpdateRegion = FUpdateTextureRegion2D(0, 0, 0, 0, SourceW, SourceH); 295 | SourceTexture = UTexture2D::CreateTransient(SourceW, SourceH, EPixelFormat::PF_G8); 296 | SourceTexture->CompressionSettings = TextureCompressionSettings::TC_Grayscale; 297 | SourceTexture->SRGB = false; 298 | SourceTexture->Filter = TextureFilter::TF_Nearest; 299 | SourceTexture->AddressX = TextureAddress::TA_Clamp; 300 | SourceTexture->AddressY = TextureAddress::TA_Clamp; 301 | SourceTexture->UpdateResource(); 302 | } 303 | 304 | if (bUseUpscaleBuffer) 305 | { 306 | // Upscale texture is the one we export to user 307 | UpscaleUpdateRegion = FUpdateTextureRegion2D(0, 0, 0, 0, UpscaleW, UpscaleH); 308 | UpscaleTexture = UTexture2D::CreateTransient(UpscaleW, UpscaleH, EPixelFormat::PF_G8); 309 | UpscaleTexture->CompressionSettings = TextureCompressionSettings::TC_Grayscale; 310 | UpscaleTexture->SRGB = false; 311 | UpscaleTexture->Filter = TextureFilter::TF_Nearest; 312 | UpscaleTexture->AddressX = TextureAddress::TA_Clamp; 313 | UpscaleTexture->AddressY = TextureAddress::TA_Clamp; 314 | UpscaleTexture->UpdateResource(); 315 | } 316 | } 317 | 318 | void AVaFogLayer::CleanupInternalBuffers() 319 | { 320 | if (SourceTexture) 321 | { 322 | SourceTexture = nullptr; 323 | } 324 | 325 | if (UpscaleTexture) 326 | { 327 | SourceTexture = nullptr; 328 | } 329 | 330 | if (SourceBuffer) 331 | { 332 | delete[] SourceBuffer; 333 | SourceBuffer = nullptr; 334 | } 335 | 336 | if (UpscaleBuffer) 337 | { 338 | delete[] UpscaleBuffer; 339 | UpscaleBuffer = nullptr; 340 | } 341 | } 342 | 343 | void AVaFogLayer::Tick(float DeltaTime) 344 | { 345 | Super::Tick(DeltaTime); 346 | 347 | // Cleanup buffer for scouting 348 | if (LayerChannel == EVaFogLayerChannel::Scouting) 349 | { 350 | FMemory::Memset(SourceBuffer, ZeroBufferValue, SourceBufferLength); 351 | } 352 | 353 | UpdateLayer(); 354 | UpdateBuffers(); 355 | } 356 | 357 | void AVaFogLayer::UpdateLayer(bool bForceFullUpdate) 358 | { 359 | UpdateBlockingVolumes(); 360 | UpdateAgents(); 361 | } 362 | 363 | void AVaFogLayer::UpdateBuffers() 364 | { 365 | if (bDebugBuffers) 366 | { 367 | UpdateTextureFromBuffer(SourceTexture, SourceBuffer, SourceBufferLength, SourceUpdateRegion); 368 | } 369 | 370 | if (bUseUpscaleBuffer) 371 | { 372 | UpdateUpscaleBuffer(); 373 | UpdateTextureFromBuffer(UpscaleTexture, UpscaleBuffer, UpscaleBufferLength, UpscaleUpdateRegion); 374 | } 375 | } 376 | 377 | void AVaFogLayer::UpdateAgents() 378 | { 379 | SCOPE_CYCLE_COUNTER(STAT_UpdateAgents); 380 | 381 | if (!BoundsVolume) 382 | { 383 | UE_LOG(LogVaFog, Warning, TEXT("[%s] No bounds volume is set"), *VA_FUNC_LINE); 384 | return; 385 | } 386 | 387 | for (auto FogAgent : FogAgents) 388 | { 389 | FIntPoint AgentLocation = BoundsVolume->TransformWorldToLayer(FogAgent->GetComponentTransform().GetLocation()); 390 | // UE_LOG(LogVaFog, Warning, TEXT("[%s] Agent [%s] location: %s"), *VA_FUNC_LINE, *FogAgent->GetName(), *AgentLocation.ToString()); 391 | 392 | if (bDebugAgents) 393 | { 394 | DrawDebugSphere(GetWorld(), FogAgent->GetComponentTransform().GetLocation(), FogAgent->VisionRadius, 32, DebugAgentsColor, false, 0.0f); 395 | } 396 | 397 | check(FogAgent->VisionRadius >= 0); 398 | 399 | FFogDrawContext DrawContext; 400 | DrawContext.TargetBuffer = SourceBuffer; 401 | DrawContext.CenterX = AgentLocation.X; 402 | DrawContext.CenterY = AgentLocation.Y; 403 | DrawContext.Radius = BoundsVolume->ScaleDistanceToLayer(FogAgent->VisionRadius); 404 | DrawContext.RadiusStrategy = FogAgent->RadiusStrategy; 405 | 406 | if (LayerChannel == EVaFogLayerChannel::Terrain) 407 | { 408 | DrawContext.HeightLevel = EVaFogHeightLevel(static_cast(FogAgent->HeightLevel) << 1); 409 | DrawContext.RevealLevel = static_cast(FogAgent->HeightLevel) << 1; 410 | } 411 | else 412 | { 413 | DrawContext.HeightLevel = FogAgent->HeightLevel; 414 | DrawContext.RevealLevel = 0xFF; 415 | } 416 | 417 | DrawVisionCircle(DrawContext); 418 | } 419 | } 420 | 421 | void AVaFogLayer::UpdateBlockingVolumes() 422 | { 423 | SCOPE_CYCLE_COUNTER(STAT_UpdateBlockingVolumes); 424 | 425 | // Flush all debug 426 | FlushPersistentDebugLines(GetWorld()); 427 | 428 | if (!BoundsVolume) 429 | { 430 | UE_LOG(LogVaFog, Warning, TEXT("[%s] No bounds volume is set"), *VA_FUNC_LINE); 431 | return; 432 | } 433 | 434 | float BoundsCellSizeX = BoundsVolume->GetCellExtent().X * 2; 435 | float BoundsCellSizeY = BoundsVolume->GetCellExtent().Y * 2; 436 | 437 | FFogDrawContext DrawContext; 438 | DrawContext.TargetBuffer = SourceBuffer; 439 | 440 | FVector PointLocation = FVector::ZeroVector; 441 | FIntPoint AgentLocation(0, 0); 442 | 443 | for (auto BlockingVolumePtr : FogBlockingVolumes) 444 | { 445 | auto BlockingVolume = BlockingVolumePtr.IsValid() ? BlockingVolumePtr.Get() : nullptr; 446 | if (!BlockingVolume) 447 | { 448 | UE_LOG(LogVaFog, Error, TEXT("[%s] BlockingVolume is nullptr: cleanup layer volumes list!"), *VA_FUNC_LINE); 449 | continue; 450 | } 451 | 452 | FVector VolumeOrigin = FVector::ZeroVector; 453 | FVector VolumeExtent = FVector::ZeroVector; 454 | BlockingVolume->GetActorBounds(false, VolumeOrigin, VolumeExtent); 455 | DrawContext.RevealLevel = static_cast(BlockingVolume->HeightLevel); 456 | 457 | // Snap to bounds grid now 458 | FVector OriginSnapped = BoundsVolume->SnapWorldToGrid(VolumeOrigin); 459 | 460 | // if (BlockingVolume->bDebugVolume) 461 | //{ 462 | // DrawDebugSphere(GetWorld(), VolumeOrigin, 100.f, 30, FColor::Red, true); 463 | // DrawDebugSphere(GetWorld(), BoundsVolume->SnapWorldToGrid(VolumeOrigin), 100.f, 30, FColor::Green, true); 464 | // } 465 | 466 | // It's not perfect match but should work 467 | int32 LineXSize = FMath::CeilToInt((VolumeExtent.X * 2.f) / BoundsCellSizeX); 468 | int32 LineYSize = FMath::CeilToInt((VolumeExtent.Y * 2.f) / BoundsCellSizeY); 469 | 470 | // Minor asjust 471 | if ((OriginSnapped.X - VolumeOrigin.X) / BoundsVolume->GetCellExtent().X >= 1.f) 472 | { 473 | LineXSize += 1; 474 | } 475 | 476 | if ((OriginSnapped.Y - VolumeOrigin.Y) / BoundsVolume->GetCellExtent().Y >= 1.f) 477 | { 478 | LineYSize += 1; 479 | } 480 | 481 | int32 LineXSizeHalf = FMath::FloorToInt(LineXSize / 2.f); 482 | int32 LineYSizeHalf = FMath::FloorToInt(LineYSize / 2.f); 483 | 484 | for (int32 i = 0; i < LineXSize; i++) 485 | { 486 | for (int32 j = 0; j < LineYSize; j++) 487 | { 488 | PointLocation = OriginSnapped + FVector((i - LineXSizeHalf - 1) * BoundsCellSizeX, (j - LineYSizeHalf) * BoundsCellSizeY, 0.f) + BoundsVolume->GetCellExtent(); 489 | 490 | if (BlockingVolume->EncompassesPoint(PointLocation)) 491 | { 492 | AgentLocation = BoundsVolume->TransformWorldToLayer(PointLocation); 493 | Reveal(DrawContext, AgentLocation.X, AgentLocation.Y); 494 | 495 | if (BlockingVolume->bDebugVolume) 496 | { 497 | DrawDebugBox(GetWorld(), PointLocation, BoundsVolume->GetCellExtent() * 0.95f, BoundsVolume->GetTransform().GetRotation(), UVaFogLibrary::GetDebugColorForHeightLevel(BlockingVolume->HeightLevel), true); 498 | } 499 | } 500 | else if (BlockingVolume->bDebugVolume) 501 | { 502 | DrawDebugBox(GetWorld(), PointLocation, BoundsVolume->GetCellExtent() * 0.8f, BoundsVolume->GetTransform().GetRotation(), FColor::White, true); 503 | } 504 | } 505 | } 506 | } 507 | } 508 | 509 | void AVaFogLayer::UpdateObstacle(UVaFogAgentComponent* FogAgent, bool bObstacleIsActive) 510 | { 511 | check(FogAgent); 512 | check(FogAgent->VisionRadius >= 0); 513 | 514 | if (!BoundsVolume) 515 | { 516 | UE_LOG(LogVaFog, Warning, TEXT("[%s] No bounds volume is set"), *VA_FUNC_LINE); 517 | return; 518 | } 519 | 520 | FIntPoint AgentLocation = BoundsVolume->TransformWorldToLayer(FogAgent->GetComponentTransform().GetLocation()); 521 | 522 | FFogDrawContext DrawContext; 523 | DrawContext.TargetBuffer = SourceBuffer; 524 | DrawContext.CenterX = AgentLocation.X; 525 | DrawContext.CenterY = AgentLocation.Y; 526 | DrawContext.Radius = BoundsVolume->ScaleDistanceToLayer(FogAgent->VisionRadius); 527 | DrawContext.RadiusStrategy = FogAgent->RadiusStrategy; 528 | DrawContext.HeightLevel = EVaFogHeightLevel(static_cast(FogAgent->HeightLevel) << 1); 529 | DrawContext.RevealLevel = (bObstacleIsActive) ? (static_cast(FogAgent->HeightLevel) << 1) : (static_cast(FogAgent->HeightLevel)); 530 | 531 | DrawVisionCircle(DrawContext); 532 | } 533 | 534 | void AVaFogLayer::UpdateUpscaleBuffer() 535 | { 536 | SCOPE_CYCLE_COUNTER(STAT_UpdateUpscaleBuffer); 537 | 538 | FFogTexel2x2 SourceTexel; 539 | FFogTexel4x4 UpscaleTexel; 540 | 541 | for (int32 x = 0; x < SourceW; ++x) 542 | { 543 | for (int32 y = 0; y < SourceH; ++y) 544 | { 545 | // Fetch original texture pixel and its neighbors 546 | SourceTexel = FetchTexelFromSource(x, y); 547 | UpscaleTexel = UpscaleTemplate.at(SourceTexel); 548 | 549 | // Apply texel to upscale buffer based on template 550 | for (int32 i = 0; i < 4; ++i) 551 | { 552 | FMemory::Memcpy(&UpscaleBuffer[(4 * y + i) * UpscaleW + 4 * x], &UpscaleTexel.pixels[i], 4 * sizeof(uint8)); 553 | } 554 | } 555 | } 556 | } 557 | 558 | void AVaFogLayer::DrawVisionCircle(const FFogDrawContext& DrawContext) 559 | { 560 | SCOPE_CYCLE_COUNTER(STAT_DrawVisionCircle); 561 | 562 | // http://www.adammil.net/blog/v125_Roguelike_Vision_Algorithms.html 563 | // Shadow casting (point-to-tile or point-to-point) 564 | // Pros: Fast. Expanding pillar shadows. Expansive walls. Continuous point visibility. 565 | // Cons: Diagonal vision much narrower than cardinal. Blind corners. Beam expands too much through a door. Asymmetrical. Nontrivial to eliminate all artifacts. 566 | 567 | Reveal(DrawContext, DrawContext.CenterX, DrawContext.CenterY); 568 | 569 | // Scan each octant 570 | for (int32 i = 0; i < 8; ++i) 571 | { 572 | DrawFieldOfView(DrawContext, 1, 1.f, 0.f, OctantTransforms[i]); 573 | } 574 | } 575 | 576 | void AVaFogLayer::DrawFieldOfView(const FFogDrawContext& DrawContext, int32 Y, float Start, float End, FFogOctantTransform Transform) 577 | { 578 | if (Start < End) 579 | { 580 | return; 581 | } 582 | 583 | SCOPE_CYCLE_COUNTER(STAT_DrawFieldOfView); 584 | 585 | int32 RadiusSquared = FMath::Square(DrawContext.Radius); 586 | float NewStart = 0.0f; 587 | bool bBlocked = false; 588 | 589 | int32 DeltaY = 0; 590 | int32 CurrentX = 0; 591 | int32 CurrentY = 0; 592 | 593 | // @TODO Make in deterministic https://github.com/ufna/VaFogOfWar/issues/57 594 | float LeftSlope = 0.f; 595 | float RightSlope = 0.f; 596 | 597 | for (int32 Distance = Y; Distance <= DrawContext.Radius && !bBlocked; ++Distance) 598 | { 599 | DeltaY = -Distance; 600 | for (int32 DeltaX = -Distance; DeltaX <= 0; DeltaX++) 601 | { 602 | CurrentX = DrawContext.CenterX + DeltaX * Transform.xx + DeltaY * Transform.xy; 603 | CurrentY = DrawContext.CenterY + DeltaX * Transform.yx + DeltaY * Transform.yy; 604 | LeftSlope = (DeltaX - 0.5f) / (DeltaY + 0.5f); 605 | RightSlope = (DeltaX + 0.5f) / (DeltaY - 0.5f); 606 | 607 | if (!(CurrentX >= 0 && CurrentY >= 0 && CurrentX < SourceW && CurrentY < SourceH) || 608 | Start < RightSlope) 609 | { 610 | continue; 611 | } 612 | else if (End > LeftSlope) 613 | { 614 | break; 615 | } 616 | 617 | // Check if previous cell was a blocking one 618 | if (bBlocked) 619 | { 620 | if (IsBlocked(CurrentX, CurrentY, DrawContext.HeightLevel)) 621 | { 622 | NewStart = RightSlope; 623 | continue; 624 | } 625 | else 626 | { 627 | bBlocked = false; 628 | Start = NewStart; 629 | } 630 | } 631 | else 632 | { 633 | // Hit a wall within sight line 634 | if (IsBlocked(CurrentX, CurrentY, DrawContext.HeightLevel) && Distance < DrawContext.Radius) 635 | { 636 | bBlocked = true; 637 | DrawFieldOfView(DrawContext, Distance + 1, Start, LeftSlope, Transform); 638 | NewStart = RightSlope; 639 | } 640 | else 641 | { 642 | // @TODO Make radius strategies configurable https://github.com/ufna/VaFogOfWar/issues/58 643 | // Check if it's within the lightable area and light if needed 644 | if (RadiusStrategies[DrawContext.RadiusStrategy]->IsInRadius(DrawContext.CenterX, DrawContext.CenterY, DrawContext.Radius, CurrentX, CurrentY)) 645 | { 646 | Reveal(DrawContext, CurrentX, CurrentY); 647 | } 648 | } 649 | } 650 | } 651 | } 652 | } 653 | 654 | void AVaFogLayer::Reveal(const FFogDrawContext& DrawContext, int32 X, int32 Y) 655 | { 656 | check(X >= 0 && X < SourceW && Y >= 0 && Y < SourceH); 657 | DrawContext.TargetBuffer[Y * SourceW + X] = DrawContext.RevealLevel; 658 | } 659 | 660 | bool AVaFogLayer::IsBlocked(int32 X, int32 Y, EVaFogHeightLevel HeightLevel) 661 | { 662 | check(X >= 0 && X < SourceW && Y >= 0 && Y < SourceH); 663 | return (TerrainBuffer) ? (TerrainBuffer[Y * SourceW + X] > static_cast(HeightLevel)) : false; 664 | } 665 | 666 | void AVaFogLayer::DrawCircle(const FFogDrawContext& DrawContext) 667 | { 668 | SCOPE_CYCLE_COUNTER(STAT_DrawCircle); 669 | 670 | if (DrawContext.Radius > SourceW) 671 | { 672 | UE_LOG(LogVaFog, Warning, TEXT("[%s] Vision radius %d is larger than source width %d"), *VA_FUNC_LINE, DrawContext.Radius, SourceW); 673 | } 674 | 675 | int32 RadiusError = -DrawContext.Radius; 676 | int32 X = FMath::Min(DrawContext.Radius, SourceW); 677 | int32 Y = 0; 678 | 679 | while (X >= Y) 680 | { 681 | int lastY = Y; 682 | 683 | RadiusError += Y; 684 | ++Y; 685 | RadiusError += Y; 686 | 687 | Plot4Points(DrawContext, X, lastY); 688 | 689 | if (RadiusError >= 0) 690 | { 691 | if (X != lastY) 692 | Plot4Points(DrawContext, lastY, X); 693 | 694 | RadiusError -= X; 695 | --X; 696 | RadiusError -= X; 697 | } 698 | } 699 | } 700 | 701 | void AVaFogLayer::Plot4Points(const FFogDrawContext& DrawContext, int32 X, int32 Y) 702 | { 703 | SCOPE_CYCLE_COUNTER(STAT_Plot4Points); 704 | 705 | DrawHorizontalLine(DrawContext.TargetBuffer, DrawContext.CenterX - X, DrawContext.CenterY + Y, DrawContext.CenterX + X); 706 | 707 | if (Y != 0) 708 | { 709 | DrawHorizontalLine(DrawContext.TargetBuffer, DrawContext.CenterX - X, DrawContext.CenterY - Y, DrawContext.CenterX + X); 710 | } 711 | } 712 | 713 | void AVaFogLayer::DrawHorizontalLine(uint8* TargetBuffer, int32 x0, int32 y0, int32 x1) 714 | { 715 | if (y0 < 0 || y0 >= SourceH || x0 >= SourceW || x1 < 0) 716 | return; 717 | 718 | SCOPE_CYCLE_COUNTER(STAT_DrawHorizontalLine); 719 | 720 | int32 x0opt = FMath::Max(x0, 0); 721 | int32 x1opt = FMath::Clamp(x1, x0opt, SourceW - 1); 722 | 723 | FMemory::Memset(&TargetBuffer[y0 * SourceW + x0opt], 0xFF, x1opt - x0opt + 1); 724 | } 725 | 726 | FFogTexel2x2 AVaFogLayer::FetchTexelFromSource(int32 W, int32 H) 727 | { 728 | // SCOPE_CYCLE_COUNTER(STAT_FetchTexelFromSource); 729 | 730 | // Clamp neighbor coords if necessary 731 | int32 NeighborW = FMath::Min(W + 1, SourceW - 1); 732 | int32 NeighborH = FMath::Min(H + 1, SourceH - 1); 733 | 734 | FFogTexel2x2 Texel; 735 | Texel.p11 = SourceBuffer[H * SourceW + W]; 736 | Texel.p12 = SourceBuffer[H * SourceW + NeighborW]; 737 | Texel.p21 = SourceBuffer[NeighborH * SourceW + W]; 738 | Texel.p22 = SourceBuffer[NeighborH * SourceW + NeighborW]; 739 | 740 | return Texel; 741 | } 742 | 743 | void AVaFogLayer::AddFogAgent(UVaFogAgentComponent* InFogAgent) 744 | { 745 | FogAgents.AddUnique(InFogAgent); 746 | 747 | OnAddFogAgent(InFogAgent); 748 | } 749 | 750 | void AVaFogLayer::RemoveFogAgent(UVaFogAgentComponent* InFogAgent) 751 | { 752 | int32 NumRemoved = FogAgents.Remove(InFogAgent); 753 | 754 | OnRemoveFogAgent(InFogAgent); 755 | 756 | if (NumRemoved == 0) 757 | { 758 | UE_LOG(LogVaFog, Error, TEXT("[%s] No cached data found for: %s"), *VA_FUNC_LINE, *InFogAgent->GetName()); 759 | } 760 | } 761 | 762 | void AVaFogLayer::AddFogBlockingVolume(AVaFogBlockingVolume* InFogBlockingVolume) 763 | { 764 | FogBlockingVolumes.AddUnique(InFogBlockingVolume); 765 | 766 | FogBlockingVolumes.Sort([](const TWeakObjectPtr& A, const TWeakObjectPtr& B) 767 | { 768 | int32 PriorityA = A.IsValid() ? A.Get()->Priority : -1; 769 | int32 PriorityB = B.IsValid() ? B.Get()->Priority : -1; 770 | return PriorityA <= PriorityB; }); 771 | 772 | OnAddFogBlockingVolume(InFogBlockingVolume); 773 | } 774 | 775 | void AVaFogLayer::RemoveFogBlockingVolume(AVaFogBlockingVolume* InFogBlockingVolume) 776 | { 777 | int32 NumRemoved = FogBlockingVolumes.Remove(InFogBlockingVolume); 778 | 779 | OnRemoveFogBlockingVolume(InFogBlockingVolume); 780 | 781 | if (NumRemoved == 0) 782 | { 783 | UE_LOG(LogVaFog, Error, TEXT("[%s] No cached data found for: %s"), *VA_FUNC_LINE, *InFogBlockingVolume->GetName()); 784 | } 785 | } 786 | 787 | bool AVaFogLayer::IsLocationRevealed(const FVector& InLocation) const 788 | { 789 | if (!BoundsVolume) 790 | { 791 | UE_LOG(LogVaFog, Warning, TEXT("[%s] Fog bounds volume is not registeret, location is not revealed by default"), *VA_FUNC_LINE); 792 | return false; 793 | } 794 | 795 | FIntPoint PointLocation = BoundsVolume->TransformWorldToLayer(InLocation); 796 | 797 | return SourceBuffer[PointLocation.Y * SourceW + PointLocation.X] == 0xFF; 798 | } 799 | 800 | void AVaFogLayer::CoverAll() 801 | { 802 | FMemory::Memset(SourceBuffer, ZeroBufferValue, SourceBufferLength); 803 | } 804 | 805 | void AVaFogLayer::RevealAll() 806 | { 807 | FMemory::Memset(SourceBuffer, 0xFF, SourceBufferLength); 808 | } 809 | 810 | void AVaFogLayer::UpdateTextureFromBuffer(UTexture2D* DestinationTexture, uint8* SrcBuffer, int32 SrcBufferLength, FUpdateTextureRegion2D& UpdateTextureRegion) 811 | { 812 | struct FTextureData 813 | { 814 | FTexture2DResource* Texture2DResource; 815 | FUpdateTextureRegion2D* Region; 816 | uint32 SrcPitch; 817 | uint8* SrcData; 818 | }; 819 | 820 | // Copy original data fro GPU 821 | uint8* Buffer = new uint8[SrcBufferLength]; 822 | FMemory::Memcpy(Buffer, SrcBuffer, SrcBufferLength); 823 | 824 | FTextureData* TextureData = new FTextureData(); 825 | TextureData->Texture2DResource = static_cast(DestinationTexture->GetResource()); 826 | TextureData->SrcPitch = UpdateTextureRegion.Width; 827 | TextureData->SrcData = Buffer; 828 | TextureData->Region = &UpdateTextureRegion; 829 | 830 | ENQUEUE_RENDER_COMMAND(UpdateTexture) 831 | ( 832 | [TextureData](FRHICommandListImmediate& RHICmdList) 833 | { 834 | int32 CurrentFirstMip = TextureData->Texture2DResource->GetCurrentFirstMip(); 835 | if (CurrentFirstMip <= 0) 836 | { 837 | RHIUpdateTexture2D( 838 | TextureData->Texture2DResource->GetTexture2DRHI(), 839 | 0 - CurrentFirstMip, 840 | *TextureData->Region, 841 | TextureData->SrcPitch, 842 | TextureData->SrcData); 843 | } 844 | delete[] TextureData->SrcData; 845 | delete TextureData; 846 | }); 847 | } 848 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogLibrary.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogLibrary.h" 4 | 5 | #include "RHI.h" 6 | 7 | UVaFogLibrary::UVaFogLibrary(const FObjectInitializer& ObjectInitializer) 8 | : Super(ObjectInitializer) 9 | { 10 | } 11 | 12 | bool UVaFogLibrary::IsRHINeedsToSwitchVerticalAxis() 13 | { 14 | // RHINeedsToSwitchVerticalAxis does nt exist anymore 15 | return false; //RHINeedsToSwitchVerticalAxis(GMaxRHIShaderPlatform); 16 | } 17 | 18 | FColor UVaFogLibrary::GetDebugColorForHeightLevel(EVaFogHeightLevel HeightLevel) 19 | { 20 | switch (HeightLevel) 21 | { 22 | case EVaFogHeightLevel::HL_1: 23 | return FColor::Silver; 24 | case EVaFogHeightLevel::HL_2: 25 | return FColor::Cyan; 26 | case EVaFogHeightLevel::HL_3: 27 | return FColor::Emerald; 28 | case EVaFogHeightLevel::HL_4: 29 | return FColor::Yellow; 30 | case EVaFogHeightLevel::HL_5: 31 | return FColor::Orange; 32 | case EVaFogHeightLevel::HL_6: 33 | return FColor::Magenta; 34 | case EVaFogHeightLevel::HL_7: 35 | return FColor::Purple; 36 | case EVaFogHeightLevel::HL_8: 37 | return FColor::Red; 38 | default: 39 | unimplemented(); 40 | } 41 | 42 | return FColor(); 43 | } 44 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogOfWar.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogOfWar.h" 4 | 5 | #include "VaFogController.h" 6 | #include "VaFogDefines.h" 7 | #include "VaFogSettings.h" 8 | 9 | #include "Developer/Settings/Public/ISettingsModule.h" 10 | #include "Engine/World.h" 11 | #include "UObject/Package.h" 12 | 13 | #define LOCTEXT_NAMESPACE "FVaFogOfWarModule" 14 | 15 | void FVaFogOfWarModule::StartupModule() 16 | { 17 | ModuleSettings = NewObject(GetTransientPackage(), "VaFogSettings", RF_Standalone); 18 | ModuleSettings->AddToRoot(); 19 | 20 | // Register settings 21 | if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) 22 | { 23 | SettingsModule->RegisterSettings("Project", "Plugins", "VaFogOfWar", 24 | LOCTEXT("RuntimeSettingsName", "VA Fog of War"), 25 | LOCTEXT("RuntimeSettingsDescription", "Configure VA Fog of War plugin"), 26 | ModuleSettings); 27 | } 28 | 29 | FWorldDelegates::OnPostWorldCleanup.AddLambda([this](UWorld* World, bool bSessionEnded, bool bCleanupResources) 30 | { 31 | FogControllers.Remove(World); 32 | 33 | UE_LOG(LogVaFog, Log, TEXT("[%s] Fog Controller is removed for: %s"), *VA_FUNC_LINE, *World->GetName()); }); 34 | 35 | FWorldDelegates::OnPostWorldInitialization.AddLambda([this](UWorld* World, const UWorld::InitializationValues IVS) 36 | { 37 | auto FogController = NewObject(GetTransientPackage()); 38 | FogController->SetFlags(RF_Standalone); 39 | FogController->AddToRoot(); 40 | 41 | FogControllers.Add(World, FogController); 42 | 43 | UE_LOG(LogVaFog, Log, TEXT("[%s] Fog Controller is created for: %s"), *VA_FUNC_LINE, *World->GetName()); }); 44 | 45 | UE_LOG(LogVaFog, Log, TEXT("[%s] VaFogOfWar module started"), *VA_FUNC_LINE); 46 | } 47 | 48 | void FVaFogOfWarModule::ShutdownModule() 49 | { 50 | if (ISettingsModule* SettingsModule = FModuleManager::GetModulePtr("Settings")) 51 | { 52 | SettingsModule->UnregisterSettings("Project", "Plugins", "VaFogOfWar"); 53 | } 54 | 55 | if (!GExitPurge) 56 | { 57 | // If we're in exit purge, this object has already been destroyed 58 | ModuleSettings->RemoveFromRoot(); 59 | 60 | for (auto FogController : FogControllers) 61 | { 62 | FogController.Value->RemoveFromRoot(); 63 | } 64 | } 65 | else 66 | { 67 | ModuleSettings = nullptr; 68 | } 69 | 70 | FogControllers.Empty(); 71 | } 72 | 73 | UVaFogSettings* FVaFogOfWarModule::GetSettings() const 74 | { 75 | check(ModuleSettings); 76 | return ModuleSettings; 77 | } 78 | 79 | UVaFogController* FVaFogOfWarModule::GetFogController(UWorld* World) const 80 | { 81 | return FogControllers.FindRef(World); 82 | } 83 | 84 | #undef LOCTEXT_NAMESPACE 85 | 86 | IMPLEMENT_MODULE(FVaFogOfWarModule, VaFogOfWar) 87 | 88 | DEFINE_LOG_CATEGORY(LogVaFog); 89 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogRadiusStrategy.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogRadiusStrategy.h" 4 | 5 | FVaFogRadiusStrategy::~FVaFogRadiusStrategy() = default; 6 | 7 | bool FVaFogRadiusStrategy_Circle::IsInRadius(int32 CenterX, int32 CenterY, int32 Radius, int32 X, int32 Y) 8 | { 9 | return ((X - CenterX) * (X - CenterX) + (Y - CenterY) * (Y - CenterY)) < FMath::Square(Radius); 10 | } 11 | 12 | bool FVaFogRadiusStrategy_Square::IsInRadius(int32 CenterX, int32 CenterY, int32 Radius, int32 X, int32 Y) 13 | { 14 | return (FMath::Abs(X - CenterX) < Radius && 15 | FMath::Abs(Y - CenterY) < Radius); 16 | } 17 | 18 | bool FVaFogRadiusStrategy_SquareStepped::IsInRadius(int32 CenterX, int32 CenterY, int32 Radius, int32 X, int32 Y) 19 | { 20 | float HalfRadius = static_cast(Radius) / 2.f; 21 | int32 HalfRadiusX = (X > CenterX) ? FMath::CeilToInt(HalfRadius) : FMath::FloorToInt(HalfRadius); 22 | int32 HalfRadiusY = (Y > CenterY) ? FMath::CeilToInt(HalfRadius) : FMath::FloorToInt(HalfRadius); 23 | 24 | return (FMath::Abs(X - CenterX) < HalfRadiusX && 25 | FMath::Abs(Y - CenterY) < HalfRadiusY); 26 | } 27 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogSettings.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogSettings.h" 4 | 5 | UVaFogSettings::UVaFogSettings(const FObjectInitializer& ObjectInitializer) 6 | : Super(ObjectInitializer) 7 | { 8 | FogLayerResolution = 128; 9 | } 10 | 11 | void UVaFogSettings::PostInitProperties() 12 | { 13 | Super::PostInitProperties(); 14 | 15 | SanatizeFogLayerResolution(); 16 | } 17 | 18 | #if WITH_EDITOR 19 | void UVaFogSettings::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) 20 | { 21 | Super::PostEditChangeProperty(PropertyChangedEvent); 22 | 23 | SanatizeFogLayerResolution(); 24 | 25 | if (PropertyChangedEvent.Property) 26 | { 27 | if (PropertyChangedEvent.Property->GetFName() == GET_MEMBER_NAME_CHECKED(UVaFogSettings, FogLayerResolution) && 28 | PropertyChangedEvent.ChangeType != EPropertyChangeType::Interactive) 29 | { 30 | // @TODO Update in-editor fog layers 31 | } 32 | } 33 | } 34 | #endif 35 | 36 | void UVaFogSettings::SanatizeFogLayerResolution() 37 | { 38 | static const int32 MaxFogLayerResolution = 512; 39 | static const int32 MinFogLayerResolution = 64; 40 | FogLayerResolution = FMath::Clamp(int32(FMath::RoundUpToPowerOfTwo(FogLayerResolution)), MinFogLayerResolution, MaxFogLayerResolution); 41 | } 42 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Private/VaFogTerrainLayer.cpp: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #include "VaFogTerrainLayer.h" 4 | 5 | #include "VaFogBoundsVolume.h" 6 | #include "VaFogController.h" 7 | #include "VaFogDefines.h" 8 | 9 | #include "Components/BillboardComponent.h" 10 | #include "Engine/Texture2D.h" 11 | #include "UObject/ConstructorHelpers.h" 12 | #include "TextureResource.h" 13 | 14 | AVaFogTerrainLayer::AVaFogTerrainLayer(const FObjectInitializer& ObjectInitializer) 15 | : Super(ObjectInitializer) 16 | { 17 | #if WITH_EDITORONLY_DATA 18 | if (SpriteComponent) 19 | { 20 | // Structure to hold one-time initialization 21 | struct FConstructorStatics 22 | { 23 | ConstructorHelpers::FObjectFinderOptional TextRenderTexture; 24 | FConstructorStatics() 25 | : TextRenderTexture(TEXT("/Engine/EditorResources/S_Terrain.S_Terrain")) 26 | { 27 | } 28 | }; 29 | static FConstructorStatics ConstructorStatics; 30 | 31 | SpriteComponent->Sprite = ConstructorStatics.TextRenderTexture.Get(); 32 | } 33 | #endif 34 | 35 | LayerChannel = EVaFogLayerChannel::Terrain; 36 | bUseUpscaleBuffer = false; 37 | ZeroBufferValue = static_cast(EVaFogHeightLevel::HL_1); 38 | 39 | InitialTerrainBuffer = nullptr; 40 | bUpdateRequired = false; 41 | } 42 | 43 | void AVaFogTerrainLayer::InitInternalBuffers() 44 | { 45 | Super::InitInternalBuffers(); 46 | 47 | InitialTerrainBuffer = new uint8[SourceBufferLength]; 48 | FMemory::Memset(InitialTerrainBuffer, ZeroBufferValue, SourceBufferLength); 49 | 50 | LoadTerrainBufferFromTexture(); 51 | 52 | // Apply initial buffer to source 53 | FMemory::Memcpy(SourceBuffer, InitialTerrainBuffer, SourceBufferLength); 54 | } 55 | 56 | void AVaFogTerrainLayer::CleanupInternalBuffers() 57 | { 58 | if (InitialTerrainBuffer) 59 | { 60 | delete[] InitialTerrainBuffer; 61 | InitialTerrainBuffer = nullptr; 62 | } 63 | 64 | Super::CleanupInternalBuffers(); 65 | } 66 | 67 | void AVaFogTerrainLayer::BeginPlay() 68 | { 69 | // Link self buffer as source 70 | TerrainBuffer = SourceBuffer; 71 | 72 | // Apply all terrain blocking volumes into initial buffer 73 | Super::BeginPlay(); 74 | } 75 | 76 | #if WITH_EDITOR 77 | void AVaFogTerrainLayer::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) 78 | { 79 | static const FName NAME_InitialTerrainTexture = FName(TEXT("InitialTerrainTexture")); 80 | 81 | Super::PostEditChangeProperty(PropertyChangedEvent); 82 | 83 | if (PropertyChangedEvent.Property) 84 | { 85 | if (PropertyChangedEvent.Property->GetFName() == NAME_InitialTerrainTexture) 86 | { 87 | CleanupInternalBuffers(); 88 | InitInternalBuffers(); 89 | } 90 | } 91 | } 92 | #endif 93 | 94 | void AVaFogTerrainLayer::UpdateLayer(bool bForceFullUpdate) 95 | { 96 | if (bForceFullUpdate || bUpdateRequired) 97 | { 98 | UpdateBlockingVolumes(); 99 | UpdateAgents(); 100 | 101 | bUpdateRequired = false; 102 | } 103 | } 104 | 105 | EVaFogHeightLevel AVaFogTerrainLayer::GetHeightLevelAtLocation(const FVector& Location) const 106 | { 107 | if (!BoundsVolume) 108 | { 109 | UE_LOG(LogVaFog, Warning, TEXT("[%s] Fog bounds volume is not registered yet, return default height level"), *VA_FUNC_LINE); 110 | return EVaFogHeightLevel::HL_1; 111 | } 112 | 113 | return GetHeightLevelAtAgentLocation(BoundsVolume->TransformWorldToLayer(Location)); 114 | } 115 | 116 | EVaFogHeightLevel AVaFogTerrainLayer::GetHeightLevelAtAgentLocation(const FIntPoint& AgentLocation) const 117 | { 118 | // @TODO Initial values should be valided before use https://github.com/ufna/VaFogOfWar/issues/68 119 | uint8 HeightLevelValue = SourceBuffer[AgentLocation.Y * SourceW + AgentLocation.X]; 120 | return static_cast(FMath::Clamp(FMath::RoundUpToPowerOfTwo(HeightLevelValue), static_cast(EVaFogHeightLevel::HL_1), static_cast(EVaFogHeightLevel::HL_8))); 121 | } 122 | 123 | void AVaFogTerrainLayer::LoadTerrainBufferFromTexture() 124 | { 125 | UE_LOG(LogVaFog, Warning, TEXT("[%s] Update initial terrain buffer"), *VA_FUNC_LINE); 126 | 127 | // Check initial state and load it if necessary 128 | if (InitialTerrainTexture) 129 | { 130 | if (InitialTerrainTexture->GetPlatformData() && InitialTerrainTexture->GetPixelFormat() != EPixelFormat::PF_Unknown) 131 | { 132 | int32 SizeX = InitialTerrainTexture->GetSizeX(); 133 | int32 SizeY = InitialTerrainTexture->GetSizeY(); 134 | EPixelFormat PixelFormat = InitialTerrainTexture->GetPixelFormat(); 135 | int32 BytesPerPixel = (PixelFormat == EPixelFormat::PF_G8) ? sizeof(uint8) : static_cast(PixelFormat); 136 | 137 | // Check that initial texture has right size 138 | if (SizeX == SourceW && SizeY == SourceH) 139 | { 140 | if (BytesPerPixel == sizeof(uint8)) 141 | { 142 | uint8* TextureData = static_cast(InitialTerrainTexture->GetPlatformData()->Mips[0].BulkData.Lock(LOCK_READ_ONLY)); 143 | 144 | if (TextureData) 145 | { 146 | FMemory::Memcpy(InitialTerrainBuffer, TextureData, SourceBufferLength); 147 | } 148 | else 149 | { 150 | UE_LOG(LogVaFog, Error, TEXT("[%s] Layer [%s] Can't lock InitialTerrainTexture to read its pixels: %s"), *VA_FUNC_LINE, *GetName(), *InitialTerrainTexture->GetName()); 151 | } 152 | 153 | InitialTerrainTexture->GetPlatformData()->Mips[0].BulkData.Unlock(); 154 | } 155 | else 156 | { 157 | UE_LOG(LogVaFog, Warning, TEXT("[%s] Layer [%s] InitialTerrainTexture has wrong BytesPerPixel: %d, expected: %d"), *VA_FUNC_LINE, *GetName(), BytesPerPixel, sizeof(uint8)); 158 | } 159 | } 160 | else 161 | { 162 | UE_LOG(LogVaFog, Warning, TEXT("[%s] Layer [%s] InitialTerrainTexture has wrong size: %d x %d, expected: %d x %d"), *VA_FUNC_LINE, *GetName(), SizeX, SizeY, SourceW, SourceH); 163 | } 164 | } 165 | else 166 | { 167 | #if WITH_EDITORONLY_DATA 168 | UE_LOG(LogVaFog, Warning, TEXT("[%s] Layer [%s] Can't load PlatformData, try to process Source texture"), *VA_FUNC_LINE, *GetName()); 169 | 170 | int32 SizeX = InitialTerrainTexture->Source.GetSizeX(); 171 | int32 SizeY = InitialTerrainTexture->Source.GetSizeY(); 172 | int32 BytesPerPixel = InitialTerrainTexture->Source.GetBytesPerPixel(); 173 | 174 | if (SizeX == SourceW && SizeY == SourceH) 175 | { 176 | uint8* TextureData = static_cast(InitialTerrainTexture->Source.LockMip(0)); 177 | if (TextureData) 178 | { 179 | // Load each forth pixel (r channel) 180 | for (int x = 0; x < SourceW; ++x) 181 | { 182 | for (int y = 0; y < SourceH; ++y) 183 | { 184 | InitialTerrainBuffer[y * SourceW + x] = TextureData[y * SourceW * BytesPerPixel + x * BytesPerPixel]; 185 | } 186 | } 187 | 188 | UE_LOG(LogVaFog, Warning, TEXT("[%s] Layer [%s] Initial terrain buffer successfully loaded from Source texture"), *VA_FUNC_LINE, *GetName()); 189 | } 190 | 191 | InitialTerrainTexture->Source.UnlockMip(0); 192 | } 193 | else 194 | { 195 | UE_LOG(LogVaFog, Warning, TEXT("[%s] Layer [%s] InitialTerrainTexture Source has wrong size: %d x %d, expected: %d x %d"), *VA_FUNC_LINE, *GetName(), SizeX, SizeY, SourceW, SourceH); 196 | } 197 | #else 198 | UE_LOG(LogVaFog, Error, TEXT("[%s] Layer [%s] Can't load platform data for initial terrain layer"), *VA_FUNC_LINE, *GetName()); 199 | #endif 200 | } 201 | } 202 | } 203 | 204 | void AVaFogTerrainLayer::OnAddFogAgent(UVaFogAgentComponent* InFogAgent) 205 | { 206 | bUpdateRequired = true; 207 | } 208 | 209 | void AVaFogTerrainLayer::OnRemoveFogAgent(UVaFogAgentComponent* InFogAgent) 210 | { 211 | UpdateObstacle(InFogAgent, false); 212 | 213 | bUpdateRequired = true; 214 | } 215 | 216 | void AVaFogTerrainLayer::OnRemoveFogBlockingVolume(AVaFogBlockingVolume* InFogBlockingVolume) 217 | { 218 | // Force update layer state for blocking volumes 219 | UpdateLayer(true); 220 | } 221 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogAgentComponent.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "VaFogTypes.h" 6 | 7 | #include "Components/SceneComponent.h" 8 | 9 | #include "VaFogAgentComponent.generated.h" 10 | 11 | UCLASS(ClassGroup = (VAFogOfWar), editinlinenew, meta = (BlueprintSpawnableComponent)) 12 | class VAFOGOFWAR_API UVaFogAgentComponent : public USceneComponent 13 | { 14 | GENERATED_UCLASS_BODY() 15 | 16 | public: 17 | virtual void InitializeComponent() override; 18 | virtual void UninitializeComponent() override; 19 | 20 | virtual void BeginPlay() override; 21 | virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override; 22 | 23 | #if WITH_EDITORONLY_DATA 24 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 25 | virtual void OnRegister() override; 26 | #endif 27 | 28 | public: 29 | /** Set agent enabled (or disable it) */ 30 | UFUNCTION(BlueprintCallable, Category = "VaFog|Agent") 31 | void EnableAgent(bool bEnable = true); 32 | 33 | /** Set agent disabled */ 34 | UFUNCTION(BlueprintCallable, Category = "VaFog|Agent") 35 | void DisableAgent(); 36 | 37 | /** Check agent status */ 38 | UFUNCTION(BlueprintCallable, Category = "VaFog|Agent") 39 | bool IsAgentEnabled() const; 40 | 41 | /** Set agent new vision radius */ 42 | UFUNCTION(BlueprintCallable, Category = "VaFog|Agent") 43 | void SetVisionRadius(int32 NewVisionRadius); 44 | 45 | /** Set agent new height level */ 46 | UFUNCTION(BlueprintCallable, Category = "VaFog|Agent") 47 | void SetHeightLevel(EVaFogHeightLevel NewHeightLevel); 48 | 49 | public: 50 | /** Is agent is enabled by default? */ 51 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VaFog Agent") 52 | bool bAgentEnabled; 53 | 54 | /** Fog layer we're scouting by the agent */ 55 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VaFog Agent") 56 | TSet TargetChannels; 57 | 58 | /** Radius strategy used for fog interaction */ 59 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VaFog Agent") 60 | EVaFogRadiusStrategy RadiusStrategy; 61 | 62 | /** Agent vision radius in cm (set 0 to use single cell vision) */ 63 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VaFog Agent") 64 | int32 VisionRadius; 65 | 66 | /** Height level where agent is standing */ 67 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VaFog Agent") 68 | EVaFogHeightLevel HeightLevel; 69 | 70 | private: 71 | /** Update agent status with Fog Controller */ 72 | void UpdateAgentRegistration(); 73 | 74 | #if WITH_EDITORONLY_DATA 75 | /** Utility function that updates which texture is displayed on the sprite dependent on the properties of the Component. */ 76 | void UpdateSpriteTexture(); 77 | #endif 78 | }; 79 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogBlockingVolume.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "VaFogTypes.h" 6 | 7 | #include "CoreMinimal.h" 8 | #include "GameFramework/Volume.h" 9 | 10 | #include "VaFogBlockingVolume.generated.h" 11 | 12 | class AVaFogLayer; 13 | 14 | UCLASS() 15 | class VAFOGOFWAR_API AVaFogBlockingVolume : public AVolume 16 | { 17 | GENERATED_UCLASS_BODY() 18 | 19 | //~ Begin AActor Interface 20 | virtual void PostLoad() override; 21 | virtual void PostActorCreated() override; 22 | virtual void OnConstruction(const FTransform& Transform) override; 23 | virtual void Destroyed() override; 24 | //~ End AActor Interface 25 | 26 | #if WITH_EDITOR 27 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 28 | #endif 29 | 30 | private: 31 | #if WITH_EDITORONLY_DATA 32 | UPROPERTY() 33 | UBillboardComponent* SpriteComponent; 34 | #endif 35 | 36 | public: 37 | /** Target layer to paint on */ 38 | UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "VaFog Blocking Volume") 39 | AVaFogLayer* Layer; 40 | 41 | /** Height level where agent is standing */ 42 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "VaFog Blocking Volume") 43 | EVaFogHeightLevel HeightLevel; 44 | 45 | /** Volumes will affect terrain level being sorted by Priority */ 46 | UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "VaFog Blocking Volume") 47 | int32 Priority; 48 | 49 | protected: 50 | /** Process volume bounds and apply its influence into terrain map */ 51 | void UpdateTargetLayer(); 52 | 53 | ////////////////////////////////////////////////////////////////////////// 54 | // Debug 55 | 56 | public: 57 | /** Debug fog layer affect */ 58 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Debug") 59 | bool bDebugVolume; 60 | }; 61 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogBoundsVolume.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "GameFramework/Volume.h" 7 | 8 | #include "VaFogBoundsVolume.generated.h" 9 | 10 | UCLASS() 11 | class VAFOGOFWAR_API AVaFogBoundsVolume : public AVolume 12 | { 13 | GENERATED_UCLASS_BODY() 14 | 15 | //~ Begin AActor Interface 16 | virtual void OnConstruction(const FTransform& Transform) override; 17 | 18 | virtual void PostInitializeComponents() override; 19 | virtual void Destroyed() override; 20 | 21 | virtual bool ShouldTickIfViewportsOnly() const override; 22 | virtual void Tick(float DeltaTime) override; 23 | //~ End AActor Interface 24 | 25 | #if WITH_EDITOR 26 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 27 | #endif 28 | 29 | private: 30 | #if WITH_EDITORONLY_DATA 31 | UPROPERTY() 32 | UBillboardComponent* SpriteComponent; 33 | #endif 34 | 35 | protected: 36 | /** Cache volume transform from scene and fog grid resolution from settings */ 37 | UFUNCTION(BlueprintCallable, Category = "VaFog|Bounds") 38 | void UpdateVolumeTransform(); 39 | 40 | public: 41 | /** Get fog cell size in units */ 42 | UFUNCTION(BlueprintCallable, Category = "VaFog|Bounds") 43 | FVector GetCellExtent() const; 44 | 45 | /** Convert world location of vector to layer one */ 46 | UFUNCTION(BlueprintCallable, Category = "VaFog|Bounds") 47 | FIntPoint TransformWorldToLayer(const FVector& AgentLocation) const; 48 | 49 | /** Convert distance in wolrd units to the layer one */ 50 | UFUNCTION(BlueprintCallable, Category = "VaFog|Bounds") 51 | int32 ScaleDistanceToLayer(const int32 Distance) const; 52 | 53 | private: 54 | int32 CachedFogLayerResolution; 55 | FVector CachedCellExtent; 56 | 57 | /** Shift that should be applied to transform layer coordinates into texture coordinates */ 58 | int32 LayerToTextureShift; 59 | 60 | /** World to layer transform */ 61 | FTransform VolumeTransform; 62 | 63 | ////////////////////////////////////////////////////////////////////////// 64 | // Debug 65 | 66 | protected: 67 | /** Show grid for this layer */ 68 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Debug") 69 | bool bDebugVolume; 70 | 71 | /** Time to show debug grid */ 72 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Debug") 73 | float DebugTime; 74 | 75 | protected: 76 | void DrawDebugGrid(); 77 | 78 | public: 79 | /** Get nearest cell center */ 80 | FVector SnapWorldToGrid(const FVector& InLocation) const; 81 | }; 82 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogController.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "VaFogTypes.h" 6 | 7 | #include "Engine/Engine.h" 8 | 9 | #include "VaFogController.generated.h" 10 | 11 | class AVaFogBoundsVolume; 12 | class UVaFogAgentComponent; 13 | class AVaFogLayer; 14 | 15 | UCLASS() 16 | class VAFOGOFWAR_API UVaFogController : public UObject 17 | { 18 | GENERATED_UCLASS_BODY() 19 | 20 | public: 21 | /** Direct access to fog controller */ 22 | static UVaFogController* Get(const UObject* WorldContextObject, EGetWorldErrorMode ErrorMode = EGetWorldErrorMode::Assert); 23 | 24 | void OnFogLayerAdded(AVaFogLayer* InFogLayer); 25 | void OnFogLayerRemoved(AVaFogLayer* InFogLayer); 26 | 27 | void OnFogAgentAdded(UVaFogAgentComponent* InFogAgent); 28 | void OnFogAgentRemoved(UVaFogAgentComponent* InFogAgent); 29 | 30 | /** Get fog layer by its channel */ 31 | AVaFogLayer* GetFogLayer(EVaFogLayerChannel LayerChannel) const; 32 | 33 | private: 34 | /** Registered fog layers */ 35 | TArray> FogLayers; 36 | }; 37 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogLayer.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "VaFogTypes.h" 6 | 7 | #include "VaFogRadiusStrategy.h" 8 | 9 | #include "GameFramework/Actor.h" 10 | #include "RHI.h" 11 | #include "RHITypes.h" 12 | 13 | #include "VaFogLayer.generated.h" 14 | 15 | class UTextureRenderTarget2D; 16 | class UTexture2D; 17 | class UMaterialInterface; 18 | 19 | class AVaFogBoundsVolume; 20 | class UVaFogAgentComponent; 21 | class AVaFogTerrainLayer; 22 | class AVaFogBlockingVolume; 23 | 24 | struct FFogTexel2x2 25 | { 26 | uint8 p11, p12; 27 | uint8 p21, p22; 28 | 29 | bool operator==(const FFogTexel2x2& p) const 30 | { 31 | return p11 == p.p11 && 32 | p12 == p.p12 && 33 | p21 == p.p21 && 34 | p22 == p.p22; 35 | } 36 | 37 | FString ToString() const 38 | { 39 | return FString::Printf(TEXT("%d %d %d %d"), p11, p12, p21, p22); 40 | } 41 | }; 42 | 43 | struct FFogTexel4x4 44 | { 45 | uint8 pixels[4][4]; 46 | }; 47 | 48 | struct FFogOctantTransform 49 | { 50 | int8 xx, xy, yx, yy; 51 | 52 | bool operator==(const FFogOctantTransform& p) const 53 | { 54 | return xx == p.xx && 55 | xy == p.xy && 56 | yx == p.yx && 57 | yy == p.yy; 58 | } 59 | }; 60 | 61 | struct FFogDrawContext 62 | { 63 | uint8* TargetBuffer; 64 | int32 CenterX; 65 | int32 CenterY; 66 | int32 Radius; 67 | EVaFogRadiusStrategy RadiusStrategy; 68 | EVaFogHeightLevel HeightLevel; 69 | uint8 RevealLevel; 70 | 71 | FFogDrawContext() 72 | : TargetBuffer(nullptr) 73 | , CenterX(0) 74 | , CenterY(0) 75 | , Radius(1) 76 | , RadiusStrategy(EVaFogRadiusStrategy::Circle) 77 | , HeightLevel(EVaFogHeightLevel::HL_1) 78 | , RevealLevel(0xFF) 79 | { 80 | } 81 | }; 82 | 83 | UCLASS(Blueprintable, BlueprintType) 84 | class VAFOGOFWAR_API AVaFogLayer : public AActor 85 | { 86 | GENERATED_BODY() 87 | 88 | public: 89 | AVaFogLayer(const FObjectInitializer& ObjectInitializer); 90 | 91 | virtual void PostLoad() override; 92 | virtual void PostActorCreated() override; 93 | virtual void PostInitializeComponents() override; 94 | virtual void Destroyed() override; 95 | virtual void BeginPlay() override; 96 | 97 | protected: 98 | virtual void InitInternalBuffers(); 99 | virtual void CleanupInternalBuffers(); 100 | 101 | public: 102 | virtual void Tick(float DeltaTime) override; 103 | 104 | /** Update whole layer state */ 105 | virtual void UpdateLayer(bool bForceFullUpdate = false); 106 | 107 | protected: 108 | /** Flush buffers to textures */ 109 | void UpdateBuffers(); 110 | 111 | /** Process agents info and update FoW map */ 112 | void UpdateAgents(); 113 | 114 | /** Process volumes info and update FoW map */ 115 | void UpdateBlockingVolumes(); 116 | 117 | /** Update single obstacle agent as event-based process */ 118 | void UpdateObstacle(UVaFogAgentComponent* FogAgent, bool bObstacleIsActive); 119 | 120 | /** Process manual upscaling from 128 to 512 */ 121 | void UpdateUpscaleBuffer(); 122 | 123 | private: 124 | /** Draw circle shaded with obstacles: http://www.adammil.net/blog/v125_Roguelike_Vision_Algorithms.html */ 125 | void DrawVisionCircle(const FFogDrawContext& DrawContext); 126 | 127 | /** Based on Shadowcasting algorithm from SquidLib by Eben Howard, http://www.roguebasin.com/index.php?title=Improved_Shadowcasting_in_Java */ 128 | void DrawFieldOfView(const FFogDrawContext& DrawContext, int32 Y, float Start, float End, FFogOctantTransform Transform); 129 | 130 | /** Set desired point as visible in TargetBuffer */ 131 | void Reveal(const FFogDrawContext& DrawContext, int32 X, int32 Y); 132 | 133 | /** Check obstacle buffer in desired point */ 134 | bool IsBlocked(int32 X, int32 Y, EVaFogHeightLevel HeightLevel); 135 | 136 | /** Rasterize circle with Bresenham's Midpoint circle algorithm, see https://en.wikipedia.org/wiki/Midpoint_circle_algorithm */ 137 | void DrawCircle(const FFogDrawContext& DrawContext); 138 | void Plot4Points(const FFogDrawContext& DrawContext, int32 X, int32 Y); 139 | void DrawHorizontalLine(uint8* TargetBuffer, int32 x0, int32 y0, int32 x1); 140 | 141 | /** Read pixel with desired position and constuct texel based on its neighbors */ 142 | FFogTexel2x2 FetchTexelFromSource(int32 W, int32 H); 143 | 144 | public: 145 | /** Defines which refresh logic will be used: permanent drawing or runtime visible area */ 146 | UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Fog of War") 147 | EVaFogLayerChannel LayerChannel; 148 | 149 | /** Layer that provides world information */ 150 | UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Fog of War") 151 | AVaFogTerrainLayer* TerrainLayer; 152 | 153 | /** Fog bounds volume we're linked in */ 154 | UPROPERTY(BlueprintReadOnly, EditAnywhere, Category = "Fog of War") 155 | AVaFogBoundsVolume* BoundsVolume; 156 | 157 | public: 158 | void AddFogAgent(UVaFogAgentComponent* InFogAgent); 159 | void RemoveFogAgent(UVaFogAgentComponent* InFogAgent); 160 | 161 | void AddFogBlockingVolume(AVaFogBlockingVolume* InFogBlockingVolume); 162 | void RemoveFogBlockingVolume(AVaFogBlockingVolume* InFogBlockingVolume); 163 | 164 | protected: 165 | virtual void OnAddFogAgent(UVaFogAgentComponent* InFogAgent){}; 166 | virtual void OnRemoveFogAgent(UVaFogAgentComponent* InFogAgent){}; 167 | virtual void OnAddFogBlockingVolume(AVaFogBlockingVolume* InFogBlockingVolume){}; 168 | virtual void OnRemoveFogBlockingVolume(AVaFogBlockingVolume* InFogBlockingVolume){}; 169 | 170 | protected: 171 | /** Registered fog agents for layer */ 172 | UPROPERTY(VisibleAnywhere, Category = "Fog of War") 173 | TArray FogAgents; 174 | 175 | /** Registered fog blocking volumes for layer */ 176 | TArray> FogBlockingVolumes; 177 | 178 | /** Radius strategy instances */ 179 | TMap RadiusStrategies; 180 | 181 | /** Is upscaling enabled or original buffer used */ 182 | bool bUseUpscaleBuffer; 183 | 184 | /** Default source buffer state */ 185 | uint8 ZeroBufferValue; 186 | 187 | /** Original layer texture on CPU */ 188 | uint8* SourceBuffer; 189 | 190 | /** Upscaled layer texture on CPU */ 191 | uint8* UpscaleBuffer; 192 | 193 | /** Separate buffer used to track obstacles and height levels (same size as source buffer) */ 194 | uint8* TerrainBuffer; 195 | 196 | /** Cached data helpers for original texture */ 197 | int32 SourceW; 198 | int32 SourceH; 199 | int32 SourceBufferLength; 200 | FUpdateTextureRegion2D SourceUpdateRegion; 201 | 202 | /** Cached data helpers for upscale texture */ 203 | int32 UpscaleW; 204 | int32 UpscaleH; 205 | int32 UpscaleBufferLength; 206 | FUpdateTextureRegion2D UpscaleUpdateRegion; 207 | 208 | ////////////////////////////////////////////////////////////////////////// 209 | // Gameplay 210 | 211 | public: 212 | /** Is location is revealed on layer */ 213 | UFUNCTION(BlueprintCallable, Category = "VaFog|FogLayer") 214 | bool IsLocationRevealed(const FVector& InLocation) const; 215 | 216 | /** Cover all the layer with fog */ 217 | UFUNCTION(BlueprintCallable, Category = "VaFog|FogLayer") 218 | void CoverAll(); 219 | 220 | /** Reveal all fog at layer (clear it) */ 221 | UFUNCTION(BlueprintCallable, Category = "VaFog|FogLayer") 222 | void RevealAll(); 223 | 224 | ////////////////////////////////////////////////////////////////////////// 225 | // Debug 226 | 227 | protected: 228 | /** Show agents vision radius for this layer */ 229 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Debug") 230 | bool bDebugAgents; 231 | 232 | /** Color to draw */ 233 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Debug") 234 | FColor DebugAgentsColor; 235 | 236 | /** Enable source and upscale buffer to texture drawing */ 237 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Debug") 238 | bool bDebugBuffers; 239 | 240 | public: 241 | /** Low-res FoW source buffer as image (check bDebugBuffers) */ 242 | UPROPERTY(Transient, BlueprintReadOnly, Category = "Fog of War") 243 | UTexture2D* SourceTexture; 244 | 245 | /** Upscaled FoW buffer as image (check bDebugUpscaleTexture) */ 246 | UPROPERTY(Transient, BlueprintReadOnly, Category = "Fog of War") 247 | UTexture2D* UpscaleTexture; 248 | 249 | protected: 250 | /** Render buffer into debug texture */ 251 | void UpdateTextureFromBuffer(UTexture2D* DestinationTexture, uint8* SrcBuffer, int32 SrcBufferLength, FUpdateTextureRegion2D& UpdateTextureRegion); 252 | 253 | #if WITH_EDITORONLY_DATA 254 | UPROPERTY() 255 | UBillboardComponent* SpriteComponent; 256 | #endif 257 | }; 258 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogLibrary.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "Kismet/BlueprintFunctionLibrary.h" 6 | 7 | #include "VaFogTypes.h" 8 | 9 | #include "VaFogLibrary.generated.h" 10 | 11 | UCLASS() 12 | class VAFOGOFWAR_API UVaFogLibrary : public UBlueprintFunctionLibrary 13 | { 14 | GENERATED_UCLASS_BODY() 15 | 16 | public: 17 | /** For mobile platforms that switch vertical axis source buffer should be flipped */ 18 | UFUNCTION(BlueprintPure, Category = "VaFog|Tools") 19 | static bool IsRHINeedsToSwitchVerticalAxis(); 20 | 21 | /** Used for debug quads colorings */ 22 | static FColor GetDebugColorForHeightLevel(EVaFogHeightLevel HeightLevel); 23 | }; 24 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogOfWar.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | #include "Modules/ModuleManager.h" 7 | 8 | class UVaFogSettings; 9 | class UVaFogController; 10 | 11 | /** 12 | * VaFogOfWar Module 13 | */ 14 | class FVaFogOfWarModule : public IModuleInterface 15 | { 16 | public: 17 | /** IModuleInterface implementation */ 18 | virtual void StartupModule() override; 19 | virtual void ShutdownModule() override; 20 | 21 | /** 22 | * Singleton-like access to this module's interface. This is just for convenience! 23 | * Beware of calling this during the shutdown phase, though. Your module might have been unloaded already. 24 | * 25 | * @return Returns singleton instance, loading the module on demand if needed 26 | */ 27 | static inline FVaFogOfWarModule& Get() 28 | { 29 | return FModuleManager::LoadModuleChecked("VaFogOfWar"); 30 | } 31 | 32 | /** 33 | * Checks to see if this module is loaded and ready. It is only valid to call Get() if IsAvailable() returns true. 34 | * 35 | * @return True if the module is loaded and ready to use 36 | */ 37 | static inline bool IsAvailable() 38 | { 39 | return FModuleManager::Get().IsModuleLoaded("VaFogOfWar"); 40 | } 41 | 42 | /** Getter for internal settings object to support runtime configuration changes */ 43 | UVaFogSettings* GetSettings() const; 44 | 45 | /** Get global fog controller */ 46 | UVaFogController* GetFogController(UWorld* World) const; 47 | 48 | private: 49 | /** Module settings */ 50 | UVaFogSettings* ModuleSettings; 51 | 52 | /** Fog controllers (one for each World we have) */ 53 | TMap FogControllers; 54 | }; 55 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogRadiusStrategy.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "CoreMinimal.h" 6 | 7 | /** 8 | * Abstract strategy pattern class that allows alternate radius calculations to be used. 9 | * This allows for circular, square, diamond, or any custom formula for radius. 10 | */ 11 | class FVaFogRadiusStrategy 12 | { 13 | public: 14 | FVaFogRadiusStrategy(){}; 15 | virtual ~FVaFogRadiusStrategy() = 0; 16 | 17 | virtual bool IsInRadius(int32 CenterX, int32 CenterY, int32 Radius, int32 X, int32 Y) { return false; }; 18 | }; 19 | 20 | class FVaFogRadiusStrategy_Circle : public FVaFogRadiusStrategy 21 | { 22 | public: 23 | virtual bool IsInRadius(int32 CenterX, int32 CenterY, int32 Radius, int32 X, int32 Y) override; 24 | }; 25 | 26 | class FVaFogRadiusStrategy_Square : public FVaFogRadiusStrategy 27 | { 28 | public: 29 | virtual bool IsInRadius(int32 CenterX, int32 CenterY, int32 Radius, int32 X, int32 Y) override; 30 | }; 31 | 32 | /** 33 | * Radius treated as diameter here 34 | */ 35 | class FVaFogRadiusStrategy_SquareStepped : public FVaFogRadiusStrategy 36 | { 37 | public: 38 | virtual bool IsInRadius(int32 CenterX, int32 CenterY, int32 Radius, int32 X, int32 Y) override; 39 | }; 40 | 41 | /** Shared reference to the radius strategy */ 42 | using FVaFogRadiusStrategyRef = TSharedRef; 43 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogSettings.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "VaFogSettings.generated.h" 6 | 7 | UCLASS(config = Engine, defaultconfig) 8 | class VAFOGOFWAR_API UVaFogSettings : public UObject 9 | { 10 | GENERATED_UCLASS_BODY() 11 | 12 | virtual void PostInitProperties() override; 13 | 14 | #if WITH_EDITOR 15 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 16 | #endif 17 | 18 | private: 19 | void SanatizeFogLayerResolution(); 20 | 21 | public: 22 | /** Must be power of two */ 23 | UPROPERTY(Config, EditAnywhere, BlueprintReadOnly, Category = "VA Fog of War Settings") 24 | int32 FogLayerResolution; 25 | }; 26 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogTerrainLayer.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "VaFogLayer.h" 6 | 7 | #include "VaFogTerrainLayer.generated.h" 8 | 9 | /** 10 | * Pre-configured layer for handling terrain and obstacles 11 | */ 12 | UCLASS() 13 | class VAFOGOFWAR_API AVaFogTerrainLayer : public AVaFogLayer 14 | { 15 | GENERATED_BODY() 16 | 17 | public: 18 | AVaFogTerrainLayer(const FObjectInitializer& ObjectInitializer); 19 | 20 | virtual void InitInternalBuffers() override; 21 | virtual void CleanupInternalBuffers() override; 22 | 23 | virtual void BeginPlay() override; 24 | 25 | #if WITH_EDITOR 26 | virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override; 27 | #endif 28 | 29 | virtual void UpdateLayer(bool bForceFullUpdate) override; 30 | 31 | /** Get initial terrain state at location */ 32 | UFUNCTION(BlueprintCallable, Category = "VaFog|Terrain") 33 | EVaFogHeightLevel GetHeightLevelAtLocation(const FVector& Location) const; 34 | 35 | /** Get initial terrain state at agent (internal) location */ 36 | UFUNCTION(BlueprintCallable, Category = "VaFog|Terrain") 37 | EVaFogHeightLevel GetHeightLevelAtAgentLocation(const FIntPoint& AgentLocation) const; 38 | 39 | public: 40 | /** Initial terrain state to be applied into the source buffer */ 41 | UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Fog of War Terrain") 42 | UTexture2D* InitialTerrainTexture; 43 | 44 | protected: 45 | /** Process input texture and load it into memory as obstacles buffer */ 46 | void LoadTerrainBufferFromTexture(); 47 | 48 | /** Initial terrain buffer for navigation */ 49 | uint8* InitialTerrainBuffer; 50 | 51 | /** Is full update required on next tick */ 52 | bool bUpdateRequired; 53 | 54 | protected: 55 | virtual void OnAddFogAgent(UVaFogAgentComponent* InFogAgent) override; 56 | virtual void OnRemoveFogAgent(UVaFogAgentComponent* InFogAgent) override; 57 | virtual void OnRemoveFogBlockingVolume(AVaFogBlockingVolume* InFogBlockingVolume) override; 58 | }; 59 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/Public/VaFogTypes.h: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | #pragma once 4 | 5 | #include "VaFogTypes.generated.h" 6 | 7 | UENUM(BlueprintType) 8 | enum class EVaFogLayerChannel : uint8 9 | { 10 | /** "Black" fog of war */ 11 | Permanent, 12 | /** "Grey" fog of war */ 13 | Scouting, 14 | /** Vision obstacles */ 15 | Terrain 16 | }; 17 | 18 | UENUM(BlueprintType) 19 | enum class EVaFogRadiusStrategy : uint8 20 | { 21 | Circle, 22 | Square, 23 | SquareStepped, // Radius treated as diameter (used for obstalces mostly) 24 | 25 | Max UMETA(Hidden) 26 | }; 27 | 28 | /** 29 | * There are six different height levels that have gameplay relevance. One height level can be connected to another by a stair (passable) or a cliff (obstructed). 30 | */ 31 | // clang-format off 32 | UENUM(BlueprintType) 33 | enum class EVaFogHeightLevel : uint8 34 | { 35 | HL_INVALID = 0 UMETA(Hidden), 36 | HL_1 = 0x01 UMETA(Hidden), 37 | HL_2 = 0x02 UMETA(DisplayName = "River"), // River 38 | HL_3 = 0x04 UMETA(DisplayName = "Lowground"), // Lowground 39 | HL_4 = 0x08 UMETA(DisplayName = "Highground"), // Highground 40 | HL_5 = 0x10 UMETA(DisplayName = "Elevated"), // Elevated 41 | HL_6 = 0x20 UMETA(DisplayName = "Cliffs"), // Cliffs 42 | HL_7 = 0x40 UMETA(DisplayName = "Valley"), // Valley 43 | HL_8 = 0x80 UMETA(Hidden) 44 | }; 45 | // clang-format on 46 | -------------------------------------------------------------------------------- /Source/VaFogOfWar/VaFogOfWar.Build.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Vladimir Alyamkin. All Rights Reserved. 2 | 3 | using UnrealBuildTool; 4 | 5 | public class VaFogOfWar : ModuleRules 6 | { 7 | public VaFogOfWar(ReadOnlyTargetRules Target) : base(Target) 8 | { 9 | PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs; 10 | 11 | PublicDependencyModuleNames.AddRange( 12 | new string[] 13 | { 14 | "Core", 15 | "RenderCore", 16 | "RHI" 17 | } 18 | ); 19 | 20 | PrivateDependencyModuleNames.AddRange( 21 | new string[] 22 | { 23 | "CoreUObject", 24 | "Engine" 25 | } 26 | ); 27 | 28 | PublicDefinitions.Add("WITH_VA_FOGOFWAR=1"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /VaFogOfWar.uplugin: -------------------------------------------------------------------------------- 1 | { 2 | "FileVersion": 3, 3 | "Version": 1, 4 | "VersionName": "1.0", 5 | "EngineVersion" : "5.4", 6 | "FriendlyName": "VaFogOfWar", 7 | "Description": "VaFogOfWar", 8 | "Category": "Rendering", 9 | "CreatedBy": "Vladimir Alyamkin", 10 | "CreatedByURL": "https://alyamkin.com", 11 | "DocsURL": "", 12 | "MarketplaceURL": "com.epicgames.launcher://ue/marketplace/content/cf7668a20dde4ef6937eaab417bf55cf", 13 | "SupportURL": "", 14 | "CanContainContent": false, 15 | "IsBetaVersion": false, 16 | "Installed": false, 17 | "Modules": [ 18 | { 19 | "Name": "VaFogOfWar", 20 | "Type": "Runtime", 21 | "LoadingPhase": "PreDefault", 22 | "WhitelistPlatforms": [ 23 | "Win64", 24 | "Mac", 25 | "IOS", 26 | "Android", 27 | "Linux" 28 | ] 29 | } 30 | ] 31 | } 32 | --------------------------------------------------------------------------------